From c1e5474f1e3edf84a511a6b2e1d238f0e15ddda0 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Fri, 20 Apr 2012 11:50:23 +0100 Subject: [PATCH] add provider project source. --- .../CustomOAuthProviderTokenServices.java | 15 + ...stomProtectedResourceProcessingFilter.java | 154 ++ .../mifosng/oauth/CustomTokenServices.java | 63 + .../oauth/MifosNgConsumerDetailsService.java | 44 + ...sAwareSecurityContextPerRequestFilter.java | 82 + ...emovableInMemoryProviderTokenServices.java | 11 + ...AdjustLoanTransactionCommandValidator.java | 52 + ...CalculateLoanScheduleCommandValidator.java | 209 ++ .../platform/CalculationPlatformService.java | 14 + .../CalculationPlatformServiceImpl.java | 142 ++ .../platform/CannotDeleteLoanException.java | 9 + .../ChangePasswordCommandValidator.java | 41 + .../platform/ClosedLoanComparator.java | 15 + .../EnrollClientCommandValidator.java | 60 + .../platform/ImportPlatformService.java | 21 + .../platform/ImportPlatformServiceImpl.java | 658 +++++++ .../mifosng/platform/InvalidSqlException.java | 17 + .../platform/LoanProductValidator.java | 219 +++ .../LoanStateTransitionCommandValidator.java | 42 + .../platform/LoanTransactionValidator.java | 47 + .../org/mifosng/platform/OfficeValidator.java | 56 + .../mifosng/platform/ReadPlatformService.java | 100 + .../platform/ReadPlatformServiceImpl.java | 1694 +++++++++++++++++ .../org/mifosng/platform/RoleValidator.java | 66 + .../org/mifosng/platform/Specifications.java | 105 + ...SubmitLoanApplicationCommandValidator.java | 64 + .../org/mifosng/platform/UserValidator.java | 71 + .../platform/WritePlatformService.java | 94 + ...WritePlatformServiceJpaRepositoryImpl.java | 1142 +++++++++++ .../platform/client/domain/Client.java | 88 + .../client/domain/ClientRepository.java | 8 + .../mifosng/platform/client/domain/Note.java | 116 ++ .../client/domain/NoteRepository.java | 11 + .../currency/domain/ApplicationCurrency.java | 63 + .../domain/ApplicationCurrencyRepository.java | 9 + .../currency/domain/MonetaryCurrency.java | 38 + .../platform/currency/domain/Money.java | 240 +++ .../ApplicationDomainRuleException.java | 29 + .../ClientNotAuthenticatedException.java | 5 + .../exceptions/InvalidSignupException.java | 9 + .../NewDataValidationException.java | 25 + .../exceptions/NoAuthorizationException.java | 25 + .../AbstractAuditableCustom.java | 142 ++ .../infrastructure/AuditorAwareImpl.java | 37 + .../BasicPasswordEncodablePlatformUser.java | 73 + .../CustomAuthenticationFailureHandler.java | 124 ++ .../DefaultPlatformPasswordEncoder.java | 26 + .../platform/infrastructure/EmailDetail.java | 32 + .../FirstTimeLoginController.java | 106 ++ ...DetectionAuthenticationSuccessHandler.java | 96 + .../FirstTimeLoginFormBean.java | 77 + .../GmailBackedPlatformEmailService.java | 45 + .../PentahoReportingController.java | 167 ++ .../PlatformEmailSendException.java | 8 + .../infrastructure/PlatformEmailService.java | 8 + .../PlatformPasswordEncoder.java | 8 + .../platform/infrastructure/PlatformUser.java | 15 + .../RandomPasswordGenerator.java | 19 + .../infrastructure/SignupController.java | 130 ++ .../infrastructure/SignupFormBean.java | 49 + .../UsernameAlreadyExistsException.java | 7 + .../loan/domain/AmortizationMethod.java | 36 + ...liningBalanceInterestRebateCalculator.java | 130 ++ ...quivalentFlatInterestRebateCalculator.java | 70 + ...valentInterestRebateCalculatorFactory.java | 34 + .../DefaultLoanLifecycleStateMachine.java | 113 ++ .../loan/domain/DerivedLoanDataProcessor.java | 489 +++++ .../platform/loan/domain/InterestMethod.java | 32 + .../loan/domain/InterestRebateCalculator.java | 19 + .../InterestRebateCalculatorFactory.java | 9 + .../loan/domain/InvalidLoanTimelineDate.java | 15 + .../mifosng/platform/loan/domain/Loan.java | 947 +++++++++ .../platform/loan/domain/LoanBuilder.java | 45 + .../platform/loan/domain/LoanEvent.java | 8 + .../domain/LoanLifecycleStateMachine.java | 7 + .../loan/domain/LoanPayoffSummary.java | 76 + .../platform/loan/domain/LoanProduct.java | 140 ++ ...MinimumRepaymentScheduleRelatedDetail.java | 15 + .../loan/domain/LoanProductRelatedDetail.java | 179 ++ .../loan/domain/LoanProductRepository.java | 9 + .../LoanRepaymentScheduleInstallment.java | 166 ++ .../platform/loan/domain/LoanRepository.java | 9 + .../platform/loan/domain/LoanStatus.java | 72 + .../loan/domain/LoanStatusRepository.java | 8 + .../platform/loan/domain/LoanTransaction.java | 161 ++ .../domain/LoanTransactionRepository.java | 9 + .../loan/domain/LoanTransactionType.java | 42 + .../loan/domain/PeriodFrequencyType.java | 38 + ...qualInstallmentsLoanScheduleGenerator.java | 268 +++ ...DecliningBalanceLoanScheduleGenerator.java | 107 ++ .../DefaultLoanScheduleGeneratorFactory.java | 46 + ...aultPaymentPeriodsInOneYearCalculator.java | 97 + .../domain/DefaultScheduledDateGenerator.java | 86 + .../domain/FlatLoanScheduleGenerator.java | 96 + .../domain/LoanScheduleGenerator.java | 14 + .../domain/LoanScheduleGeneratorFactory.java | 10 + .../PaymentPeriodsInOneYearCalculator.java | 17 + .../domain/ScheduledDateGenerator.java | 19 + .../domain/OauthConsumerDetail.java | 127 ++ .../domain/OauthConsumerDetailRepository.java | 11 + .../platform/organisation/domain/Office.java | 162 ++ .../organisation/domain/OfficeRepository.java | 9 + .../organisation/domain/Organisation.java | 93 + .../domain/OrganisationCurrency.java | 60 + .../domain/OrganisationRepository.java | 7 + .../ApplicationConfigurationResource.java | 197 ++ .../mifosng/platform/rest/ClientResource.java | 232 +++ .../platform/rest/ExtraDataResource.java | 128 ++ .../rest/FlexibleReportingResource.java | 113 ++ .../mifosng/platform/rest/ImportResource.java | 81 + .../platform/rest/ImportTriggerResource.java | 60 + .../platform/rest/LoanOpenResource.java | 104 + .../platform/rest/LoanProductResource.java | 147 ++ .../mifosng/platform/rest/LoanResource.java | 526 +++++ .../mifosng/platform/rest/NoteResource.java | 36 + .../mifosng/platform/rest/OfficeResource.java | 135 ++ .../ProtectedFlexibleReportingResource.java | 155 ++ .../mifosng/platform/rest/TenantResource.java | 38 + .../rest/UserAdmistrationResource.java | 456 +++++ .../mifosng/platform/rest/UserResource.java | 85 + .../AccessConfirmationController.java | 85 + .../security/PlatformUserDetailsService.java | 10 + .../PlatformUserDetailsServiceImpl.java | 24 + .../mifosng/platform/user/domain/AppUser.java | 308 +++ .../user/domain/AppUserRepository.java | 8 + .../user/domain/JpaUserDomainService.java | 93 + .../JpaUserPriviledgeDomainService.java | 114 ++ .../PasswordMustBeDifferentException.java | 5 + .../platform/user/domain/Permission.java | 64 + .../user/domain/PermissionBuilder.java | 245 +++ .../platform/user/domain/PermissionGroup.java | 5 + .../user/domain/PermissionRepository.java | 7 + .../user/domain/PlatformUserRepository.java | 9 + .../mifosng/platform/user/domain/Role.java | 90 + .../platform/user/domain/RoleRepository.java | 8 + .../user/domain/UserDomainService.java | 12 + .../domain/UserPriviledgeDomainService.java | 10 + .../UsernameMustBeDifferentException.java | 5 + .../src/main/resources/META-INF/orm.xml | 12 + .../main/resources/META-INF/persistence.xml | 14 + .../resources/META-INF/spring/appContext.xml | 54 + .../META-INF/spring/infrastructure.xml | 36 + .../META-INF/spring/securityContext.xml | 137 ++ .../META-INF/spring/securityContextOld.xml | 241 +++ .../src/main/resources/ccylist.csv | 164 ++ .../src/main/resources/logback.xml | 32 + ...PESALoanDisbursalsExportSummary.properties | 17 + .../activeLoanSummaryBranch.properties | 18 + .../bi/reports/activeLoansCenter.properties | 21 + .../activeLoansLastInstallment.properties | 20 + .../reports/activeLoansLoanOfficer.properties | 21 + .../reports/activeLoansLoanProduct.properties | 21 + .../reports/activeLoansLoanPurpose.properties | 21 + .../mifos/bi/reports/agingSummary.properties | 37 + .../bi/reports/balanceOutstanding.properties | 29 + ...lanceOutstandingBySourceOfFunds.properties | 16 + .../reports/branchExpectedCashFlow.properties | 21 + .../reports/centerCollectionSheet.properties | 40 + .../centerScheduleLoanOfficer.properties | 18 + .../mifos/bi/reports/clientExit.properties | 21 + .../mifos/bi/reports/clientSummary.properties | 29 + .../closedLoanSummaryBranch.properties | 13 + .../mifos/bi/reports/closedLoans.properties | 44 + .../reports/dormantClientsSummary.properties | 13 + .../reports/dueVsCollectedBranch.properties | 20 + .../dueVsCollectedBranch_es.properties | 20 + .../dueVsCollectedBranch_fr.properties | 20 + .../reports/dueVsCollectedCenter.properties | 21 + .../dueVsCollectedCenter_es.properties | 21 + .../dueVsCollectedCenter_fr.properties | 21 + .../dueVsCollectedLoanOfficer.properties | 20 + .../dueVsCollectedLoanOfficer_es.properties | 20 + .../dueVsCollectedLoanOfficer_fr.properties | 20 + .../mifos/bi/reports/fundsMovement.properties | 23 + .../groupCollectionSheetMPESA.properties | 38 + .../bi/reports/groupsInformation.properties | 23 + .../org/mifos/bi/reports/loanAging.properties | 22 + .../loanClassificationProduct.properties | 20 + .../loanClassificationProduct_es.properties | 20 + .../loanClassificationProduct_fr.properties | 20 + .../bi/reports/loanOfficerDetailed.properties | 71 + .../reports/loanOfficerDetailed_es.properties | 71 + .../reports/loanOfficerDetailed_fr.properties | 71 + ...cerPerformanceSummaryCumulative.properties | 26 + ...PerformanceSummaryCumulative_es.properties | 26 + ...PerformanceSummaryCumulative_fr.properties | 26 + ...rPerformanceSummaryDuringPeriod.properties | 22 + ...rformanceSummaryDuringPeriod_es.properties | 22 + ...rformanceSummaryDuringPeriod_fr.properties | 22 + .../reports/loansPendingApproval.properties | 20 + .../bi/reports/loansToBeDisbursed.properties | 20 + .../mifos/bi/reports/mfiProgress.properties | 72 + .../bi/reports/mfiProgress_es.properties | 72 + .../bi/reports/mfiProgress_fr.properties | 72 + .../bi/reports/mifosTransactions.properties | 25 + .../reports/mifosTransactions_es.properties | 25 + .../reports/mifosTransactions_fr.properties | 25 + .../reports/outreachSummaryBranch.properties | 14 + .../bi/reports/overdueMatureLoans.properties | 34 + .../src/main/webapp/META-INF/context.xml | 6 + .../webapp/WEB-INF/spring/webmvc-config.xml | 43 + .../main/webapp/WEB-INF/static/allinone.css | 54 + .../main/webapp/WEB-INF/static/favicon.ico | Bin 0 -> 318 bytes .../webapp/WEB-INF/static/jquery-1.6.3.min.js | 4 + .../jquery-ui-1.8.16.custom.min.js | 791 ++++++++ .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes .../images/ui-bg_flat_55_fbec88_40x100.png | Bin 0 -> 182 bytes .../images/ui-bg_glass_75_d0e5f5_1x400.png | Bin 0 -> 124 bytes .../images/ui-bg_glass_85_dfeffc_1x400.png | Bin 0 -> 123 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 119 bytes .../ui-bg_gloss-wave_55_5c9ccc_500x100.png | Bin 0 -> 3457 bytes .../ui-bg_inset-hard_100_f5f8f9_1x100.png | Bin 0 -> 104 bytes .../ui-bg_inset-hard_100_fcfdfd_1x100.png | Bin 0 -> 112 bytes .../images/ui-icons_217bc0_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_469bdd_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_6da8d5_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_d8e7f3_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_f9bd01_256x240.png | Bin 0 -> 5355 bytes .../redmond/jquery-ui-1.8.16.custom.css | 568 ++++++ .../ui-bg_diagonals-thick_18_b81900_40x40.png | Bin 0 -> 260 bytes .../ui-bg_diagonals-thick_20_666666_40x40.png | Bin 0 -> 251 bytes .../images/ui-bg_flat_10_000000_40x100.png | Bin 0 -> 178 bytes .../images/ui-bg_glass_100_f6f6f6_1x400.png | Bin 0 -> 104 bytes .../images/ui-bg_glass_100_fdf5ce_1x400.png | Bin 0 -> 125 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes .../ui-bg_gloss-wave_35_f6a828_500x100.png | Bin 0 -> 3762 bytes .../ui-bg_highlight-soft_100_eeeeee_1x100.png | Bin 0 -> 90 bytes .../ui-bg_highlight-soft_75_ffe45c_1x100.png | Bin 0 -> 129 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_228ef1_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_ef8c08_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_ffd27a_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_ffffff_256x240.png | Bin 0 -> 4369 bytes .../ui-lightness/jquery-ui-1.8.16.custom.css | 568 ++++++ .../WEB-INF/views/access_confirmation.jsp | 44 + .../main/webapp/WEB-INF/views/common-head.jsp | 15 + .../webapp/WEB-INF/views/firsttimelogin.jsp | 60 + .../src/main/webapp/WEB-INF/views/index.jsp | 19 + .../src/main/webapp/WEB-INF/views/login.jsp | 70 + .../webapp/WEB-INF/views/sessionTimeout.jsp | 45 + .../webapp/WEB-INF/views/tenant/signup.jsp | 67 + .../WEB-INF/views/tenant/signupsuccess.jsp | 67 + .../src/main/webapp/WEB-INF/web.xml | 93 + .../domain/DerivedLoanDataProcessorTest.java | 376 ++++ ...anRepaymentScheduleInstallmentBuilder.java | 41 + .../loan/domain/LoanTransactionBuilder.java | 21 + .../loan/domain/MonetaryCurrencyBuilder.java | 23 + .../platform/loan/domain/MoneyBuilder.java | 26 + .../platform/loanschedule/domain/PmtTest.java | 27 + .../src/test/resources/META-INF/context.xml | 33 + 252 files changed, 20100 insertions(+) create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/CustomOAuthProviderTokenServices.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/CustomProtectedResourceProcessingFilter.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/CustomTokenServices.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/MifosNgConsumerDetailsService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/OAuthParametersAwareSecurityContextPerRequestFilter.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/oauth/RemovableInMemoryProviderTokenServices.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/AdjustLoanTransactionCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/CalculateLoanScheduleCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformServiceImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/CannotDeleteLoanException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ChangePasswordCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ClosedLoanComparator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/EnrollClientCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformServiceImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/InvalidSqlException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/LoanProductValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/LoanStateTransitionCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/LoanTransactionValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/OfficeValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformServiceImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/RoleValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/Specifications.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/SubmitLoanApplicationCommandValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/UserValidator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformServiceJpaRepositoryImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Client.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/client/domain/ClientRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Note.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/client/domain/NoteRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrency.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrencyRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/MonetaryCurrency.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/Money.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ApplicationDomainRuleException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ClientNotAuthenticatedException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidSignupException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NewDataValidationException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NoAuthorizationException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AbstractAuditableCustom.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AuditorAwareImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/CustomAuthenticationFailureHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/DefaultPlatformPasswordEncoder.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/EmailDetail.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginController.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginDetectionAuthenticationSuccessHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginFormBean.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/GmailBackedPlatformEmailService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PentahoReportingController.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailSendException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformPasswordEncoder.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/RandomPasswordGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupController.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupFormBean.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/UsernameAlreadyExistsException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AmortizationMethod.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentDecliningBalanceInterestRebateCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentFlatInterestRebateCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentInterestRebateCalculatorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessor.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestMethod.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculatorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InvalidLoanTimelineDate.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanBuilder.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanLifecycleStateMachine.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanPayoffSummary.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProduct.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRelatedDetail.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatusRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/PeriodFrequencyType.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceEqualInstallmentsLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultScheduledDateGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGeneratorFactory.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/ScheduledDateGenerator.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetail.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetailRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Office.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OfficeRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Organisation.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationCurrency.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ApplicationConfigurationResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ClientResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ExtraDataResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/FlexibleReportingResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportTriggerResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanOpenResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanProductResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/NoteResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/OfficeResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/ProtectedFlexibleReportingResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/TenantResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/UserAdmistrationResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/rest/UserResource.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/security/AccessConfirmationController.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsServiceImpl.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUserRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserDomainService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserPriviledgeDomainService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PasswordMustBeDifferentException.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Permission.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionBuilder.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionGroup.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PlatformUserRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Role.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/RoleRepository.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserDomainService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserPriviledgeDomainService.java create mode 100644 mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UsernameMustBeDifferentException.java create mode 100644 mifosng-provider/src/main/resources/META-INF/orm.xml create mode 100644 mifosng-provider/src/main/resources/META-INF/persistence.xml create mode 100644 mifosng-provider/src/main/resources/META-INF/spring/appContext.xml create mode 100644 mifosng-provider/src/main/resources/META-INF/spring/infrastructure.xml create mode 100644 mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml create mode 100644 mifosng-provider/src/main/resources/META-INF/spring/securityContextOld.xml create mode 100644 mifosng-provider/src/main/resources/ccylist.csv create mode 100644 mifosng-provider/src/main/resources/logback.xml create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/MPESALoanDisbursalsExportSummary.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoanSummaryBranch.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansCenter.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLastInstallment.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanOfficer.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanProduct.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanPurpose.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/agingSummary.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstanding.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstandingBySourceOfFunds.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/branchExpectedCashFlow.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/centerCollectionSheet.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/centerScheduleLoanOfficer.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/clientExit.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/clientSummary.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoanSummaryBranch.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoans.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dormantClientsSummary.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/fundsMovement.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/groupCollectionSheetMPESA.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/groupsInformation.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanAging.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loansPendingApproval.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/loansToBeDisbursed.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_es.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_fr.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/outreachSummaryBranch.properties create mode 100644 mifosng-provider/src/main/resources/org/mifos/bi/reports/overdueMatureLoans.properties create mode 100644 mifosng-provider/src/main/webapp/META-INF/context.xml create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/spring/webmvc-config.xml create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/allinone.css create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/favicon.ico create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-1.6.3.min.js create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/jquery-ui-1.8.16.custom.min.js create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_55_fbec88_40x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_85_dfeffc_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_217bc0_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_2e83ff_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_469bdd_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_6da8d5_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_cd0a0a_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_d8e7f3_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_f9bd01_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/jquery-ui-1.8.16.custom.css create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_flat_10_000000_40x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_222222_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_228ef1_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ef8c08_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ffd27a_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ffffff_256x240.png create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/jquery-ui-1.8.16.custom.css create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/access_confirmation.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/common-head.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/firsttimelogin.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/index.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/login.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/sessionTimeout.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signup.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signupsuccess.jsp create mode 100644 mifosng-provider/src/main/webapp/WEB-INF/web.xml create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessorTest.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MonetaryCurrencyBuilder.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MoneyBuilder.java create mode 100644 mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/PmtTest.java create mode 100644 mifosng-provider/src/test/resources/META-INF/context.xml diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/CustomOAuthProviderTokenServices.java b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomOAuthProviderTokenServices.java new file mode 100644 index 000000000..28b33649d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomOAuthProviderTokenServices.java @@ -0,0 +1,15 @@ +package org.mifosng.oauth; + +import org.springframework.security.oauth.provider.token.OAuthProviderToken; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices; + +/** + * This is just a temporary hack, should pass oauth access token, access secret, consumer key, consumer secret to get access to authenticated user permissions + */ +public interface CustomOAuthProviderTokenServices extends OAuthProviderTokenServices { + + OAuthProviderToken getTokenByNonEncodedKey(String oauthToken); + + void removeTokenByNonEncodedKey(String oauthToken); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/CustomProtectedResourceProcessingFilter.java b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomProtectedResourceProcessingFilter.java new file mode 100644 index 000000000..823ef2038 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomProtectedResourceProcessingFilter.java @@ -0,0 +1,154 @@ +package org.mifosng.oauth; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth.common.OAuthConsumerParameter; +import org.springframework.security.oauth.common.OAuthException; +import org.springframework.security.oauth.common.signature.OAuthSignatureMethod; +import org.springframework.security.oauth.common.signature.SignatureSecret; +import org.springframework.security.oauth.common.signature.UnsupportedSignatureMethodException; +import org.springframework.security.oauth.provider.ConsumerAuthentication; +import org.springframework.security.oauth.provider.ConsumerCredentials; +import org.springframework.security.oauth.provider.ConsumerDetails; +import org.springframework.security.oauth.provider.InvalidOAuthParametersException; +import org.springframework.security.oauth.provider.filter.ProtectedResourceProcessingFilter; +import org.springframework.security.oauth.provider.token.OAuthProviderToken; + +public class CustomProtectedResourceProcessingFilter extends + ProtectedResourceProcessingFilter { + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (!skipProcessing(request)) { + if (requiresAuthentication(request, response, chain)) { + if (!allowMethod(request.getMethod().toUpperCase())) { + + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + try { + Map oauthParams = getProviderSupport().parseParameters(request); + + if (parametersAreAdequate(oauthParams)) { + + String consumerKey = oauthParams.get(OAuthConsumerParameter.oauth_consumer_key.toString()); + if (consumerKey == null) { + throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingConsumerKey", "Missing consumer key.")); + } + + //load the consumer details. + ConsumerDetails consumerDetails = getConsumerDetailsService().loadConsumerByConsumerKey(consumerKey); + + //validate the parameters for the consumer. + validateOAuthParams(consumerDetails, oauthParams); + + //extract the credentials. + String token = oauthParams.get(OAuthConsumerParameter.oauth_token.toString()); + String signatureMethod = oauthParams.get(OAuthConsumerParameter.oauth_signature_method.toString()); + String signature = oauthParams.get(OAuthConsumerParameter.oauth_signature.toString()); + String signatureBaseString = getProviderSupport().getSignatureBaseString(request); + ConsumerCredentials credentials = new ConsumerCredentials(consumerKey, signature, signatureMethod, signatureBaseString, token); + + //create an authentication request. + ConsumerAuthentication authentication = new ConsumerAuthentication(consumerDetails, credentials, oauthParams); + authentication.setDetails(createDetails(request, consumerDetails)); + + Authentication previousAuthentication = SecurityContextHolder.getContext().getAuthentication(); + try { + //set the authentication request (unauthenticated) into the context. + SecurityContextHolder.getContext().setAuthentication(authentication); + + //validate the signature. + if (token == null) { + validateSignature(authentication); + } else { + if (!"OPTIONS".equalsIgnoreCase(request.getMethod().toUpperCase())) { + validateSignature(authentication); + } + } + + //mark the authentication request as validated. + authentication.setSignatureValidated(true); + + //mark that processing has been handled. + request.setAttribute(OAUTH_PROCESSING_HANDLED, Boolean.TRUE); + + //go. + onValidSignature(request, response, chain); + } + finally { + //clear out the consumer authentication to make sure it doesn't get cached. + resetPreviousAuthentication(previousAuthentication); + } + } + else if (!isIgnoreInadequateCredentials()) { + throw new InvalidOAuthParametersException(messages.getMessage("OAuthProcessingFilter.missingCredentials", "Inadequate OAuth consumer credentials.")); + } + else { + chain.doFilter(request, response); + } + } + catch (AuthenticationException ae) { + fail(request, response, ae); + } + catch (ServletException e) { + if (e.getRootCause() instanceof AuthenticationException) { + fail(request, response, (AuthenticationException) e.getRootCause()); + } + else { + throw e; + } + } + } + else { + chain.doFilter(servletRequest, servletResponse); + } + } + else { + chain.doFilter(servletRequest, servletResponse); + } + } + + @Override + protected void validateSignature(ConsumerAuthentication authentication) + throws AuthenticationException { + SignatureSecret secret = authentication.getConsumerDetails() + .getSignatureSecret(); + String token = authentication.getConsumerCredentials().getToken(); + OAuthProviderToken authToken = null; + if (token != null && !"".equals(token)) { + authToken = getTokenServices().getToken(token); + } + + String signatureMethod = authentication.getConsumerCredentials() + .getSignatureMethod(); + OAuthSignatureMethod method; + try { + method = getSignatureMethodFactory().getSignatureMethod( + signatureMethod, secret, + authToken != null ? authToken.getSecret() : null); + } catch (UnsupportedSignatureMethodException e) { + throw new OAuthException(e.getMessage(), e); + } + + String signatureBaseString = authentication.getConsumerCredentials() + .getSignatureBaseString(); + String signature = authentication.getConsumerCredentials() + .getSignature(); + method.verify(signatureBaseString, signature); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/CustomTokenServices.java b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomTokenServices.java new file mode 100644 index 000000000..2d8237b8f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/CustomTokenServices.java @@ -0,0 +1,63 @@ +package org.mifosng.oauth; + +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth.provider.token.OAuthAccessProviderToken; +import org.springframework.security.oauth.provider.token.OAuthProviderToken; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl; + +public class CustomTokenServices implements CustomOAuthProviderTokenServices { + + private final ConcurrentHashMap requestTokenStore = new ConcurrentHashMap(); + private final RemovableInMemoryProviderTokenServices delegate; + + public CustomTokenServices(RemovableInMemoryProviderTokenServices delegate) { + this.delegate = delegate; + } + + @Override + public OAuthProviderToken getToken(String token) + throws AuthenticationException { + return this.delegate.getToken(token); + } + + @Override + public OAuthProviderToken createUnauthorizedRequestToken( + String consumerKey, String callbackUrl) + throws AuthenticationException { + return this.delegate.createUnauthorizedRequestToken(consumerKey, callbackUrl); + } + + @Override + public void authorizeRequestToken(String requestToken, String verifier, + Authentication authentication) throws AuthenticationException { + this.delegate.authorizeRequestToken(requestToken, verifier, authentication); + } + + @Override + public OAuthAccessProviderToken createAccessToken(String requestToken) + throws AuthenticationException { + OAuthProviderTokenImpl accessToken = (OAuthProviderTokenImpl)this.delegate.createAccessToken(requestToken); + this.requestTokenStore.put(requestToken, accessToken); + return accessToken; + } + + @Override + public OAuthProviderToken getTokenByNonEncodedKey(String oauthToken) { + return this.requestTokenStore.get(oauthToken); + } + + @Override + public void removeTokenByNonEncodedKey(String oauthToken) { + OAuthProviderTokenImpl accessToken = this.requestTokenStore.remove(oauthToken); + if (accessToken != null) { + String acccesTokenValue = accessToken.getValue(); + this.delegate.removeAccessToken(acccesTokenValue); + } else { + this.delegate.removeAccessToken(oauthToken); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/MifosNgConsumerDetailsService.java b/mifosng-provider/src/main/java/org/mifosng/oauth/MifosNgConsumerDetailsService.java new file mode 100644 index 000000000..20ac2c317 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/MifosNgConsumerDetailsService.java @@ -0,0 +1,44 @@ +package org.mifosng.oauth; + +import java.util.HashMap; +import java.util.Map; + +import org.mifosng.platform.oauthconsumer.domain.OauthConsumerDetail; +import org.mifosng.platform.oauthconsumer.domain.OauthConsumerDetailRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth.common.OAuthException; +import org.springframework.security.oauth.provider.ConsumerDetails; +import org.springframework.security.oauth.provider.ConsumerDetailsService; +import org.springframework.security.oauth.provider.InvalidOAuthParametersException; + +public class MifosNgConsumerDetailsService implements ConsumerDetailsService { + + private Map consumerDetailsStore = new HashMap(); + + @Autowired + private OauthConsumerDetailRepository oauthConsumerDetailRepository; + + /** + * consumer service is used for by spring security oauth for all requests so makes sense to store consumer details in memory + * rather than query the database based on each request. + */ + @Override + public ConsumerDetails loadConsumerByConsumerKey(final String consumerKey) + throws OAuthException { + + ConsumerDetails detailsFromInMemoryStore = consumerDetailsStore.get(consumerKey); + OauthConsumerDetail detailsFromDatabase = null; + if (detailsFromInMemoryStore == null) { + detailsFromDatabase = this.oauthConsumerDetailRepository.findByConsumerKey(consumerKey); + } + + ConsumerDetails consumerDetails = detailsFromInMemoryStore != null ? detailsFromInMemoryStore : detailsFromDatabase; + + if (consumerDetails == null) { + throw new InvalidOAuthParametersException("Consumer not found: " + consumerKey); + } else { + consumerDetailsStore.put(consumerKey, detailsFromDatabase); + } + return consumerDetails; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/OAuthParametersAwareSecurityContextPerRequestFilter.java b/mifosng-provider/src/main/java/org/mifosng/oauth/OAuthParametersAwareSecurityContextPerRequestFilter.java new file mode 100644 index 000000000..36db0a5c7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/OAuthParametersAwareSecurityContextPerRequestFilter.java @@ -0,0 +1,82 @@ +package org.mifosng.oauth; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth.common.OAuthConsumerParameter; +import org.springframework.security.oauth.provider.OAuthProviderSupport; +import org.springframework.security.oauth.provider.filter.CoreOAuthProviderSupport; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices; +import org.springframework.web.filter.GenericFilterBean; + +public class OAuthParametersAwareSecurityContextPerRequestFilter extends + GenericFilterBean { + + private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport(); + + private final OAuthProviderTokenServices tokenServices; + + public OAuthParametersAwareSecurityContextPerRequestFilter(OAuthProviderTokenServices tokenServices) { + this.tokenServices = tokenServices; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + Map oauthParams = this.providerSupport.parseParameters(request); + + String anonymousUserHash = "" + "anonymousUser".hashCode(); + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS")); + Authentication authentication = new AnonymousAuthenticationToken(anonymousUserHash, "anonymousUser", authorities); + + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(authentication); + + if (oauthTokenExists(oauthParams)) { + String oauthToken = retrieveOAuthToken(oauthParams); + OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(oauthToken); + + if (token.getUserAuthentication() != null) { + context.setAuthentication(token.getUserAuthentication()); + } + } else { + String requestToken = request.getParameter("requestToken"); + if (StringUtils.isNotBlank(requestToken)) { + OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(requestToken); + + if (token.getUserAuthentication() != null) { + context.setAuthentication(token.getUserAuthentication()); + } + } + } + + chain.doFilter(request, response); + } + + private String retrieveOAuthToken(Map oauthParams) { + return oauthParams.get(OAuthConsumerParameter.oauth_token.toString()); + } + + private boolean oauthTokenExists(Map oauthParams) { + return oauthParams.containsKey(OAuthConsumerParameter.oauth_token.toString()); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/oauth/RemovableInMemoryProviderTokenServices.java b/mifosng-provider/src/main/java/org/mifosng/oauth/RemovableInMemoryProviderTokenServices.java new file mode 100644 index 000000000..5caa2ec94 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/oauth/RemovableInMemoryProviderTokenServices.java @@ -0,0 +1,11 @@ +package org.mifosng.oauth; + +import org.springframework.security.oauth.provider.token.InMemoryProviderTokenServices; + +public class RemovableInMemoryProviderTokenServices extends + InMemoryProviderTokenServices { + + public void removeAccessToken(String acccesTokenValue) { + super.removeToken(acccesTokenValue); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/AdjustLoanTransactionCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/AdjustLoanTransactionCommandValidator.java new file mode 100644 index 000000000..29efd0802 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/AdjustLoanTransactionCommandValidator.java @@ -0,0 +1,52 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.AdjustLoanTransactionCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class AdjustLoanTransactionCommandValidator { + + private final AdjustLoanTransactionCommand command; + + public AdjustLoanTransactionCommandValidator(AdjustLoanTransactionCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (command.getLoanId() == null || command.getLoanId().longValue() <= 0) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId"); + dataValidationErrors.add(error); + } + + if (command.getRepaymentId() == null || command.getRepaymentId().longValue() <= 0) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.transaction.id.is.invalid", "repaymentId"); + dataValidationErrors.add(error); + } + + if (command.getPaymentDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.date.cannot.be.blank", "paymentDate"); + dataValidationErrors.add(error); + } + + if (command.getPaymentAmount() == null || command.getPaymentAmount().doubleValue() < 0) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.adjustment.must.be.zero.or.greater", "paymentAmount"); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) { + ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "comment"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/CalculateLoanScheduleCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/CalculateLoanScheduleCommandValidator.java new file mode 100644 index 000000000..3a5862f6a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/CalculateLoanScheduleCommandValidator.java @@ -0,0 +1,209 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.InterestMethod; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; + +public class CalculateLoanScheduleCommandValidator { + + private final CalculateLoanScheduleCommand command; + + public CalculateLoanScheduleCommandValidator(CalculateLoanScheduleCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (StringUtils.isBlank(command.getCurrencyCode())) { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.cannot.be.empty", "currencyCode"); + dataValidationErrors.add(error); + } + + if (command.getDigitsAfterDecimal() != null) { + + if (command.getDigitsAfterDecimal() < 0 || command.getDigitsAfterDecimal() > 6) { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.must.be.between.zero.and.six.inclusive", "selectedDigitsAfterDecimal", command.getDigitsAfterDecimal()); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.cannot.be.empty", "selectedDigitsAfterDecimal"); + dataValidationErrors.add(error); + } + + if (command.getPrincipal() != null) { + if (command.getPrincipal().doubleValue() <= Double.valueOf("0.0")) { + ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.must.be.greater.than.zero", "principalMoney"); + dataValidationErrors.add(error); + } + // TODO - check number of digits before decimal and after decimal + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.cannot.be.empty", "principalMoney"); + dataValidationErrors.add(error); + } + + if (command.getRepaymentFrequency() != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency()); + ErrorResponse error = null; + switch (frequencyType) { + case DAYS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 365); + break; + case WEEKS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 52); + break; + case MONTHS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 12); + break; + case YEARS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 2); + break; + default: + error = new ErrorResponse("validation.msg.product.repayment.frequency.invalid", "selectedRepaymentFrequencyOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.repayment.frequency.cannot.be.empty", "selectedRepaymentFrequencyOption"); + dataValidationErrors.add(error); + } + + if (command.getNumberOfRepayments() != null) { + if (command.getNumberOfRepayments() < 1 || command.getNumberOfRepayments() > 100) { + ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.is.out.of.range", "numberOfRepayments"); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.cannot.be.empty", "numberOfRepayments"); + dataValidationErrors.add(error); + } + + if (command.getAmortizationMethod() != null) { + AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + ErrorResponse error = null; + switch (amortizationMethod) { + case EQUAL_PRINCIPAL: + break; + case EQUAL_INSTALLMENTS: + break; + default: + error = new ErrorResponse("validation.msg.product.amortization.method.invalid", "selectedAmortizationMethodOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.amortization.method.cannot.be.empty", "selectedAmortizationMethodOption"); + dataValidationErrors.add(error); + } + + + if (command.getExpectedDisbursementDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.expectedDisbursementDate.method.cannot.be.blank", "expectedDisbursementDate"); + dataValidationErrors.add(error); + } else { + + if (command.getRepaymentsStartingFromDate() != null + && command.getExpectedDisbursementDate().isAfter(command.getRepaymentsStartingFromDate())) { + + ErrorResponse error = new ErrorResponse("validation.msg.loan.expectedDisbursementDate.cannot.be.after.first.repayment.date", "expectedDisbursementDate"); + dataValidationErrors.add(error); + } + } + + if (command.getRepaymentsStartingFromDate() != null && command.getInterestCalculatedFromDate() == null) { + + ErrorResponse error = new ErrorResponse("validation.msg.loan.interestCalculatedFromDate.must.be.entered.when.using.repayments.startfrom.field", "interestCalculatedFromDate"); + dataValidationErrors.add(error); + } else if (command.getRepaymentsStartingFromDate() == null && command.getInterestCalculatedFromDate() != null) { + + // validate interestCalculatedFromDate is after or on repaymentsStartingFromDate + + if (command.getExpectedDisbursementDate().isAfter(command.getInterestCalculatedFromDate())) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.interestCalculatedFromDate.cannot.be.before.disbursement.date", "interestCalculatedFromDate"); + dataValidationErrors.add(error); + } + } + + if (command.getInterestRatePerPeriod()!= null) { + if (command.getInterestRatePerPeriod().doubleValue() < Double.valueOf("0.0")) { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.negative", "nominalInterestRate"); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.empty", "nominalInterestRate"); + dataValidationErrors.add(error); + } + + // interest rate period type + if (command.getInterestRateFrequencyMethod() != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + ErrorResponse error = null; + switch (frequencyType) { + case DAYS: + break; + case WEEKS: + break; + case MONTHS: + break; + case YEARS: + break; + default: + error = new ErrorResponse("validation.msg.product.interest.frequency.invalid", "selectedInterestFrequencyOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.frequency.cannot.be.empty", "selectedInterestFrequencyOption"); + dataValidationErrors.add(error); + } + + if (command.getInterestMethod() != null) { + InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod()); + ErrorResponse error = null; + switch (interestMethod) { + case DECLINING_BALANCE: + break; + case FLAT: + break; + default: + error = new ErrorResponse("validation.msg.product.interest.method.invalid", "selectedInterestMethodOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.method.cannot.be.empty", "selectedInterestMethodOption"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Validation errors exist."); + } + } + + private ErrorResponse validateNumberExistsAndInRange(String errorCodePreFix, String fieldName, Integer value, int min, int max) { + ErrorResponse error = null; + if (value == null) { + error = new ErrorResponse(errorCodePreFix + ".cannot.be.empty", fieldName); + } else { + if (value < min || value > max) { + error = new ErrorResponse(errorCodePreFix + ".outside.allowed.range", fieldName); + } + } + return error; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformService.java new file mode 100644 index 000000000..9fce71800 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformService.java @@ -0,0 +1,14 @@ +package org.mifosng.platform; + +import org.joda.time.LocalDate; +import org.mifosng.data.LoanPayoffReadModel; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.command.CalculateLoanScheduleCommand; + +public interface CalculationPlatformService { + + LoanSchedule calculateLoanSchedule(CalculateLoanScheduleCommand command); + + LoanPayoffReadModel calculatePayoffOn(Long valueOf, LocalDate payoffDate); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformServiceImpl.java new file mode 100644 index 000000000..c4f69cc0b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/CalculationPlatformServiceImpl.java @@ -0,0 +1,142 @@ +package org.mifosng.platform; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.LoanPayoffReadModel; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.platform.currency.domain.ApplicationCurrency; +import org.mifosng.platform.currency.domain.ApplicationCurrencyRepository; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.InterestMethod; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanPayoffSummary; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; +import org.mifosng.platform.loan.domain.LoanRepository; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; +import org.mifosng.platform.loanschedule.domain.DefaultLoanScheduleGeneratorFactory; +import org.mifosng.platform.loanschedule.domain.LoanScheduleGenerator; +import org.mifosng.platform.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CalculationPlatformServiceImpl implements + CalculationPlatformService { + + private final LoanScheduleGeneratorFactory loanScheduleFactory; + private final LoanRepository loanRepository; + private final ApplicationCurrencyRepository applicationCurrencyRepository; + + @Autowired + public CalculationPlatformServiceImpl(final LoanRepository loanRepository, final ApplicationCurrencyRepository applicationCurrencyRepository) { + this.loanRepository = loanRepository; + this.applicationCurrencyRepository = applicationCurrencyRepository; + this.loanScheduleFactory = new DefaultLoanScheduleGeneratorFactory(); + } + + @Override + public LoanSchedule calculateLoanSchedule(final CalculateLoanScheduleCommand command) { + + CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator(command); + validator.validate(); + + ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(command.getCurrencyCode()); + CurrencyData currencyData = new CurrencyData(applicationCurrency.getCode(), applicationCurrency.getName(), command.getDigitsAfterDecimal(), + applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode()); + + MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + + final BigDecimal principalAmount = BigDecimal.valueOf(command.getPrincipal() + .doubleValue()); + + final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command + .getInterestRatePerPeriod().doubleValue()); + final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + + final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod()); + + final Integer repayEvery = command.getRepaymentEvery(); + final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType + .fromInt(command.getRepaymentFrequency()); + final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + + final boolean flexibleRepaymentSchedule = command.isFlexibleRepaymentSchedule(); + final boolean interestRebateAllowed = command.isInterestRebateAllowed(); + + // apr calculator + BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO; + switch (interestPeriodFrequencyType) { + case DAYS: + break; + case WEEKS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod() + .multiply(BigDecimal.valueOf(52)); + break; + case MONTHS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod() + .multiply(BigDecimal.valueOf(12)); + break; + case YEARS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod().multiply(BigDecimal.valueOf(1)); + break; + case INVALID: + break; + } + + LoanProductRelatedDetail loanScheduleInfo = new LoanProductRelatedDetail(currency, principalAmount, + defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, + repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, BigDecimal.ZERO, flexibleRepaymentSchedule, interestRebateAllowed); + + LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory + .create(interestMethod, amortizationMethod); + + return loanScheduleGenerator.generate(loanScheduleInfo, new LocalDate( + command.getExpectedDisbursementDate()), command + .getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), currencyData); + } + + @Override + public LoanPayoffReadModel calculatePayoffOn(final Long loanId, + final LocalDate payoffDate) { + + // use jdbc approach to calculating payoff if possible. + final Loan loan = this.loanRepository.findOne(loanId); + + final LoanPayoffSummary payoffSummary = loan.getPayoffSummaryOn(payoffDate); + + final String currencyCode = payoffSummary.getTotalPaidToDate().getCurrencyCode(); + + ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneByCode(currencyCode); + final int currencyDigitsAfterDecimal = payoffSummary.getTotalPaidToDate().getCurrencyDigitsAfterDecimal(); + + CurrencyData currency = new CurrencyData(currencyCode, applicationCurrency.getName(), currencyDigitsAfterDecimal, applicationCurrency.getDisplaySymbol(), applicationCurrency.getNameCode()); + MoneyData totalPaidToDate = MoneyData.of(currency, payoffSummary.getTotalPaidToDate() + .getAmount()); + + MoneyData totalInterestOutstandingBasedOnExpectedMaturityDate = MoneyData.of(currency, payoffSummary.getTotalOutstandingBasedOnExpectedMaturityDate() + .getAmount()); + + MoneyData totalInterestOutstandingBasedOnPayoffDate = MoneyData.of(currency, payoffSummary.getTotalOutstandingBasedOnPayoffDate() + .getAmount()); + + MoneyData interestRebateOwed = MoneyData.of(currency, payoffSummary.getRebateOwed().getAmount()); + + return new LoanPayoffReadModel(payoffSummary.getReference().toString(), + payoffSummary.getAcutalDisbursementDate(), + payoffSummary.getExpectedMaturityDate(), + payoffSummary.getProjectedMaturityDate(), + payoffSummary.getExpectedLoanTermInDays(), + payoffSummary.getProjectedLoanTermInDays(), totalPaidToDate, + totalInterestOutstandingBasedOnExpectedMaturityDate, + totalInterestOutstandingBasedOnPayoffDate, interestRebateOwed); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/CannotDeleteLoanException.java b/mifosng-provider/src/main/java/org/mifosng/platform/CannotDeleteLoanException.java new file mode 100644 index 000000000..a2ddf8b44 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/CannotDeleteLoanException.java @@ -0,0 +1,9 @@ +package org.mifosng.platform; + +public class CannotDeleteLoanException extends RuntimeException { + + public CannotDeleteLoanException(final String message) { + super(message); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ChangePasswordCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/ChangePasswordCommandValidator.java new file mode 100644 index 000000000..b79e18833 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ChangePasswordCommandValidator.java @@ -0,0 +1,41 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.ChangePasswordCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class ChangePasswordCommandValidator { + + private final ChangePasswordCommand command; + + public ChangePasswordCommandValidator(ChangePasswordCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (StringUtils.isBlank(command.getPassword())) { + ErrorResponse error = new ErrorResponse("validation.msg.changepassword.password.cannot.be.blank", "password"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getPasswordrepeat())) { + ErrorResponse error = new ErrorResponse("validation.msg.changepassword.passwordrepeat.cannot.be.blank", "passwordrepeat"); + dataValidationErrors.add(error); + } else { + if (!command.getPasswordrepeat().equals(command.getPassword())) { + ErrorResponse error = new ErrorResponse("validation.msg.changepassword.passwordrepeat.not.the.same", "passwordrepeat"); + dataValidationErrors.add(error); + } + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ClosedLoanComparator.java b/mifosng-provider/src/main/java/org/mifosng/platform/ClosedLoanComparator.java new file mode 100644 index 000000000..f56900ec8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ClosedLoanComparator.java @@ -0,0 +1,15 @@ +package org.mifosng.platform; + +import java.util.Comparator; + +import org.mifosng.data.LoanAccountData; + +public class ClosedLoanComparator implements Comparator { + + @Override + public int compare(final LoanAccountData o1, + final LoanAccountData o2) { + return o1.getClosedOnDate().compareTo(o2.getClosedOnDate()); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/EnrollClientCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/EnrollClientCommandValidator.java new file mode 100644 index 000000000..7b1aa1a0b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/EnrollClientCommandValidator.java @@ -0,0 +1,60 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.EnrollClientCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class EnrollClientCommandValidator { + + private final EnrollClientCommand command; + + public EnrollClientCommandValidator(EnrollClientCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (command.getOfficeId() == null || command.getOfficeId().equals(Long.valueOf(-1))) { + ErrorResponse error = new ErrorResponse("validation.msg.client.office.cannot.be.blank", "officeId"); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(command.getFullname()) && (StringUtils.isNotBlank(command.getFirstname()) || StringUtils.isNotBlank(command.getLastname()))) { + ErrorResponse error = new ErrorResponse("validation.msg.client.fullname.cannot.entered.when.firstname.or.lastname.entered", "fullname"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getFirstname()) && StringUtils.isBlank(command.getLastname()) ) { + + if (StringUtils.isBlank(command.getFullname())) { + ErrorResponse error = new ErrorResponse("validation.msg.client.fullname.must.be.provide.or.firstname.lastname", "fullname"); + dataValidationErrors.add(error); + } + } else { + if (StringUtils.isBlank(command.getFirstname())) { + ErrorResponse error = new ErrorResponse("validation.msg.client.firstname.cannot.be.blank", "firstname"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getLastname())) { + ErrorResponse error = new ErrorResponse("validation.msg.client.lastname.cannot.be.blank", "lastname"); + dataValidationErrors.add(error); + } + } + + if (command.getJoiningDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.client.joining.date.cannot.be.blank", "joiningDate"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation erros exist."); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformService.java new file mode 100644 index 000000000..1b31bd83f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformService.java @@ -0,0 +1,21 @@ +package org.mifosng.platform; + +import org.mifosng.data.command.ImportClientCommand; +import org.mifosng.data.command.ImportLoanCommand; +import org.mifosng.data.command.ImportLoanRepaymentsCommand; + +public interface ImportPlatformService { + + void importClients(ImportClientCommand command); + + ImportClientCommand populateClientImportFromCsv(); + + void importLoans(ImportLoanCommand command); + + ImportLoanCommand populateLoanImportFromCsv(); + + void importLoanRepayments(ImportLoanRepaymentsCommand command); + + ImportLoanRepaymentsCommand populateLoanRepaymentsImportFromCsv(); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformServiceImpl.java new file mode 100644 index 000000000..ba999b2a5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ImportPlatformServiceImpl.java @@ -0,0 +1,658 @@ +package org.mifosng.platform; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.ScheduledLoanInstallment; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.data.command.EnrollClientCommand; +import org.mifosng.data.command.ImportClientCommand; +import org.mifosng.data.command.ImportLoanCommand; +import org.mifosng.data.command.ImportLoanRepaymentsCommand; +import org.mifosng.data.command.LoanStateTransitionCommand; +import org.mifosng.data.command.LoanTransactionCommand; +import org.mifosng.data.command.SubmitApproveDisburseLoanCommand; +import org.mifosng.data.command.SubmitLoanApplicationCommand; +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.client.domain.ClientRepository; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine; +import org.mifosng.platform.loan.domain.InterestMethod; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanBuilder; +import org.mifosng.platform.loan.domain.LoanLifecycleStateMachine; +import org.mifosng.platform.loan.domain.LoanProduct; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; +import org.mifosng.platform.loan.domain.LoanProductRepository; +import org.mifosng.platform.loan.domain.LoanRepaymentScheduleInstallment; +import org.mifosng.platform.loan.domain.LoanRepository; +import org.mifosng.platform.loan.domain.LoanStatus; +import org.mifosng.platform.loan.domain.LoanStatusRepository; +import org.mifosng.platform.loan.domain.LoanTransaction; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.OfficeRepository; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.csvreader.CsvReader; + +@Service +public class ImportPlatformServiceImpl implements ImportPlatformService { + + private final OfficeRepository officeRepository; + private final ClientRepository clientRepository; + private final LoanProductRepository loanProductRepository; + private final CalculationPlatformService calculationPlatformService; + private final LoanRepository loanRepository; + private final LoanStatusRepository loanStatusRepository; + + @Autowired + public ImportPlatformServiceImpl( + final OfficeRepository officeRepository, final ClientRepository clientRepository, final LoanProductRepository loanProductRepository, + final CalculationPlatformService calculationPlatformService, final LoanRepository loanRepository, final LoanStatusRepository loanStatusRepository) { + this.officeRepository = officeRepository; + this.clientRepository = clientRepository; + this.loanProductRepository = loanProductRepository; + this.calculationPlatformService = calculationPlatformService; + this.loanRepository = loanRepository; + this.loanStatusRepository = loanStatusRepository; + } + + private AppUser extractAuthenticatedUser() { + AppUser currentUser = null; + SecurityContext context = SecurityContextHolder.getContext(); + if (context != null) { + Authentication auth = context.getAuthentication(); + if (auth != null) { + currentUser = (AppUser) auth.getPrincipal(); + } + } + + if (currentUser == null) { + throw new ClientNotAuthenticatedException(); + } + + return currentUser; + } + + @Transactional + @Override + public void importClients(ImportClientCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + List allOffices = this.officeRepository.findAll(officesBelongingTo(currentUser.getOrganisation())); + + List newClientCollection = new ArrayList(); + + for (EnrollClientCommand client : command.getClients()) { + Office clientOffice = findById(allOffices, client.getOfficeId()); + + Client newClient = Client.newClient(currentUser.getOrganisation(), clientOffice, client.getFirstname(), client.getLastname(), client.getJoiningDate(), client.getExternalId()); + + newClientCollection.add(newClient); + } + + this.clientRepository.save(newClientCollection); + } + + @Transactional + @Override + public void importLoans(ImportLoanCommand importCommand) { + + AppUser currentUser = extractAuthenticatedUser(); + + List newLoanCollection = new ArrayList(); + + for (SubmitApproveDisburseLoanCommand combinedCommand : importCommand.getLoans()) { + + SubmitLoanApplicationCommand command = combinedCommand.getSubmitLoanApplicationCommand(); + + // most of this code can be extracted out into an Assembler + LoanProduct loanProduct = this.loanProductRepository.findOne(command + .getProductId()); + Client client = this.clientRepository.findOne(command.getApplicantId()); + + MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + + final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command + .getInterestRatePerPeriod().doubleValue()); + final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + + // apr calculator + BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO; + switch (interestPeriodFrequencyType) { + case DAYS: + break; + case WEEKS: + defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(52)); + break; + case MONTHS: + defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(12)); + break; + case YEARS: + defaultAnnualNominalInterestRate = command.getInterestRatePerPeriod(); + break; + case INVALID: + break; + } + + final InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod()); + + final Integer repayEvery = command.getRepaymentEvery(); + final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType + .fromInt(command.getRepaymentFrequency()); + final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + final boolean flexibleRepaymentSchedule = command.isFlexibleRepaymentSchedule(); + final boolean interestRebateAllowed = command.isInterestRebateAllowed(); + + LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency, command.getPrincipal(), + defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, + repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceAmount(), flexibleRepaymentSchedule, interestRebateAllowed); + + LoanSchedule loanSchedule = command.getLoanSchedule(); + List loanRepaymentSchedule = loanSchedule + .getScheduledLoanInstallments(); + + Loan loan = new LoanBuilder().with(currentUser.getOrganisation()) + .with(loanProduct).with(client) + .withExternalSystemId(command.getExternalId()) + .with(loanRepaymentScheduleDetail).build(); + + for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) { + + MoneyData readPrincipalDue = scheduledLoanInstallment + .getPrincipalDue(); + + MoneyData readInterestDue = scheduledLoanInstallment + .getInterestDue(); + + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + loan, scheduledLoanInstallment.getInstallmentNumber(), + scheduledLoanInstallment.getPeriodStart(), + scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(), + readInterestDue.getAmount()); + loan.addRepaymentScheduleInstallment(installment); + } + + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + + loan.submitApplication(command.getSubmittedOnDate(), command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), loanLifecycleStateMachine); + + LoanStateTransitionCommand approveLoanCommand = combinedCommand.getApproveLoanCommand(); + if (approveLoanCommand != null) { + loan.approve(approveLoanCommand.getEventDate(), loanLifecycleStateMachine); + } + + LoanStateTransitionCommand disburseLoanCommand = combinedCommand.getDisburseLoanCommand(); + if (disburseLoanCommand != null) { + loan.disburse(disburseLoanCommand.getEventDate(), loanLifecycleStateMachine); + } + + newLoanCollection.add(loan); + } + + this.loanRepository.save(newLoanCollection); + } + + private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { + List allowedLoanStatuses = this.loanStatusRepository.findAll(); + return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses); + } + + @Transactional + @Override + public void importLoanRepayments(ImportLoanRepaymentsCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine(); + + List newLoanCollection = new ArrayList(); + + List allLoans = this.loanRepository.findAll(loansBelongingTo(currentUser.getOrganisation())); + for (LoanTransactionCommand repaymentDetail : command.getRepayments()) { + Loan loan = findLoanByIdentifier(allLoans, repaymentDetail.getLoanId().toString()); + + Money repaymentAmount = Money.of(loan.getCurrency(), repaymentDetail.getPaymentAmount()); + + LoanTransaction loanRepayment = LoanTransaction.repayment(repaymentAmount, repaymentDetail.getPaymentDate()); + loan.makeRepayment(loanRepayment, loanLifecycleStateMachine); + } + + this.loanRepository.save(newLoanCollection); + + } + + public static Specification officesBelongingTo(final Organisation organisation) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, + final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.equal(root.get("organisation"), organisation); + } + }; + } + + public static Specification productsBelongingTo(final Organisation organisation) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, + final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.equal(root.get("organisation"), organisation); + } + }; + } + + public static Specification loansBelongingTo(final Organisation organisation) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, + final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.equal(root.get("organisation"), organisation); + } + }; + } + + public static Specification clientsBelongingTo(final Organisation organisation) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, + final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.equal(root.get("organisation"), organisation); + } + }; + } + + @Override + public ImportLoanRepaymentsCommand populateLoanRepaymentsImportFromCsv() { + + ImportLoanRepaymentsCommand command = new ImportLoanRepaymentsCommand(); + + AppUser currentUser = extractAuthenticatedUser(); + + List allLoans = this.loanRepository.findAll(loansBelongingTo(currentUser.getOrganisation())); + ClassPathResource repaymentsCsvFile = new ClassPathResource("creocore-loan-repayments.csv"); + + File file = null; + FileInputStream fileInputStream = null; + CsvReader loans = null; + try { + DateTimeFormatter isoParser = ISODateTimeFormat.date(); + loans = new CsvReader(repaymentsCsvFile.getInputStream(), + Charset.defaultCharset()); + loans.readHeaders(); + + List repaymentDetails = new ArrayList(); + + while (loans.readRecord()) { + String loanExternalId = loans.get("LoanExternalId"); + Loan matchingLoan = findLoanByIdentifier(allLoans, loanExternalId.trim()); + + String paymentDate = loans.get("PaymentDate"); + LocalDate paidOnDate = null; + if (StringUtils.isNotBlank(paymentDate)) { + paidOnDate = new LocalDate(isoParser.parseDateTime(paymentDate)); + } + + String paymentAmount = loans.get("PaymentAmount"); + BigDecimal repayment = BigDecimal.ZERO; + if (StringUtils.isNotBlank(paymentAmount)) { + repayment = BigDecimal.valueOf(Double.valueOf(paymentAmount)); + } + + String noNoteComment = "No note due to migration from mifos."; + + LoanTransactionCommand repaymentCommand = new LoanTransactionCommand(matchingLoan.getId(), paidOnDate, noNoteComment, repayment); + + repaymentDetails.add(repaymentCommand); + } + + command.setRepayments(repaymentDetails); + + return command; + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (file != null) { + file = null; + } + if (fileInputStream != null) { + fileInputStream = null; + } + if (loans != null) { + loans = null; + } + } + } + + @Override + public ImportLoanCommand populateLoanImportFromCsv() { + + ImportLoanCommand command = new ImportLoanCommand(); + + AppUser currentUser = extractAuthenticatedUser(); + + List allProducts = this.loanProductRepository.findAll(productsBelongingTo(currentUser.getOrganisation())); + + List allClients = this.clientRepository.findAll(clientsBelongingTo(currentUser.getOrganisation())); + + ClassPathResource loanCsvFile = new ClassPathResource("creocore-loans.csv"); + + File file = null; + FileInputStream fileInputStream = null; + CsvReader loans = null; + try { + DateTimeFormatter isoParser = ISODateTimeFormat.date(); + loans = new CsvReader(loanCsvFile.getInputStream(), + Charset.defaultCharset()); + loans.readHeaders(); + + List parsedLoanDetails = new ArrayList(); + + while (loans.readRecord()) { + String clientIdentifier = loans.get("ClientExternalId"); + Client matchingClient = findClientByIdentifier(allClients, clientIdentifier.trim()); + + String loanExternalId = loans.get("LoanExternalId"); + + String productIdentifier = loans.get("ProductExternalId"); + LoanProduct matchingProduct = findProductByIdentifier(allProducts, productIdentifier); + + String loanPrincipal = loans.get("LoanPrincipal"); + Number principal = BigDecimal.ZERO; + if (StringUtils.isNotBlank(loanPrincipal)) { + principal = Double.valueOf(loanPrincipal); + } + + String interestRateAsString = loans.get("InterestRate"); + Number interestRatePerYear = BigDecimal.ZERO; + if (StringUtils.isNotBlank(interestRateAsString)) { + interestRatePerYear = Double.valueOf(interestRateAsString); + } + + String submitted = loans.get("SubmittedOn"); + LocalDate submittedOnDate = null; + if (StringUtils.isNotBlank(submitted)) { + submittedOnDate = new LocalDate(isoParser.parseDateTime(submitted)); + } + + String noNoteComment = "No note due to migration from mifos."; + + String installmentsAsString = loans.get("Installments"); + Integer numberOfRepayments = Integer.valueOf(0); + if (StringUtils.isNotBlank(installmentsAsString)) { + numberOfRepayments = Integer.valueOf(installmentsAsString); + } + + String approved = loans.get("ApprovedOn"); + LocalDate approvedDate = null; + if (StringUtils.isNotBlank(approved)) { + approvedDate = new LocalDate(isoParser.parseDateTime(approved)); + } + + String disbursed = loans.get("DateDisbursalTransaction"); + LocalDate disbursedDate = null; + LocalDate expectedDisbursementDate = null; + + if (StringUtils.isNotBlank(disbursed)) { + disbursedDate = new LocalDate(isoParser.parseDateTime(disbursed)); + expectedDisbursementDate = disbursedDate; + } else { + expectedDisbursementDate = approvedDate; + } + + String repaymentsStartOn = loans.get("FirstInstallmentDate"); + LocalDate repaymentsStartingFromDate = null; + LocalDate interestCalculatedFromDate = null; + if (StringUtils.isNotBlank(repaymentsStartOn)) { + repaymentsStartingFromDate = new LocalDate(isoParser.parseDateTime(repaymentsStartOn)); + interestCalculatedFromDate = expectedDisbursementDate; + } + + String currencyCode = matchingProduct.getCurrency().getCode(); + int digitsAfterDecimal = matchingProduct.getCurrency().getDigitsAfterDecimal(); + boolean flexibleRepaymentSchedule = matchingProduct.isFlexibleRepaymentSchedule(); + Integer repaymentEvery = matchingProduct.getRepayEvery(); + Integer repaymentFrequency = matchingProduct.getRepaymentPeriodFrequencyType().getValue(); + Integer amortizationMethod = matchingProduct.getAmortizationMethod().getValue(); + + final Money toleranceAmount = matchingProduct.getInArrearsTolerance(); + + Number interestRatePerPeriod = interestRatePerYear; + Integer interestRatePeriodFrequency = PeriodFrequencyType.YEARS.getValue(); + Integer interestMethod = matchingProduct.getInterestMethod().getValue(); + + CalculateLoanScheduleCommand calculateLoanScheduleCommand = new CalculateLoanScheduleCommand(currencyCode, digitsAfterDecimal, principal, + interestRatePerPeriod, + interestRatePeriodFrequency, + interestMethod, + repaymentEvery, + repaymentFrequency, numberOfRepayments, amortizationMethod, flexibleRepaymentSchedule, + matchingProduct.isInterestRebateAllowed(), expectedDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate); + + LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateLoanScheduleCommand); + + SubmitLoanApplicationCommand parsedNewLoanDetails = new SubmitLoanApplicationCommand(matchingClient.getId(), matchingProduct.getId(), submittedOnDate, noNoteComment, + expectedDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate, loanSchedule, + currencyCode, digitsAfterDecimal, principal, + interestRatePerPeriod, interestRatePeriodFrequency, interestMethod, + repaymentEvery, repaymentFrequency, numberOfRepayments, amortizationMethod, toleranceAmount.getAmount(), + flexibleRepaymentSchedule, matchingProduct.isInterestRebateAllowed()); + + parsedNewLoanDetails.setExternalId(loanExternalId); + + LoanStateTransitionCommand approvedLoanCommand = null; + if (approvedDate != null) { + approvedLoanCommand = new LoanStateTransitionCommand(null, approvedDate, noNoteComment); + } + + LoanStateTransitionCommand disburseLoanCommand = null; + if (disbursedDate != null) { + disburseLoanCommand = new LoanStateTransitionCommand(null, disbursedDate, noNoteComment); + } + + SubmitApproveDisburseLoanCommand submitApproveDisburseLoanCommand = new SubmitApproveDisburseLoanCommand(); + submitApproveDisburseLoanCommand.setSubmitLoanApplicationCommand(parsedNewLoanDetails); + submitApproveDisburseLoanCommand.setApproveLoanCommand(approvedLoanCommand); + submitApproveDisburseLoanCommand.setDisburseLoanCommand(disburseLoanCommand); + + parsedLoanDetails.add(submitApproveDisburseLoanCommand); + } + + command.setLoans(parsedLoanDetails); + + return command; + } catch (FileNotFoundException e) { + throw new RuntimeException(e); +// throw new BulkuploadFileNotFoundException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); +// throw new BulkuploadInvalidDataException(e); + } catch (IOException e) { + throw new RuntimeException(e); +// throw new BulkuploadParseException(e); + } finally { + if (file != null) { + file = null; + } + if (fileInputStream != null) { + fileInputStream = null; + } + if (loans != null) { + loans = null; + } + } + } + + @Override + public ImportClientCommand populateClientImportFromCsv() { + + ImportClientCommand command = new ImportClientCommand(); + + AppUser currentUser = extractAuthenticatedUser(); + + List allOffices = this.officeRepository.findAll(officesBelongingTo(currentUser.getOrganisation())); + + ClassPathResource clientCsv = new ClassPathResource("creocore-clients.csv"); + + File file = null; + FileInputStream fileInputStream = null; + CsvReader clients = null; + try { + DateTimeFormatter isoParser = ISODateTimeFormat.date(); + clients = new CsvReader(clientCsv.getInputStream(), + Charset.defaultCharset()); + clients.readHeaders(); + +// int clientCsvHeaderCount = clients.getHeaderCount(); +// if (clientCsvHeaderCount != 5) { +// success = false; +// } + + List parsedClients = new ArrayList(); + while (clients.readRecord()) { + String externalId = clients.get("ExternalId"); + String firstname = clients.get("FirstName"); + String lastname = clients.get("LastName"); + String joined = clients.get("Joined"); + String officeIdentifier = clients.get("Office"); + Office matchingOffice = findOfficeByIdentifier(allOffices, officeIdentifier.trim()); + + LocalDate joiningDate = new LocalDate(isoParser.parseDateTime(joined)); + + EnrollClientCommand parsedClient = new EnrollClientCommand(firstname, lastname, "", matchingOffice.getId(), joiningDate); + parsedClient.setExternalId(externalId); + + parsedClients.add(parsedClient); + } + + command.setClients(parsedClients); + + return command; + } catch (FileNotFoundException e) { +// throw new BulkuploadFileNotFoundException(e); + } catch (IllegalArgumentException e) { +// throw new BulkuploadInvalidDataException(e); + } catch (IOException e) { +// throw new BulkuploadParseException(e); + } finally { + if (file != null) { + file = null; + } + if (fileInputStream != null) { + fileInputStream = null; + } + if (clients != null) { + clients = null; + } + } + + return command; + } + + private Office findById(List allOffices, Long id) { + + Office match = null; + + for (Office office : allOffices) { + if (id.equals(office.getId())) { + match = office; + break; + } + } + + return match; + } + + + private Loan findLoanByIdentifier(List allEntities, String identifier) { + Loan match = null; + + for (Loan entity : allEntities) { + if (entity.identifiedBy(identifier)) { + match = entity; + break; + } + } + + return match; + } + + private Office findOfficeByIdentifier(List allOffices, String officeIdentifier) { + Office match = null; + + for (Office office : allOffices) { + if (office.identifiedBy(officeIdentifier)) { + match = office; + break; + } + } + + return match; + } + + private Client findClientByIdentifier(List allClients, String identifier) { + Client match = null; + + for (Client client : allClients) { + if (client.identifiedBy(identifier)) { + match = client; + break; + } + } + + return match; + } + + private LoanProduct findProductByIdentifier(List allProducts, String identifier) { + LoanProduct match = null; + + for (LoanProduct client : allProducts) { + if (client.identifiedBy(identifier)) { + match = client; + break; + } + } + + return match; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/InvalidSqlException.java b/mifosng-provider/src/main/java/org/mifosng/platform/InvalidSqlException.java new file mode 100644 index 000000000..e87d4fe5f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/InvalidSqlException.java @@ -0,0 +1,17 @@ +package org.mifosng.platform; + +import java.sql.SQLException; + +public class InvalidSqlException extends RuntimeException { + + private final String sql; + + public InvalidSqlException(SQLException e, String sql) { + super(e); + this.sql = sql; + } + + public String getSql() { + return sql; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/LoanProductValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/LoanProductValidator.java new file mode 100644 index 000000000..75205cd82 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/LoanProductValidator.java @@ -0,0 +1,219 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.CreateLoanProductCommand; +import org.mifosng.data.command.LoanProductCommandData; +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.InterestMethod; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; + +public class LoanProductValidator { + + public void validateForUpdate(UpdateLoanProductCommand command) { + + List dataValidationErrors = new ArrayList(); + + Long productId = command.getId(); + if (productId == null) { + ErrorResponse error = new ErrorResponse("validation.msg.product.id.cannot.be.empty", "id"); + dataValidationErrors.add(error); + } + + validateRemainingAttributes(command, dataValidationErrors); + } + + public void validateForCreate(CreateLoanProductCommand command) { + List dataValidationErrors = new ArrayList(); + + validateRemainingAttributes(command, dataValidationErrors); + } + + private void validateRemainingAttributes(LoanProductCommandData command, List dataValidationErrors) { + + if (StringUtils.isBlank(command.getName())) { + ErrorResponse error = new ErrorResponse("validation.msg.product.name.cannot.be.blank", "name"); + dataValidationErrors.add(error); + } + + if (command.getDescription() != null) { + if (command.getDescription().length() > 500) { + ErrorResponse error = new ErrorResponse("validation.msg.product.description.exceeds.max.length", "description"); + dataValidationErrors.add(error); + } + } + + if (StringUtils.isBlank(command.getCurrencyCode())) { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.cannot.be.empty", "currencyCode"); + dataValidationErrors.add(error); + } + + if (command.getDigitsAfterDecimal() != null) { + + if (command.getDigitsAfterDecimal() < 0 || command.getDigitsAfterDecimal() > 6) { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.must.be.between.zero.and.six.inclusive", "selectedDigitsAfterDecimal", command.getDigitsAfterDecimal()); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.currency.digitsAfterDecimal.cannot.be.empty", "selectedDigitsAfterDecimal"); + dataValidationErrors.add(error); + } + + if (command.getPrincipal() != null) { + if (command.getPrincipal().doubleValue() <= Double.valueOf("0.0")) { + ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.must.be.greater.than.zero", "amount"); + dataValidationErrors.add(error); + } + // TODO - check number of digits before decimal and after decimal + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.principal.amount.cannot.be.empty", "amount"); + dataValidationErrors.add(error); + } + + if (command.getRepaymentFrequency() != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency()); + ErrorResponse error = null; + switch (frequencyType) { + case DAYS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 365); + break; + case WEEKS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 52); + break; + case MONTHS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 12); + break; + case YEARS: + error = validateNumberExistsAndInRange("validation.msg.product.repayment.repaidEvery", "repaidEvery", command.getRepaymentEvery(), 1, 2); + break; + default: + error = new ErrorResponse("validation.msg.product.repayment.frequency.invalid", "selectedRepaymentFrequencyOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.repayment.frequency.cannot.be.empty", "selectedRepaymentFrequencyOption"); + dataValidationErrors.add(error); + } + + if (command.getNumberOfRepayments() != null) { + if (command.getNumberOfRepayments() < 1 || command.getNumberOfRepayments() > 100) { + ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.is.out.of.range", "numberOfRepayments"); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.number.of.repayments.cannot.be.empty", "numberOfRepayments"); + dataValidationErrors.add(error); + } + + if (command.getAmortizationMethod() != null) { + AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + ErrorResponse error = null; + switch (amortizationMethod) { + case EQUAL_PRINCIPAL: + break; + case EQUAL_INSTALLMENTS: + break; + default: + error = new ErrorResponse("validation.msg.product.amortization.method.invalid", "selectedAmortizationMethodOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.amortization.method.cannot.be.empty", "selectedAmortizationMethodOption"); + dataValidationErrors.add(error); + } + + // refactor in validation of monetary value. + if (command.getInArrearsToleranceAmount() != null) { + if (command.getInArrearsToleranceAmount().doubleValue() < Double.valueOf("0.0")) { + ErrorResponse error = new ErrorResponse("validation.msg.product.arrears.tolerance.amount.cannot.be.negative", "inArrearsTolerance"); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.arrears.tolerance.amount.cannot.be.empty", "inArrearsTolerance"); + dataValidationErrors.add(error); + } + + if (command.getInterestRatePerPeriod()!= null) { + if (command.getInterestRatePerPeriod().doubleValue() < Double.valueOf("0.0")) { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.negative", "nominalInterestRate"); + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.rate.per.period.amount.cannot.be.empty", "nominalInterestRate"); + dataValidationErrors.add(error); + } + + // interest rate period type + if (command.getInterestRateFrequencyMethod() != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + ErrorResponse error = null; + switch (frequencyType) { + case DAYS: + break; + case WEEKS: + break; + case MONTHS: + break; + case YEARS: + break; + default: + error = new ErrorResponse("validation.msg.product.interest.frequency.invalid", "selectedInterestFrequencyOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.frequency.cannot.be.empty", "selectedInterestFrequencyOption"); + dataValidationErrors.add(error); + } + + if (command.getInterestMethod() != null) { + InterestMethod interestMethod = InterestMethod.fromInt(command.getInterestMethod()); + ErrorResponse error = null; + switch (interestMethod) { + case DECLINING_BALANCE: + break; + case FLAT: + break; + default: + error = new ErrorResponse("validation.msg.product.interest.method.invalid", "selectedInterestMethodOption"); + break; + } + if (error != null) { + dataValidationErrors.add(error); + } + } else { + ErrorResponse error = new ErrorResponse("validation.msg.product.interest.method.cannot.be.empty", "selectedInterestMethodOption"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Validation errors exist."); + } + + } + + private ErrorResponse validateNumberExistsAndInRange(String errorCodePreFix, String fieldName, Integer value, int min, int max) { + ErrorResponse error = null; + if (value == null) { + error = new ErrorResponse(errorCodePreFix + ".cannot.be.empty", fieldName); + } else { + if (value < min || value > max) { + error = new ErrorResponse(errorCodePreFix + ".outside.allowed.range", fieldName); + } + } + return error; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/LoanStateTransitionCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/LoanStateTransitionCommandValidator.java new file mode 100644 index 000000000..8ee1c898d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/LoanStateTransitionCommandValidator.java @@ -0,0 +1,42 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.LoanStateTransitionCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class LoanStateTransitionCommandValidator { + + private final LoanStateTransitionCommand command; + + public LoanStateTransitionCommandValidator(LoanStateTransitionCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (command.getLoanId() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId"); + dataValidationErrors.add(error); + } + + if (command.getEventDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.state.transition.date.cannot.be.blank", "eventDate"); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) { + ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "comment"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/LoanTransactionValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/LoanTransactionValidator.java new file mode 100644 index 000000000..c16556d9f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/LoanTransactionValidator.java @@ -0,0 +1,47 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.LoanTransactionCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class LoanTransactionValidator { + + private final LoanTransactionCommand command; + + public LoanTransactionValidator(LoanTransactionCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (command.getLoanId() == null || command.getLoanId().doubleValue() <= 0) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.id.is.invalid", "loanId"); + dataValidationErrors.add(error); + } + + if (command.getPaymentDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.date.cannot.be.blank", "transactionDate"); + dataValidationErrors.add(error); + } + + if (command.getPaymentAmount() == null || command.getPaymentAmount().doubleValue() <= 0) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.repayment.must.be.greater.than.zero", "transactionAmount"); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(command.getComment()) && command.getComment().length() > 1000) { + ErrorResponse error = new ErrorResponse("validation.msg.note.exceeds.max.length", "transactionComment"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/OfficeValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/OfficeValidator.java new file mode 100644 index 000000000..a42f69bec --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/OfficeValidator.java @@ -0,0 +1,56 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosng.data.ErrorResponse; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class OfficeValidator { + + private final String name; + private final Long parentId; + private final LocalDate openingDate; + private final String externalId; + + public OfficeValidator(String name, Long parentId, LocalDate openingDate, String externalId) { + this.name = name; + this.parentId = parentId; + this.openingDate = openingDate; + this.externalId = externalId; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (StringUtils.isBlank(this.name)) { + ErrorResponse error = new ErrorResponse("validation.msg.office.name.cannot.be.blank", "name"); + dataValidationErrors.add(error); + } + + if (this.parentId == null || parentId.equals(Long.valueOf(-1))) { + ErrorResponse error = new ErrorResponse("validation.msg.office.parent.cannot.be.blank", "parentId"); + dataValidationErrors.add(error); + } + + if (this.openingDate == null) { + ErrorResponse error = new ErrorResponse("validation.msg.office.opening.date.cannot.be.blank", "openingDate"); + dataValidationErrors.add(error); + } + + if (StringUtils.isNotBlank(this.externalId)) { + + if (this.externalId.trim().length() > 100) { + ErrorResponse error = new ErrorResponse("validation.msg.office.externalId.exceeds.max.length", "externalId"); + dataValidationErrors.add(error); + } + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformService.java new file mode 100644 index 000000000..88f925c45 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformService.java @@ -0,0 +1,100 @@ +package org.mifosng.platform; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.mifosng.data.AppUserData; +import org.mifosng.data.ClientData; +import org.mifosng.data.ClientDataWithAccountsData; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.EnumOptionReadModel; +import org.mifosng.data.ExtraDatasets; +import org.mifosng.data.LoanAccountData; +import org.mifosng.data.LoanProductData; +import org.mifosng.data.LoanRepaymentData; +import org.mifosng.data.NewLoanWorkflowStepOneData; +import org.mifosng.data.NoteData; +import org.mifosng.data.OfficeData; +import org.mifosng.data.OrganisationReadModel; +import org.mifosng.data.PermissionData; +import org.mifosng.data.RoleData; +import org.mifosng.data.reports.GenericResultset; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface ReadPlatformService { + + Collection retrieveAll(); + + Collection retrieveAllIndividualClients(); + + Collection retrieveAllOffices(); + + ClientData retrieveIndividualClient(Long clientId); + + ClientDataWithAccountsData retrieveClientAccountDetails(Long clientId); + + NewLoanWorkflowStepOneData retrieveClientAndProductDetails(Long clientId, Long productId); + + LoanAccountData retrieveLoanAccountDetails(Long loanId); + + LoanRepaymentData retrieveNewLoanRepaymentDetails(Long loanId); + + LoanRepaymentData retrieveNewLoanWaiverDetails(Long loanId); + + LoanRepaymentData retrieveLoanRepaymentDetails(Long loanId, Long repaymentId); + + Collection retrieveAllLoanProducts(); + + List retrieveAllowedCurrencies(); + + List retrieveAllPlatformCurrencies(); + + List retrieveLoanAmortizationMethodOptions(); + + List retrieveLoanInterestMethodOptions(); + + List retrieveRepaymentFrequencyOptions(); + + List retrieveInterestFrequencyOptions(); + + LoanProductData retrieveLoanProduct(Long productId); + + LoanProductData retrieveNewLoanProductDetails(); + + OfficeData retrieveOffice(Long officeId); + + Collection retrieveAllUsers(); + + AppUserData retrieveNewUserDetails(); + + AppUserData retrieveUser(Long userId); + + AppUserData retrieveCurrentUser(); + + Collection retrieveAllRoles(); + + RoleData retrieveRole(Long roleId); + + Collection retrieveAllPermissionGroups(); + + Collection retrieveAllPermissions(); + + @PreAuthorize(value = "hasAnyRole('REPORTING_SUPER_USER_ROLE')") + GenericResultset retrieveGenericResultset(String rptDB, String name, + String type, Map extractedQueryParams); + + ClientData retrieveNewClientDetails(); + + Collection retrieveAllClientNotes(Long clientId); + + NoteData retrieveClientNote(Long clientId, Long noteId); + + ExtraDatasets retrieveExtraDatasetNames(String datasetType); + + GenericResultset retrieveExtraData(String datasetType, String datasetName, + String datasetPKValue); + + void tempSaveExtraData(String datasetType, String datasetName, + String datasetPKValue, Map queryParams); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformServiceImpl.java new file mode 100644 index 000000000..506f090bb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/ReadPlatformServiceImpl.java @@ -0,0 +1,1694 @@ +package org.mifosng.platform; + +import static org.mifosng.platform.Specifications.clientsThatMatch; +import static org.mifosng.platform.Specifications.loanTransactionsThatMatch; +import static org.mifosng.platform.Specifications.loansThatMatch; +import static org.mifosng.platform.Specifications.usersThatMatch; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.mifosng.data.AppUserData; +import org.mifosng.data.ClientData; +import org.mifosng.data.ClientDataWithAccountsData; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.DerivedLoanData; +import org.mifosng.data.EnumOptionReadModel; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ExtraDatasets; +import org.mifosng.data.LoanAccountData; +import org.mifosng.data.LoanProductData; +import org.mifosng.data.LoanRepaymentData; +import org.mifosng.data.MoneyData; +import org.mifosng.data.NewLoanWorkflowStepOneData; +import org.mifosng.data.NoteData; +import org.mifosng.data.OfficeData; +import org.mifosng.data.OrganisationReadModel; +import org.mifosng.data.PermissionData; +import org.mifosng.data.RoleData; +import org.mifosng.data.reports.GenericResultset; +import org.mifosng.data.reports.ResultsetColumnHeader; +import org.mifosng.data.reports.ResultsetDataRow; +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.client.domain.ClientRepository; +import org.mifosng.platform.currency.domain.ApplicationCurrency; +import org.mifosng.platform.currency.domain.ApplicationCurrencyRepository; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanRepository; +import org.mifosng.platform.loan.domain.LoanTransaction; +import org.mifosng.platform.loan.domain.LoanTransactionRepository; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; +import org.mifosng.platform.user.domain.AppUserRepository; +import org.mifosng.platform.user.domain.PermissionGroup; +import org.mifosng.platform.user.domain.Role; +import org.mifosng.platform.user.domain.RoleRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class ReadPlatformServiceImpl implements ReadPlatformService { + + private final static Logger logger = LoggerFactory + .getLogger(ReadPlatformServiceImpl.class); + + private final SimpleJdbcTemplate jdbcTemplate; + private final ClientRepository clientRepository; + private final LoanRepository loanRepository; + private final LoanTransactionRepository loanTransactionRepository; + private final ApplicationCurrencyRepository applicationCurrencyRepository; + private final DataSource dataSource; + private final AppUserRepository appUserRepository; + private final RoleRepository roleRepository; + + @Autowired + public ReadPlatformServiceImpl(final DataSource dataSource, + final ClientRepository clientRepository, + final LoanRepository loanRepository, + final LoanTransactionRepository loanTransactionRepository, + final ApplicationCurrencyRepository applicationCurrencyRepository, + final AppUserRepository appUserRepository, + final RoleRepository roleRepository) { + this.loanTransactionRepository = loanTransactionRepository; + this.applicationCurrencyRepository = applicationCurrencyRepository; + this.appUserRepository = appUserRepository; + this.roleRepository = roleRepository; + this.jdbcTemplate = new SimpleJdbcTemplate(dataSource); + this.clientRepository = clientRepository; + this.loanRepository = loanRepository; + this.dataSource = dataSource; + } + + private AppUser extractAuthenticatedUser() { + AppUser currentUser = null; + SecurityContext context = SecurityContextHolder.getContext(); + if (context != null) { + Authentication auth = context.getAuthentication(); + if (auth != null) { + currentUser = (AppUser) auth.getPrincipal(); + } + } + + if (currentUser == null) { + throw new ClientNotAuthenticatedException(); + } + + return currentUser; + } + + @Override + public Collection retrieveAll() { + + String sql = "select o.id as id, o.name as name, o.contact_name as contactName, o.opening_date as openingDate from org_organisation o"; + + RowMapper rm = new OrganisationMapper(); + + return this.jdbcTemplate.query(sql, rm); + } + + protected static final class OrganisationMapper implements + RowMapper { + + @Override + public OrganisationReadModel mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + String name = rs.getString("name"); + String contactName = rs.getString("contactName"); + LocalDate openingDate = new LocalDate(rs.getDate("openingDate")); + + return new OrganisationReadModel(id, name, contactName, openingDate); + } + } + + @Override + public Collection retrieveAllIndividualClients() { + + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + String officeIdsList = generateOfficeIdInClause(offices); + ClientMapper rm = new ClientMapper(offices, currentUser.getOrganisation()); + + String sql = "select " + + rm.clientSchema() + + " where c.org_id = ? and c.office_id in (" + officeIdsList + ") order by c.lastname ASC, c.firstname ASC"; + + return this.jdbcTemplate.query(sql, rm, new Object[] { currentUser.getOrganisation().getId()}); + } + + private String generateOfficeIdInClause(List offices) { + String officeIdsList = ""; + for (int i=0; i < offices.size(); i++) { + Long id = offices.get(i).getId(); + if (i==0) { + officeIdsList = id.toString(); + } else { + officeIdsList += "," + id.toString(); + } + } + return officeIdsList; + } + + @Override + public NoteData retrieveClientNote(Long clientId, Long noteId) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + Collection allUsers = retrieveAllUsers(); + + NoteMapper noteMapper = new NoteMapper(allUsers); + + String sql = "select " + noteMapper.schema() + + " where n.org_id = ? and n.client_id = ? and n.id = ?"; + + return this.jdbcTemplate.queryForObject(sql, noteMapper, + new Object[] { currentUser.getOrganisation().getId(), + clientId, noteId }); + } catch (EmptyResultDataAccessException e) { + ErrorResponse errorResponse = new ErrorResponse( + "error.msg.client.id.invalid", "id"); + throw new ApplicationDomainRuleException( + Arrays.asList(errorResponse), "Client with identifier " + + clientId.toString() + " does not exist."); + } + } + + @Override + public Collection retrieveAllClientNotes(Long clientId) { + + AppUser currentUser = extractAuthenticatedUser(); + + Collection allUsers = retrieveAllUsers(); + + NoteMapper noteMapper = new NoteMapper(allUsers); + + String sql = "select " + + noteMapper.schema() + + " where n.org_id = ? and n.client_id = ? order by n.created_date DESC"; + + return this.jdbcTemplate.query(sql, noteMapper, new Object[] { + currentUser.getOrganisation().getId(), clientId }); + } + + protected static final class NoteMapper implements RowMapper { + + private final Collection allUsers; + + public NoteMapper(Collection allUsers) { + this.allUsers = allUsers; + } + + public String schema() { + return "n.id as id, n.client_id as clientId, n.loan_id as loanId, n.loan_transaction_id as transactionId, n.note_type_enum as noteTypeEnum, n.note as note, " + + "n.created_date as createdDate, n.createdby_id as createdById, n.lastmodified_date as lastModifiedDate, n.lastmodifiedby_id as lastModifiedById" + + " from portfolio_note n"; + } + + @Override + public NoteData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + Long clientId = rs.getLong("clientId"); + Long loanId = rs.getLong("loanId"); + Long transactionId = rs.getLong("transactionId"); + Integer noteTypeId = rs.getInt("noteTypeEnum"); + String note = rs.getString("note"); + + DateTime createdDate = new DateTime(rs.getTimestamp("createdDate")); + Long createdById = rs.getLong("createdById"); + String createdByUsername = findUserById(createdById, allUsers); + + DateTime lastModifiedDate = new DateTime( + rs.getTimestamp("lastModifiedDate")); + Long lastModifiedById = rs.getLong("lastModifiedById"); + String updatedByUsername = findUserById(createdById, allUsers); + + return new NoteData(id, clientId, loanId, transactionId, + noteTypeId, note, createdDate, createdById, + createdByUsername, lastModifiedDate, lastModifiedById, + updatedByUsername); + } + + private String findUserById(Long createdById, + Collection allUsers) { + String username = ""; + for (AppUserData appUserData : allUsers) { + if (appUserData.getId().equals(createdById)) { + username = appUserData.getUsername(); + break; + } + } + return username; + } + } + + protected static final class ClientMapper implements RowMapper { + + private final List offices; + private final Organisation organisation; + + public ClientMapper(final List offices, Organisation organisation) { + this.offices = offices; + this.organisation = organisation; + } + + public String clientSchema() { + return "c.org_id as orgId, c.office_id as officeId, c.id as id, c.firstname as firstname, c.lastname as lastname, c.external_id as externalId, c.joining_date as joinedDate from portfolio_client c"; + } + + @Override + public ClientData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long orgId = rs.getLong("orgId"); + Long officeId = rs.getLong("officeId"); + Long id = rs.getLong("id"); + String firstname = rs.getString("firstname"); + if (StringUtils.isBlank(firstname)) { + firstname = ""; + } + String lastname = rs.getString("lastname"); + String externalId = rs.getString("externalId"); + LocalDate joinedDate = new LocalDate(rs.getDate("joinedDate")); + + String officeName = fromOfficeList(this.offices, officeId); + + String orgname = ""; + if (organisation.getId().equals(orgId)) { + orgname = organisation.getName(); + } + + return new ClientData(orgId, orgname, officeId, officeName, id, firstname, lastname, externalId, joinedDate); + } + + private String fromOfficeList(final List officeList, + final Long officeId) { + String match = ""; + for (OfficeData office : officeList) { + if (office.getId().equals(officeId)) { + match = office.getName(); + } + } + + return match; + } + } + + private List retrieveOffices() { + + AppUser currentUser = extractAuthenticatedUser(); + + String hierarchy = currentUser.getOffice().getHierarchy(); + String hierarchySearchString = hierarchy + "%"; + + OfficeMapper rm = new OfficeMapper(); + String sql = "select " + rm.officeSchema() + "where o.org_id = ? and o.hierarchy like ? order by o.hierarchy"; + + return this.jdbcTemplate.query(sql, rm, new Object[] {currentUser.getOrganisation().getId(), hierarchySearchString}); + } + + protected static final class OfficeMapper implements RowMapper { + + public String officeSchema() { + return " o.id as id, o.name as name, o.external_id as externalId, o.opening_date as openingDate, o.hierarchy as hierarchy, parent.id as parentId, parent.name as parentName " + + "from org_office o LEFT JOIN org_office AS parent ON parent.id = o.parent_id "; + } + + @Override + public OfficeData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + String name = rs.getString("name"); + String externalId = rs.getString("externalId"); + LocalDate openingDate = new LocalDate(rs.getDate("openingDate")); + String hierarchy = rs.getString("hierarchy"); + Long parentId = rs.getLong("parentId"); + String parentName = rs.getString("parentName"); + + return new OfficeData(id, name, externalId, openingDate, hierarchy, parentId, parentName); + } + } + + @Override + public Collection retrieveAllOffices() { + + return retrieveOffices(); + } + + @Override + public OfficeData retrieveOffice(final Long officeId) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + OfficeMapper rm = new OfficeMapper(); + String sql = "select " + rm.officeSchema() + + " where o.org_id = ? and o.id = ?"; + + OfficeData selectedOffice = this.jdbcTemplate.queryForObject(sql, + rm, new Object[] { currentUser.getOrganisation().getId(), + officeId }); + + List allowedParents = new ArrayList(); + + if (StringUtils.isNotBlank(selectedOffice.getParentName())) { + Collection allOffices = retrieveAllOffices(); + + for (OfficeData office : allOffices) { + + if (!office.getId().equals(selectedOffice.getId())) { + allowedParents.add(office); + } + } + } + + selectedOffice.setAllowedParents(allowedParents); + + return selectedOffice; + } catch (EmptyResultDataAccessException e) { + ErrorResponse errorResponse = new ErrorResponse( + "error.msg.office.id.invalid", "id"); + throw new ApplicationDomainRuleException( + Arrays.asList(errorResponse), "Office with identifier " + + officeId.toString() + " does not exist."); + } + } + + @Override + public ClientData retrieveIndividualClient(final Long clientId) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + ClientMapper rm = new ClientMapper(offices, currentUser.getOrganisation()); + + String sql = "select " + rm.clientSchema() + + " where c.id = ? and c.org_id = ?"; + + return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { + clientId, currentUser.getOrganisation().getId() }); + } catch (EmptyResultDataAccessException e) { + ErrorResponse errorResponse = new ErrorResponse( + "error.msg.client.id.invalid", "id"); + throw new ApplicationDomainRuleException( + Arrays.asList(errorResponse), "Client with identifier " + + clientId.toString() + " does not exist."); + } + } + + @Override + public ClientDataWithAccountsData retrieveClientAccountDetails( + final Long clientId) { + + AppUser currentUser = extractAuthenticatedUser(); + + ClientData clientAccount = retrieveIndividualClient(clientId); + + // TODO - rewrite using jdbc sql approach + Client client = this.clientRepository.findOne(clientsThatMatch( + currentUser.getOrganisation(), clientId)); + + Collection allLoans = this.loanRepository.findAll(loansThatMatch( + currentUser.getOrganisation(), client)); + + List pendingApprovalLoans = new ArrayList(); + List awaitingDisbursalLoans = new ArrayList(); + List openLoans = new ArrayList(); + List closedLoans = new ArrayList(); + + for (Loan realLoan : allLoans) { + + ApplicationCurrency currency = this.applicationCurrencyRepository + .findOneByCode(realLoan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode()); + + CurrencyData currencyData = new CurrencyData(currency.getCode(), + currency.getName(), currency.getDecimalPlaces(), + currency.getDisplaySymbol(), currency.getNameCode()); + + LoanAccountData loan = convertToData(realLoan, currencyData); + + if (loan.isClosed() || loan.isInterestRebateOutstanding()) { + closedLoans.add(loan); + } else if (loan.isPendingApproval()) { + pendingApprovalLoans.add(loan); + } else if (loan.isWaitingForDisbursal()) { + awaitingDisbursalLoans.add(loan); + } else if (loan.isOpen()) { + openLoans.add(loan); + } + } + + Collections.sort(closedLoans, new ClosedLoanComparator()); + + return new ClientDataWithAccountsData(clientAccount, + pendingApprovalLoans, awaitingDisbursalLoans, openLoans, + closedLoans); + } + + @Override + public LoanRepaymentData retrieveNewLoanRepaymentDetails(Long loanId) { + + AppUser currentUser = extractAuthenticatedUser(); + + // TODO - OPTIMIZE - write simple sql query to fetch back date of + // possible next transaction date. + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), loanId)); + + ApplicationCurrency currency = this.applicationCurrencyRepository + .findOneByCode(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode()); + + CurrencyData currencyData = new CurrencyData(currency.getCode(), + currency.getName(), currency.getDecimalPlaces(), + currency.getDisplaySymbol(), currency.getNameCode()); + + LocalDate earliestUnpaidInstallmentDate = loan + .possibleNextRepaymentDate(); + Money possibleNextRepaymentAmount = loan.possibleNextRepaymentAmount(); + MoneyData possibleNextRepayment = MoneyData.of(currencyData, + possibleNextRepaymentAmount.getAmount()); + + LoanRepaymentData newRepaymentDetails = new LoanRepaymentData(); + newRepaymentDetails.setDate(earliestUnpaidInstallmentDate); + newRepaymentDetails.setTotal(possibleNextRepayment); + + return newRepaymentDetails; + } + + @Override + public LoanRepaymentData retrieveNewLoanWaiverDetails(Long loanId) { + + AppUser currentUser = extractAuthenticatedUser(); + + // TODO - OPTIMIZE - write simple sql query to fetch back date of + // possible next transaction date. + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), loanId)); + + ApplicationCurrency currency = this.applicationCurrencyRepository + .findOneByCode(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode()); + + CurrencyData currencyData = new CurrencyData(currency.getCode(), + currency.getName(), currency.getDecimalPlaces(), + currency.getDisplaySymbol(), currency.getNameCode()); + + Money totalOutstanding = loan.getTotalOutstanding(); + MoneyData totalOutstandingData = MoneyData.of(currencyData, + totalOutstanding.getAmount()); + + LoanRepaymentData newWaiverDetails = new LoanRepaymentData(); + newWaiverDetails.setDate(new LocalDate()); + newWaiverDetails.setTotal(totalOutstandingData); + + return newWaiverDetails; + } + + @Override + public LoanRepaymentData retrieveLoanRepaymentDetails(Long loanId, + Long repaymentId) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanTransaction transaction = this.loanTransactionRepository + .findOne(loanTransactionsThatMatch( + currentUser.getOrganisation(), repaymentId)); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), loanId)); + + ApplicationCurrency currency = this.applicationCurrencyRepository + .findOneByCode(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode()); + + CurrencyData currencyData = new CurrencyData(currency.getCode(), + currency.getName(), currency.getDecimalPlaces(), + currency.getDisplaySymbol(), currency.getNameCode()); + MoneyData total = MoneyData.of(currencyData, transaction.getAmount()); + LocalDate date = transaction.getTransactionDate(); + + LoanRepaymentData loanRepaymentData = new LoanRepaymentData(); + loanRepaymentData.setId(repaymentId); + loanRepaymentData.setTotal(total); + loanRepaymentData.setDate(date); + + return loanRepaymentData; + } + + public NewLoanWorkflowStepOneData retrieveClientAndProductDetails(final Long clientId, final Long productId) { + + AppUser currentUser = extractAuthenticatedUser(); + + NewLoanWorkflowStepOneData workflowData = new NewLoanWorkflowStepOneData(); + workflowData.setOrganisationId(currentUser.getOrganisation().getId()); + workflowData.setOrganisationName(currentUser.getOrganisation() + .getName()); + + Collection loanProducts = retrieveAllLoanProducts(); + workflowData.setAllowedProducts(new ArrayList( + loanProducts)); + + if (loanProducts.size() == 1) { + LoanProductData selectedProduct = retrieveLoanProduct(workflowData.getAllowedProducts().get(0).getId()); + + workflowData.setProductId(selectedProduct.getId()); + workflowData.setProductName(selectedProduct.getName()); + workflowData.setSelectedProduct(selectedProduct); + } else { + LoanProductData selectedProduct = findLoanProductById(loanProducts, productId); + + workflowData.setProductId(selectedProduct.getId()); + workflowData.setProductName(selectedProduct.getName()); + workflowData.setSelectedProduct(selectedProduct); + } + + ClientData clientAccount = retrieveIndividualClient(clientId); + workflowData.setClientId(clientAccount.getId()); + workflowData.setClientName(clientAccount.getDisplayName()); + + return workflowData; + } + + private LoanProductData findLoanProductById(Collection loanProducts, Long productId) { + LoanProductData match = retrieveNewLoanProductDetails(); + for (LoanProductData loanProductData : loanProducts) { + if (loanProductData.getId().equals(productId)) { + match = retrieveLoanProduct(loanProductData.getId()); + break; + } + } + return match; + } + + @Override + public LoanAccountData retrieveLoanAccountDetails(Long loanId) { + + // TODO - OPTIMISE - prefer jdbc sql approach to return only what we + // need of loan information. + AppUser currentUser = extractAuthenticatedUser(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), loanId)); + + ApplicationCurrency currency = this.applicationCurrencyRepository + .findOneByCode(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode()); + + CurrencyData currencyData = new CurrencyData(currency.getCode(), + currency.getName(), currency.getDecimalPlaces(), + currency.getDisplaySymbol(), currency.getNameCode()); + + LoanAccountData loanData = convertToData(loan, currencyData); + + return loanData; + } + + private LoanAccountData convertToData(final Loan realLoan, CurrencyData currencyData) { + + DerivedLoanData loanData = realLoan.deriveLoanData(currencyData); + + LocalDate expectedDisbursementDate = null; + if (realLoan.getExpectedDisbursedOnDate() != null) { + expectedDisbursementDate = new LocalDate( + realLoan.getExpectedDisbursedOnDate()); + } + + Money loanPrincipal = realLoan.getLoanRepaymentScheduleDetail() + .getPrincipal(); + MoneyData principal = MoneyData.of(currencyData, loanPrincipal.getAmount()); + + Money loanArrearsTolerance = realLoan.getInArrearsTolerance(); + MoneyData tolerance = MoneyData.of(currencyData, loanArrearsTolerance.getAmount()); + + Money interestRebate = realLoan.getInterestRebateOwed(); + MoneyData interestRebateOwed = MoneyData.of(currencyData, interestRebate.getAmount()); + + boolean interestRebateOutstanding = false; // realLoan.isInterestRebateOutstanding(), + + // permissions + boolean waiveAllowed = loanData.getSummary().isWaiveAllowed(tolerance) + && realLoan.isNotClosed(); + boolean undoDisbursalAllowed = realLoan.isDisbursed() + && realLoan.isOpenWithNoRepaymentMade(); + boolean makeRepaymentAllowed = realLoan.isDisbursed() + && realLoan.isNotClosed(); + + LocalDate loanStatusDate = realLoan.getLoanStatusSinceDate(); + + boolean rejectAllowed = realLoan.isNotApproved() + && realLoan.isNotDisbursed() && realLoan.isNotClosed(); + boolean withdrawnByApplicantAllowed = realLoan.isNotDisbursed() + && realLoan.isNotClosed(); + boolean undoApprovalAllowed = realLoan.isApproved() + && realLoan.isNotClosed(); + boolean disbursalAllowed = realLoan.isApproved() + && realLoan.isNotDisbursed() && realLoan.isNotClosed(); + + return new LoanAccountData(realLoan.isClosed(), realLoan.isOpen(), + realLoan.isOpenWithRepaymentMade(), interestRebateOutstanding, + realLoan.isSubmittedAndPendingApproval(), + realLoan.isWaitingForDisbursal(), undoDisbursalAllowed, + makeRepaymentAllowed, rejectAllowed, + withdrawnByApplicantAllowed, undoApprovalAllowed, + disbursalAllowed, realLoan.getLoanStatusDisplayName(), + loanStatusDate, realLoan.getId(), realLoan.getExternalId(), + realLoan.getLoanProduct().getName(), + realLoan.getClosedOnDate(), realLoan.getSubmittedOnDate(), + realLoan.getApprovedOnDate(), expectedDisbursementDate, + realLoan.getDisbursedOnDate(), realLoan.getExpectedMaturityDate(), realLoan.getExpectedFirstRepaymentOnDate(), realLoan.getInterestCalculatedFromDate(), + principal, + realLoan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), + realLoan.getLoanRepaymentScheduleDetail().getNominalInterestRatePerPeriod(), realLoan + .getLoanRepaymentScheduleDetail() + .getInterestPeriodFrequencyType().getValue(), realLoan + .getLoanRepaymentScheduleDetail() + .getInterestPeriodFrequencyType().toString(), realLoan + .getLoanRepaymentScheduleDetail().getInterestMethod() + .getValue(), realLoan.getLoanRepaymentScheduleDetail() + .getInterestMethod().toString(), realLoan + .getLoanRepaymentScheduleDetail() + .getAmortizationMethod().getValue(), realLoan + .getLoanRepaymentScheduleDetail() + .getAmortizationMethod().toString(), realLoan + .getLoanRepaymentScheduleDetail() + .getNumberOfRepayments(), realLoan + .getLoanRepaymentScheduleDetail().getRepayEvery(), + realLoan.getLoanRepaymentScheduleDetail() + .getRepaymentPeriodFrequencyType().getValue(), realLoan + .getLoanRepaymentScheduleDetail() + .getRepaymentPeriodFrequencyType().toString(), + tolerance, loanData, waiveAllowed, interestRebateOwed); + } + + @Override + public Collection retrieveAllLoanProducts() { + + AppUser currentUser = extractAuthenticatedUser(); + + List allowedCurrencies = retrieveAllowedCurrencies(); + + LoanProductMapper rm = new LoanProductMapper(allowedCurrencies); + + String sql = "select " + rm.loanProductSchema() + + " where lp.org_id = ?"; + + return this.jdbcTemplate.query(sql, rm, new Object[] { currentUser + .getOrganisation().getId() }); + } + + protected static final class LoanProductMapper implements + RowMapper { + + private final List allowedCurrencies; + + public LoanProductMapper(List allowedCurrencies) { + this.allowedCurrencies = allowedCurrencies; + } + + public String loanProductSchema() { + return "lp.id as id, lp.name as name, lp.description as description, lp.flexible_repayment_schedule as isFlexible, lp.interest_rebate as isInterestRebateAllowed, " + + "lp.principal_amount as principal, lp.currency_code as currencyCode, lp.currency_digits as currencyDigits, " + + "lp.nominal_interest_rate_per_period as interestRatePerPeriod, lp.interest_period_frequency_enum as interestRatePerPeriodFreq, " + + "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, " + + "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, " + + "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, " + + "lp.created_date as createdon, lp.lastmodified_date as modifiedon " + + " from portfolio_product_loan lp"; + } + + @Override + public LoanProductData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + String name = rs.getString("name"); + String description = rs.getString("description"); + boolean isFlexible = rs.getBoolean("isFlexible"); + boolean isInterestRebateAllowed = rs + .getBoolean("isInterestRebateAllowed"); + + String currencyCode = rs.getString("currencyCode"); + int currencyDigits = rs.getInt("currencyDigits"); + + CurrencyData currencyData = findCurrencyByCode(currencyCode, allowedCurrencies); + currencyData.setDecimalPlaces(currencyDigits); + + BigDecimal principal = rs.getBigDecimal("principal"); + BigDecimal tolerance = rs.getBigDecimal("tolerance"); + + MoneyData principalMoney = MoneyData.of(currencyData, principal); + MoneyData toleranceMoney = MoneyData.of(currencyData, tolerance); + + + BigDecimal interestRatePerPeriod = rs + .getBigDecimal("interestRatePerPeriod"); + int interestRatePeriod = rs.getInt("interestRatePerPeriodFreq"); + BigDecimal annualInterestRate = rs + .getBigDecimal("annualInterestRate"); + int interestMethod = rs.getInt("interestMethod"); + + int repaidEvery = rs.getInt("repaidEvery"); + int repaymentFrequency = rs.getInt("repaymentPeriodFrequency"); + int numberOfRepayments = rs.getInt("numberOfRepayments"); + int amortizationMethod = rs.getInt("amortizationMethod"); + + DateTime createdOn = new DateTime(rs.getDate("createdOn")); + DateTime lastModifedOn = new DateTime(rs.getDate("modifiedon")); + + return new LoanProductData(id, name, description, isFlexible, + isInterestRebateAllowed, principalMoney, + interestRatePerPeriod, interestRatePeriod, + annualInterestRate, interestMethod, repaidEvery, + repaymentFrequency, numberOfRepayments, amortizationMethod, + toleranceMoney, createdOn, lastModifedOn); + } + + private CurrencyData findCurrencyByCode(String currencyCode, List allowedCurrencies) { + CurrencyData match = null; + for (CurrencyData currencyData : allowedCurrencies) { + if (currencyData.getCode().equalsIgnoreCase(currencyCode)) { + match = currencyData; + break; + } + } + return match; + } + } + + @Override + public LoanProductData retrieveLoanProduct(final Long loanProductId) { + + List allowedCurrencies = retrieveAllowedCurrencies(); + + LoanProductMapper rm = new LoanProductMapper(allowedCurrencies); + String sql = "select " + rm.loanProductSchema() + " where lp.id = ?"; + + LoanProductData productData = this.jdbcTemplate.queryForObject(sql, rm, + new Object[] { loanProductId }); + + populateProductDataWithDropdownOptions(productData); + + return productData; + } + + private void populateProductDataWithDropdownOptions( + LoanProductData productData) { + + List possibleCurrencies = retrieveAllowedCurrencies(); + + List possibleAmortizationOptions = retrieveLoanAmortizationMethodOptions(); + List possibleInterestOptions = retrieveLoanInterestMethodOptions(); + List repaymentFrequencyOptions = retrieveRepaymentFrequencyOptions(); + List interestFrequencyOptions = retrieveInterestFrequencyOptions(); + + productData.setPossibleCurrencies(possibleCurrencies); + productData.setPossibleAmortizationOptions(possibleAmortizationOptions); + productData.setPossibleInterestOptions(possibleInterestOptions); + productData.setRepaymentFrequencyOptions(repaymentFrequencyOptions); + productData.setInterestFrequencyOptions(interestFrequencyOptions); + } + + @Override + public LoanProductData retrieveNewLoanProductDetails() { + + LoanProductData productData = new LoanProductData(); + + populateProductDataWithDropdownOptions(productData); + + if (productData.getPossibleCurrencies().size() >= 1) { + CurrencyData currency = productData.getPossibleCurrencies().get(0); + MoneyData zero = MoneyData.zero(currency); + productData.setPrincipalMoney(zero); + productData.setInArrearsTolerance(zero); + } + + productData.setAmortizationMethod(Integer.valueOf(1)); + productData.setInterestMethod(Integer.valueOf(0)); + productData.setRepaymentPeriodFrequency(2); + productData.setInterestRatePeriod(2); + + productData.setRepaidEvery(1); + productData.setNumberOfRepayments(0); + + return productData; + } + + @Override + public List retrieveAllowedCurrencies() { + + AppUser currentUser = extractAuthenticatedUser(); + + String sql = "select c.code as code, c.name as name, c.decimal_places as decimalPlaces, c.display_symbol as displaySymbol, c.internationalized_name_code as nameCode from org_organisation_currency c where c.org_id = ?"; + + RowMapper rm = new CurrencyMapper(); + + return this.jdbcTemplate.query(sql, rm, new Object[] { currentUser + .getOrganisation().getId() }); + } + + @Override + public List retrieveAllPlatformCurrencies() { + + String sql = "select c.code as code, c.name as name, c.decimal_places as decimalPlaces, c.display_symbol as displaySymbol, c.internationalized_name_code as nameCode from ref_currency c"; + + RowMapper rm = new CurrencyMapper(); + + return this.jdbcTemplate.query(sql, rm, new Object[] {}); + } + + protected static final class CurrencyMapper implements + RowMapper { + + @Override + public CurrencyData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + String code = rs.getString("code"); + String name = rs.getString("name"); + int decimalPlaces = rs.getInt("decimalPlaces"); + String displaySymbol = rs.getString("displaySymbol"); + String nameCode = rs.getString("nameCode"); + + return new CurrencyData(code, name, decimalPlaces, displaySymbol, + nameCode); + } + } + + @Override + public List retrieveLoanAmortizationMethodOptions() { + EnumOptionReadModel equalInstallments = new EnumOptionReadModel( + "Equal installments", AmortizationMethod.EQUAL_INSTALLMENTS + .getValue().longValue()); + EnumOptionReadModel equalPrinciplePayments = new EnumOptionReadModel( + "Equal principle payments", AmortizationMethod.EQUAL_PRINCIPAL + .getValue().longValue()); + + List allowedAmortizationMethods = Arrays.asList( + equalInstallments, equalPrinciplePayments); + + return allowedAmortizationMethods; + } + + @Override + public List retrieveLoanInterestMethodOptions() { + EnumOptionReadModel interestRateCalculationMethod2 = new EnumOptionReadModel( + "Declining Balance", Long.valueOf(0)); + EnumOptionReadModel interestRateCalculationMethod1 = new EnumOptionReadModel( + "Flat", Long.valueOf(1)); + + List allowedRepaymentScheduleCalculationMethods = Arrays + .asList(interestRateCalculationMethod2, + interestRateCalculationMethod1); + + return allowedRepaymentScheduleCalculationMethods; + } + + @Override + public List retrieveRepaymentFrequencyOptions() { + EnumOptionReadModel frequency1 = new EnumOptionReadModel("Days", + Long.valueOf(0)); + EnumOptionReadModel frequency2 = new EnumOptionReadModel("Weeks", + Long.valueOf(1)); + EnumOptionReadModel frequency3 = new EnumOptionReadModel("Months", + Long.valueOf(2)); + List repaymentFrequencyOptions = Arrays.asList( + frequency1, frequency2, frequency3); + return repaymentFrequencyOptions; + } + + @Override + public List retrieveInterestFrequencyOptions() { + EnumOptionReadModel perWeek = new EnumOptionReadModel("Per week", + PeriodFrequencyType.WEEKS.getValue().longValue()); + EnumOptionReadModel perMonth = new EnumOptionReadModel("Per month", + PeriodFrequencyType.MONTHS.getValue().longValue()); + EnumOptionReadModel perYear = new EnumOptionReadModel("Per year", + PeriodFrequencyType.YEARS.getValue().longValue()); + List repaymentFrequencyOptions = Arrays.asList( + perWeek, perMonth, perYear); + return repaymentFrequencyOptions; + } + + @Override + public Collection retrieveAllUsers() { + + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + String officeIdsList = generateOfficeIdInClause(offices); + + AppUserMapper mapper = new AppUserMapper(offices); + String sql = "select " + mapper.schema() + " where u.org_id = ? and u.office_id in (" + officeIdsList + ")"; + + return this.jdbcTemplate.query(sql, mapper, new Object[] { currentUser + .getOrganisation().getId() }); + } + + @Override + public AppUserData retrieveNewUserDetails() { + + List offices = retrieveOffices(); + + List availableRoles = new ArrayList(retrieveAllRoles()); + + AppUserData userData = new AppUserData(); + userData.setAllowedOffices(offices); + userData.setAvailableRoles(availableRoles); + + return userData; + } + + @Override + public AppUserData retrieveUser(Long userId) { + + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + + List availableRoles = new ArrayList( + retrieveAllRoles()); + + AppUser user = this.appUserRepository.findOne(usersThatMatch( + currentUser.getOrganisation(), userId)); + + List userRoleData = new ArrayList(); + Set userRoles = user.getRoles(); + for (Role role : userRoles) { + userRoleData.add(role.toData()); + } + + AppUserData userData = new AppUserData(user.getId(), + user.getUsername(), user.getEmail(), user.getOrganisation() + .getId(), user.getOffice().getId(), user.getOffice() + .getName()); + userData.setFirstname(user.getFirstname()); + userData.setLastname(user.getLastname()); + + userData.setAllowedOffices(offices); + + availableRoles.removeAll(userRoleData); + userData.setAvailableRoles(availableRoles); + userData.setSelectedRoles(userRoleData); + + return userData; + } + + @Override + public AppUserData retrieveCurrentUser() { + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + + List availableRoles = new ArrayList(retrieveAllRoles()); + + AppUser user = this.appUserRepository.findOne(usersThatMatch( + currentUser.getOrganisation(), currentUser.getId())); + + List userRoleData = new ArrayList(); + Set userRoles = user.getRoles(); + for (Role role : userRoles) { + userRoleData.add(role.toData()); + } + + AppUserData userData = new AppUserData(user.getId(), + user.getUsername(), user.getEmail(), user.getOrganisation() + .getId(), user.getOffice().getId(), user.getOffice() + .getName()); + userData.setFirstname(user.getFirstname()); + userData.setLastname(user.getLastname()); + + userData.setAllowedOffices(offices); + + availableRoles.removeAll(userRoleData); + userData.setAvailableRoles(availableRoles); + userData.setSelectedRoles(userRoleData); + + return userData; + } + + protected static final class AppUserMapper implements + RowMapper { + + private final List offices; + + public AppUserMapper(final List offices) { + this.offices = offices; + } + + @Override + public AppUserData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + String username = rs.getString("username"); + String firstname = rs.getString("firstname"); + String lastname = rs.getString("lastname"); + String email = rs.getString("email"); + Long orgId = rs.getLong("orgId"); + Long officeId = rs.getLong("officeId"); + + String officeName = fromOfficeList(this.offices, officeId); + + AppUserData user = new AppUserData(id, username, email, orgId, + officeId, officeName); + user.setLastname(lastname); + user.setFirstname(firstname); + + return user; + } + + public String schema() { + return " u.id as id, u.username as username, u.firstname as firstname, u.lastname as lastname, u.email as email, u.org_id as orgId, u.office_id as officeId from admin_appuser u "; + } + + private String fromOfficeList(final List officeList, + final Long officeId) { + String match = ""; + for (OfficeData office : officeList) { + if (office.getId().equals(officeId)) { + match = office.getName(); + } + } + + return match; + } + } + + @Override + public Collection retrieveAllRoles() { + AppUser currentUser = extractAuthenticatedUser(); + + RoleMapper mapper = new RoleMapper(); + String sql = "select " + mapper.schema() + " where r.org_id = ?"; + + return this.jdbcTemplate.query(sql, mapper, new Object[] { currentUser + .getOrganisation().getId() }); + } + + @Override + public RoleData retrieveRole(Long roleId) { + + Role role = this.roleRepository.findOne(roleId); + + return role.toData(); + } + + protected static final class RoleMapper implements RowMapper { + + @Override + public RoleData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + Long orgId = rs.getLong("orgId"); + String name = rs.getString("name"); + String description = rs.getString("description"); + + return new RoleData(id, orgId, name, description); + } + + public String schema() { + return " r.id as id, r.org_id as orgId, r.name as name, r.description as description from admin_role r "; + } + } + + @Override + public Collection retrieveAllPermissionGroups() { + + Collection options = new ArrayList(); + + for (PermissionGroup group : PermissionGroup.values()) { + options.add(new EnumOptionReadModel(group.name(), Integer.valueOf( + group.ordinal()).longValue())); + } + return options; + } + + @Override + public Collection retrieveAllPermissions() { + + AppUser currentUser = extractAuthenticatedUser(); + + PermissionMapper mapper = new PermissionMapper(); + String sql = "select " + mapper.schema() + " where p.org_id = ?"; + + return this.jdbcTemplate.query(sql, mapper, new Object[] { currentUser + .getOrganisation().getId() }); + } + + @Override + public ClientData retrieveNewClientDetails() { + + AppUser currentUser = extractAuthenticatedUser(); + + List offices = retrieveOffices(); + + ClientData clientData = new ClientData(); + clientData.setOfficeId(currentUser.getOffice().getId()); + clientData.setOrganisationId(currentUser.getOrganisation().getId()); + clientData.setAllowedOffices(offices); + + clientData.setDisplayName(""); + clientData.setFirstname(""); + clientData.setLastname(""); + clientData.setId(Long.valueOf(-1)); + clientData.setJoinedDate(new LocalDate()); + + return clientData; + } + + protected static final class PermissionMapper implements + RowMapper { + + @Override + public PermissionData mapRow(final ResultSet rs, final int rowNum) + throws SQLException { + + Long id = rs.getLong("id"); + Long orgId = rs.getLong("orgId"); + String name = rs.getString("name"); + String description = rs.getString("description"); + String code = rs.getString("code"); + int groupType = rs.getInt("groupType"); + + return new PermissionData(id, orgId, name, description, code, + groupType); + } + + public String schema() { + return " p.id as id, p.org_id as orgId, p.default_name as name, p.default_description as description, p.code as code, p.group_enum as groupType from admin_permission p "; + } + } + + @Override + public GenericResultset retrieveGenericResultset(final String rptDB, + final String name, final String type, + final Map queryParams) { + + if (name == null) { + logger.info("Report Name not Found"); + return null; + } + + long startTime = System.currentTimeMillis(); + logger.info("STARTING REPORT: " + name + " Type: " + type); + +// AppUser currentUser = extractAuthenticatedUser(); +// Collection permissions = currentUser.getAuthorities(); + /* + * AppUser currentUser = extractAuthenticatedUser(); Boolean validUser = + * verifyUserDetails(currentUser); + * + * if (!validUser) { return null; } + * + * String orgId = currentUser.getOrganisation().getId().toString(); put + * back in later + */ + String orgId = "1"; + + String sql; + try { + sql = getSQLtoRun(rptDB, name, type, orgId, queryParams); + } catch (SQLException e) { + logger.info(name + ": Failed in getSQLtoRun"); + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST).entity(e.getMessage()).build()); + } + //logger.info(name + ": RUNNING SQL"); + + GenericResultset result = null; + try { + result = fillReportingGenericResultSet(sql); + } catch (SQLException e) { + logger.info("Error - SQL: " + sql); + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST).entity(e.getMessage()).build()); + } + + long elapsed = System.currentTimeMillis() - startTime; + logger.info("FINISHING Report/Request Name: " + name + " - " + type + + " Elapsed Time: " + elapsed); + return result; + } + + @Override + public ExtraDatasets retrieveExtraDatasetNames(String datasetType) { + + if (datasetType == null) { + logger.info("Extra Data Dataset Type Not Found"); + return null; + } + + List names = new ArrayList(); + Connection db_connection; + try { + db_connection = dataSource.getConnection(); + Statement db_statement = db_connection.createStatement(); + String sql = "select d.`name` as datasetName from stretchydata_dataset d join stretchydata_datasettype t on t.id = d.datasettype_id where t.`name` = '" + + datasetType + "' order by d.`name`"; + + //logger.info("extra tables sql: " + sql); + ResultSet rs = db_statement.executeQuery(sql); + + while (rs.next()) { + //logger.info("datasetName: " + rs.getString("datasetName")); + names.add(rs.getString("datasetName")); + } + + db_statement.close(); + db_statement = null; + db_connection.close(); + db_connection = null; + } catch (SQLException e) { + logger.info(": Failed in retrieveExtraDatasetNames"); + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST).entity(e.getMessage()).build()); + } + + return new ExtraDatasets(names); + } + + @Override + public GenericResultset retrieveExtraData(String datasetType, + String datasetName, String datasetPKValue) { + + if (datasetType == null) { + logger.info("Extra Data Table Type not Found"); + return null; + } + if (datasetName == null) { + logger.info("Extra Data Table Name not Found"); + return null; + } + if (datasetPKValue == null) { + logger.info("Extra Data Table ID not Found"); + return null; + } + + long startTime = System.currentTimeMillis(); + logger.info("STARTING EXTRA DATA TABLE: " + datasetName + " ID: " + + datasetPKValue); + + try { + GenericResultset result = fillExtraDataGenericResultSet( + datasetType, datasetName, datasetPKValue); + + long elapsed = System.currentTimeMillis() - startTime; + logger.info("FINISHING EXTRA DATA TABLE: " + datasetName + + " Elapsed Time: " + elapsed); + return result; + } catch (SQLException e) { + logger.info("Error - SQL: " + e.toString()); + throw new InvalidSqlException(e, e.toString()); + } + } + + private GenericResultset fillExtraDataGenericResultSet(String datasetType, + String datasetName, String datasetPKValue) throws SQLException { + + GenericResultset result = new GenericResultset(); + String fullDatasetName = getFullDatasetName(datasetType, datasetName); + Connection db_connection = dataSource.getConnection(); + Statement db_statement1 = db_connection.createStatement(); + Statement db_statement2 = db_connection.createStatement(); + Statement db_statement3 = db_connection.createStatement(); + String sql = "select f.`name`, f.data_type, f.data_length, f.display_type, f.allowed_list_id from stretchydata_datasettype t join stretchydata_dataset d on d.datasettype_id = t.id join stretchydata_dataset_fields f on f.dataset_id = d.id where d.`name` = '" + + datasetName + + "' and t.`name` = '" + + datasetType + + "' order by f.id"; + + //logger.info("specific: " + sql); + ResultSet rsmd = db_statement1.executeQuery(sql); + + List columnHeaders = new ArrayList(); + Boolean firstColumn = true; + Integer allowedListId; + String selectFieldList = ""; + String selectFieldSeparator = ""; + while (rsmd.next()) { + ResultsetColumnHeader rsch = new ResultsetColumnHeader(); + rsch.setColumnName(rsmd.getString("name")); + + if (firstColumn) { + selectFieldSeparator = " "; + firstColumn = false; + } else { + selectFieldSeparator = ", "; + } + selectFieldList += selectFieldSeparator + "`" + rsch.getColumnName() + "`"; + + rsch.setColumnType(rsmd.getString("data_type")); + rsch.setColumnLength(rsmd.getInt("data_length")); + rsch.setColumnDisplayType(rsmd.getString("display_type")); + allowedListId = rsmd.getInt("allowed_list_id"); + if (allowedListId != null) { + sql = "select v.`name` from stretchydata_allowed_value v where allowed_list_id = " + allowedListId + " order by id"; + ResultSet rsValues = db_statement2.executeQuery(sql); + while (rsValues.next()) { + rsch.getColumnValues().add(rsValues.getString("name")); + } + } + columnHeaders.add(rsch); + } + result.setColumnHeaders(columnHeaders); + + sql = "select " + selectFieldList + " from `" + fullDatasetName + + "` where id = " + datasetPKValue; + + ResultSet rs = db_statement3.executeQuery(sql); + String columnName = null; + String columnValue = null; + List resultsetDataRows = new ArrayList(); + ResultsetDataRow resultsetDataRow; + while (rs.next()) { + resultsetDataRow = new ResultsetDataRow(); + List columnValues = new ArrayList(); + + for (int i = 0; i < columnHeaders.size(); i++) { + columnName = columnHeaders.get(i).getColumnName(); + columnValue = rs.getString(columnName); + columnValues.add(columnValue); + } + resultsetDataRow.setRow(columnValues); + resultsetDataRows.add(resultsetDataRow); + } + result.setData(resultsetDataRows); + + db_statement1.close(); + db_statement1 = null; + db_statement2.close(); + db_statement2 = null; + db_statement3.close(); + db_statement3 = null; + db_connection.close(); + db_connection = null; + + return result; + + } + + private String getFullDatasetName(final String datasetType, + final String datasetName) { + return datasetType + "_extra_" + datasetName; + } + + private GenericResultset fillReportingGenericResultSet(final String sql) + throws SQLException { + + GenericResultset result = new GenericResultset(); + + Connection db_connection = dataSource.getConnection(); + Statement db_statement = db_connection.createStatement(); + ResultSet rs = db_statement.executeQuery(sql); + + ResultSetMetaData rsmd = rs.getMetaData(); + String columnName = null; + String columnValue = null; + List columnHeaders = new ArrayList(); + for (int i = 0; i < rsmd.getColumnCount(); i++) { + ResultsetColumnHeader rsch = new ResultsetColumnHeader(); + rsch.setColumnName(rsmd.getColumnName(i + 1)); + rsch.setColumnType(rsmd.getColumnTypeName(i + 1)); + columnHeaders.add(rsch); + } + result.setColumnHeaders(columnHeaders); + + List resultsetDataRows = new ArrayList(); + ResultsetDataRow resultsetDataRow; + while (rs.next()) { + resultsetDataRow = new ResultsetDataRow(); + List columnValues = new ArrayList(); + for (int i = 0; i < rsmd.getColumnCount(); i++) { + columnName = rsmd.getColumnName(i + 1); + columnValue = rs.getString(columnName); + columnValues.add(columnValue); + } + resultsetDataRow.setRow(columnValues); + resultsetDataRows.add(resultsetDataRow); + } + result.setData(resultsetDataRows); + + db_statement.close(); + db_statement = null; + db_connection.close(); + db_connection = null; + + return result; + + } + + private String getSQLtoRun(final String rptDB, final String name, + final String type, final String orgId, + final Map queryParams) throws SQLException { + String sql = null; + + if (type.equals("report")) { + sql = getReportSql(rptDB, name); + } else { + // todo - dont need to check for orgID if special parameter sql (but + // prob need to check restrictions + sql = getParameterSql(rptDB, name); + } + + sql = replace(sql, "${orgId}", orgId); + + Set keys = queryParams.keySet(); + for (String key : keys) { + String pValue = queryParams.get(key); + // logger.info("(" + key + " : " + pValue + ")"); + sql = replace(sql, key, pValue); + } + + // wrap sql to prevent JDBC sql errors and also prevent malicious sql + sql = "select x.* from (" + sql + ") x"; + + return sql; + + } + +// private Boolean verifyUserDetails(AppUser usr) { +// +// // some logs to be taken out after testing +// String idDetails = usr.getId() + ", " + usr.getLastname() + ", " +// + usr.getFirstname(); +// logger.info("Id: " + idDetails + " Organisation: " +// + usr.getOrganisation().getId() + " Office: " +// + usr.getOffice().getId() + " Role Names: " +// + usr.getRoleNames()); +// String otherDetails = "Head Officer User? " + usr.isHeadOfficeUser() +// + " Enabled: " + usr.isEnabled(); +// logger.info(otherDetails); +// if (usr.getAuthorities() != null) { +//// for (GrantedAuthority grantedAuthority : usr.getAuthorities()) { +// // logger.info("Granted Authority: " + +// // grantedAuthority.getAuthority()); +//// } +// } +// logger.info(""); +// +// // some checks +// if (usr.getOrganisation().getId() == null) { +// logger.info("Organisation ID not Found"); +// return false; +// } +// +// return true; +// } + + private String getReportSql(String rptDB, String reportName) + throws SQLException { + String sql = "select report_sql as the_sql from " + rptDB + + ".stretchy_report where report_name = '" + reportName + "'"; + // logger.info("Report SQL: " + sql); + + return getSql(sql); + } + + private String getParameterSql(String rptDB, String parameterName) + throws SQLException { + String sql = "select parameter_sql as the_sql from " + rptDB + + ".stretchy_parameter where parameter_name = '" + + parameterName + "'"; + // logger.info("Parameter SQL: " + sql); + + return getSql(sql); + } + + private String getSql(String inputSql) throws SQLException { + + Connection db_connection = dataSource.getConnection(); + Statement db_statement = db_connection.createStatement(); + ResultSet rs = db_statement.executeQuery(inputSql); + + String sql = null; + + while (rs.next()) { + sql = rs.getString("the_sql"); + } + + db_statement.close(); + db_statement = null; + db_connection.close(); + db_connection = null; + + return sql; + } + + static String replace(String str, String pattern, String replace) { + int s = 0; + int e = 0; + StringBuffer result = new StringBuffer(); + + while ((e = str.indexOf(pattern, s)) >= 0) { + result.append(str.substring(s, e)); + result.append(replace); + s = e + pattern.length(); + } + result.append(str.substring(s)); + return result.toString(); + } + + @Override + public void tempSaveExtraData(String datasetType, String datasetName, + String datasetPKValue, Map queryParams) { + //logger.info("SaveExtraData - DatasetType: " + datasetType + // + " DatasetName: " + datasetName + " datasetPKValue: " + // + datasetPKValue); + + String fullDatasetName = getFullDatasetName(datasetType, datasetName); + String saveSql = getSaveSql(fullDatasetName, datasetPKValue, + queryParams); + try { + Connection db_connection = dataSource.getConnection(); + Statement db_statement = db_connection.createStatement(); + db_statement.executeUpdate(saveSql); + + db_statement.close(); + db_statement = null; + db_connection.close(); + db_connection = null; + } catch (SQLException e) { + logger.info("SQL: " + saveSql + " ERROR: " + e.toString()); + throw new InvalidSqlException(e, e.toString()); + } + + } + + private String getSaveSql(String fullDatasetName, String datasetPKValue, + Map queryParams) { + + String errMsg = ""; + String transType = queryParams.get("ed_transType"); + if (!(transType.equals("E") || transType.equals("A"))) { + errMsg = "transType not E or A - Value is: " + transType; + logger.info(errMsg); + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST).entity(errMsg).build()); + } + + String pValue = ""; + String pValueWrite = ""; + String saveSql = ""; + String singleQuote = "'"; + String underscore = "_"; + String space = " "; + Set keys = queryParams.keySet(); + + if (transType.equals("E")) { + boolean firstColumn = true; + saveSql = "update `" + fullDatasetName + "` "; + + for (String key : keys) { + if (!(key.equals("ed_transType") || key.equals("id"))) { + if (firstColumn) { + saveSql += " set "; + firstColumn = false; + } else { + saveSql += ", "; + } + + pValue = queryParams.get(key); + if (pValue == null || pValue.equals("")) { + pValueWrite = "null"; + } else { + pValueWrite = singleQuote + + replace(pValue, singleQuote, singleQuote + + singleQuote) + singleQuote; + } + saveSql += "`" + replace(key, underscore, space) + "` = " + + pValueWrite; + } + } + + saveSql += " where id = " + datasetPKValue; + } else { + String insertColumns = ""; + String selectColumns = ""; + String columnName = ""; + for (String key : keys) { + pValue = queryParams.get(key); + if (!(key.equals("ed_transType") || key.equals("id"))) { + + pValue = queryParams.get(key); + if (pValue == null || pValue.equals("")) { + pValueWrite = "null"; + } else { + pValueWrite = singleQuote + + replace(pValue, singleQuote, singleQuote + + singleQuote) + singleQuote; + } + columnName = "`" + replace(key, underscore, space) + "`"; + insertColumns += ", " + columnName; + selectColumns += "," + pValueWrite + " as " + columnName; + } + } + + saveSql = "insert into `" + fullDatasetName + "` (id" + + insertColumns + ")" + " select " + datasetPKValue + + " as id" + selectColumns; + } + //logger.info("Save SQL: " + saveSql); + return saveSql; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/RoleValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/RoleValidator.java new file mode 100644 index 000000000..0b9d9cba9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/RoleValidator.java @@ -0,0 +1,66 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.RoleCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.springframework.util.ObjectUtils; + +public class RoleValidator { + + private final RoleCommand command; + + public RoleValidator(RoleCommand command) { + this.command = command; + } + + public void validateForCreate() { + List dataValidationErrors = new ArrayList(); + + validate(dataValidationErrors); + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + + public void validateForUpdate() { + List dataValidationErrors = new ArrayList(); + + if (command.getId() == null || command.getId() < 1) { + ErrorResponse error = new ErrorResponse("validation.msg.role.id.not.provided", "id"); + dataValidationErrors.add(error); + } + + validate(dataValidationErrors); + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + + private void validate(List dataValidationErrors) { + if (StringUtils.isBlank(command.getName())) { + ErrorResponse error = new ErrorResponse("validation.msg.role.name.cannot.be.blank", "name"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getDescription())) { + ErrorResponse error = new ErrorResponse("validation.msg.role.description.cannot.be.blank", "description"); + dataValidationErrors.add(error); + } else { + if (command.getDescription().trim().length() > 500) { + ErrorResponse error = new ErrorResponse("validation.msg.role.description.exceeds.max.length", "description"); + dataValidationErrors.add(error); + } + } + + if (ObjectUtils.isEmpty(command.getPermissionIds())) { + ErrorResponse error = new ErrorResponse("validation.msg.role.permissions.cannot.be.empty", "selectedItems"); + dataValidationErrors.add(error); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/Specifications.java b/mifosng-provider/src/main/java/org/mifosng/platform/Specifications.java new file mode 100644 index 000000000..2e33c4e90 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/Specifications.java @@ -0,0 +1,105 @@ +package org.mifosng.platform; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanProduct; +import org.mifosng.platform.loan.domain.LoanTransaction; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; +import org.mifosng.platform.user.domain.Role; +import org.springframework.data.jpa.domain.Specification; + +public final class Specifications { + + public static Specification usersThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification usersThatMatch(final Organisation organisation, final String username) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("username"), username)); + } + }; + } + + public static Specification rolesThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification loanTransactionsThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification loansThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification loansThatMatch(final Organisation organisation, final Client client) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("client"), client)); + } + }; + } + + public static Specification productThatMatches(final Organisation organisation, final Long id) { + return new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification officesThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } + + public static Specification clientsThatMatch(final Organisation organisation, final Long id) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id)); + } + }; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/SubmitLoanApplicationCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/SubmitLoanApplicationCommandValidator.java new file mode 100644 index 000000000..8550e9971 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/SubmitLoanApplicationCommandValidator.java @@ -0,0 +1,64 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.data.command.SubmitLoanApplicationCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class SubmitLoanApplicationCommandValidator { + + private final SubmitLoanApplicationCommand command; + + public SubmitLoanApplicationCommandValidator(SubmitLoanApplicationCommand command) { + this.command = command; + } + + public void validate() { + List dataValidationErrors = new ArrayList(); + + if (command.getLoanSchedule() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.submit.loan.loan.schedule.cannot.be.blank", "loanSchedule"); + dataValidationErrors.add(error); + } + + if (command.getSubmittedOnDate() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.submitted.on.date.cannot.be.blank", "submittedOnDate"); + dataValidationErrors.add(error); + } else { + if (command.getSubmittedOnDate().isAfter(command.getExpectedDisbursementDate())) { + ErrorResponse error = new ErrorResponse("validation.msg.loan.submitted.on.date.cannot.be.after.expectedDisbursementDate", "submittedOnDate"); + dataValidationErrors.add(error); + } + } + + try { + // reuse calculate loan schedule validator for now + CalculateLoanScheduleCommand calculateLoanScheduleCommand = new CalculateLoanScheduleCommand( + command.getCurrencyCode(), command.getDigitsAfterDecimal(), + command.getPrincipal(), command.getInterestRatePerPeriod(), + command.getInterestRateFrequencyMethod(), + command.getInterestMethod(), command.getRepaymentEvery(), + command.getRepaymentFrequency(), + command.getNumberOfRepayments(), + command.getAmortizationMethod(), + command.isFlexibleRepaymentSchedule(), + command.isInterestRebateAllowed(), + command.getExpectedDisbursementDate(), + command.getRepaymentsStartingFromDate(), + command.getInterestCalculatedFromDate()); + + CalculateLoanScheduleCommandValidator validator = new CalculateLoanScheduleCommandValidator( + calculateLoanScheduleCommand); + validator.validate(); + } catch (NewDataValidationException e) { + dataValidationErrors.addAll(e.getValidationErrors()); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/UserValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/UserValidator.java new file mode 100644 index 000000000..c5e3c8b49 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/UserValidator.java @@ -0,0 +1,71 @@ +package org.mifosng.platform; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.UserCommand; +import org.mifosng.platform.exceptions.NewDataValidationException; + +public class UserValidator { + + private final UserCommand command; + + public UserValidator(UserCommand command) { + this.command = command; + } + + public void validate() { + + List dataValidationErrors = new ArrayList(); + + validateAccountSettings(dataValidationErrors); + + if (command.getOfficeId() == null || command.getOfficeId().equals(Long.valueOf(-1))) { + ErrorResponse error = new ErrorResponse("validation.msg.user.office.cannot.be.blank", "officeId"); + dataValidationErrors.add(error); + } + + if (command.getRoleIds() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.user.roles.cannot.be.blank", "selectedItems"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + + public void validateAccountSettingDetails() { + List dataValidationErrors = new ArrayList(); + + validateAccountSettings(dataValidationErrors); + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + } + + private void validateAccountSettings(List dataValidationErrors) { + if (StringUtils.isBlank(command.getUsername())) { + ErrorResponse error = new ErrorResponse("validation.msg.user.username.cannot.be.blank", "username"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getFirstname())) { + ErrorResponse error = new ErrorResponse("validation.msg.user.firstname.cannot.be.blank", "firstname"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getLastname())) { + ErrorResponse error = new ErrorResponse("validation.msg.user.lastname.cannot.be.blank", "lastname"); + dataValidationErrors.add(error); + } + + if (StringUtils.isBlank(command.getEmail())) { + ErrorResponse error = new ErrorResponse("validation.msg.user.email.cannot.be.blank", "email"); + dataValidationErrors.add(error); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformService.java new file mode 100644 index 000000000..256afb5cc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformService.java @@ -0,0 +1,94 @@ +package org.mifosng.platform; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.command.AdjustLoanTransactionCommand; +import org.mifosng.data.command.ChangePasswordCommand; +import org.mifosng.data.command.CreateLoanProductCommand; +import org.mifosng.data.command.EnrollClientCommand; +import org.mifosng.data.command.LoanStateTransitionCommand; +import org.mifosng.data.command.LoanTransactionCommand; +import org.mifosng.data.command.NoteCommand; +import org.mifosng.data.command.OfficeCommand; +import org.mifosng.data.command.RoleCommand; +import org.mifosng.data.command.SignupCommand; +import org.mifosng.data.command.SubmitLoanApplicationCommand; +import org.mifosng.data.command.UndoLoanApprovalCommand; +import org.mifosng.data.command.UndoLoanDisbursalCommand; +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.data.command.UpdateOrganisationCurrencyCommand; +import org.mifosng.data.command.UpdateUsernamePasswordCommand; +import org.mifosng.data.command.UserCommand; +import org.mifosng.platform.exceptions.InvalidSignupException; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface WritePlatformService { + + Long createOffice(OfficeCommand command); + + Long updateOffice(OfficeCommand command); + + Long createUser(UserCommand command); + + Long updateUser(UserCommand command); + + Long updateCurrentUser(UserCommand command); + + Long updateCurrentUserPassword(ChangePasswordCommand command); + + void deleteUser(Long userId); + + Long createRole(RoleCommand command); + + Long updateRole(RoleCommand command); + + /** + * @throws InvalidSignupException when sign up fails + */ + Long signup(SignupCommand command); + + void updateUsernamePasswordOnFirstTimeLogin(UpdateUsernamePasswordCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_ENROLL_NEW_CLIENT_ROLE')") + Long enrollClient(EnrollClientCommand command); + + void updateOrganisationCurrencies(UpdateOrganisationCurrencyCommand command); + + EntityIdentifier createLoanProduct(CreateLoanProductCommand command); + + EntityIdentifier updateLoanProduct(UpdateLoanProductCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_DELETE_LOAN_THAT_IS_SUBMITTED_AND_NOT_APPROVED')") + EntityIdentifier deleteLoan(Long loanId); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_SUBMIT_NEW_LOAN_APPLICATION_ROLE', 'CAN_SUBMIT_HISTORIC_LOAN_APPLICATION_ROLE')") + EntityIdentifier submitLoanApplication(SubmitLoanApplicationCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_LOAN_ROLE', 'CAN_APPROVE_LOAN_IN_THE_PAST_ROLE')") + EntityIdentifier approveLoanApplication(LoanStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_UNDO_LOAN_APPROVAL_ROLE')") + EntityIdentifier undoLoanApproval(UndoLoanApprovalCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_REJECT_LOAN_ROLE', 'CAN_REJECT_LOAN_IN_THE_PAST_ROLE')") + EntityIdentifier rejectLoan(LoanStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_WITHDRAW_LOAN_ROLE', 'CAN_WITHDRAW_LOAN_IN_THE_PAST_ROLE')") + EntityIdentifier withdrawLoan(LoanStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_DISBURSE_LOAN_ROLE', 'CAN_DISBURSE_LOAN_IN_THE_PAST_ROLE')") + EntityIdentifier disburseLoan(LoanStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_UNDO_LOAN_DISBURSAL_ROLE')") + public EntityIdentifier undloLoanDisbursal(UndoLoanDisbursalCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_ROLE', 'CAN_MAKE_LOAN_REPAYMENT_IN_THE_PAST_ROLE')") + public EntityIdentifier makeLoanRepayment(LoanTransactionCommand command); + + EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command); + + EntityIdentifier waiveLoanAmount(LoanTransactionCommand command); + + EntityIdentifier addClientNote(NoteCommand command); + + EntityIdentifier updateNote(NoteCommand command); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..c718c5033 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/WritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,1142 @@ +package org.mifosng.platform; + +import static org.mifosng.platform.Specifications.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.ScheduledLoanInstallment; +import org.mifosng.data.command.AdjustLoanTransactionCommand; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.data.command.ChangePasswordCommand; +import org.mifosng.data.command.CreateLoanProductCommand; +import org.mifosng.data.command.EnrollClientCommand; +import org.mifosng.data.command.LoanStateTransitionCommand; +import org.mifosng.data.command.LoanTransactionCommand; +import org.mifosng.data.command.NoteCommand; +import org.mifosng.data.command.OfficeCommand; +import org.mifosng.data.command.RoleCommand; +import org.mifosng.data.command.SignupCommand; +import org.mifosng.data.command.SubmitLoanApplicationCommand; +import org.mifosng.data.command.UndoLoanApprovalCommand; +import org.mifosng.data.command.UndoLoanDisbursalCommand; +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.data.command.UpdateOrganisationCurrencyCommand; +import org.mifosng.data.command.UpdateUsernamePasswordCommand; +import org.mifosng.data.command.UserCommand; +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.client.domain.ClientRepository; +import org.mifosng.platform.client.domain.Note; +import org.mifosng.platform.client.domain.NoteRepository; +import org.mifosng.platform.currency.domain.ApplicationCurrency; +import org.mifosng.platform.currency.domain.ApplicationCurrencyRepository; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.InvalidSignupException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.mifosng.platform.exceptions.NoAuthorizationException; +import org.mifosng.platform.infrastructure.BasicPasswordEncodablePlatformUser; +import org.mifosng.platform.infrastructure.PlatformPasswordEncoder; +import org.mifosng.platform.infrastructure.PlatformUser; +import org.mifosng.platform.infrastructure.UsernameAlreadyExistsException; +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.DefaultLoanLifecycleStateMachine; +import org.mifosng.platform.loan.domain.InterestMethod; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanBuilder; +import org.mifosng.platform.loan.domain.LoanLifecycleStateMachine; +import org.mifosng.platform.loan.domain.LoanProduct; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; +import org.mifosng.platform.loan.domain.LoanProductRepository; +import org.mifosng.platform.loan.domain.LoanRepaymentScheduleInstallment; +import org.mifosng.platform.loan.domain.LoanRepository; +import org.mifosng.platform.loan.domain.LoanStatus; +import org.mifosng.platform.loan.domain.LoanStatusRepository; +import org.mifosng.platform.loan.domain.LoanTransaction; +import org.mifosng.platform.loan.domain.LoanTransactionRepository; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.OfficeRepository; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.organisation.domain.OrganisationCurrency; +import org.mifosng.platform.organisation.domain.OrganisationRepository; +import org.mifosng.platform.user.domain.AppUser; +import org.mifosng.platform.user.domain.AppUserRepository; +import org.mifosng.platform.user.domain.Permission; +import org.mifosng.platform.user.domain.PermissionRepository; +import org.mifosng.platform.user.domain.PlatformUserRepository; +import org.mifosng.platform.user.domain.Role; +import org.mifosng.platform.user.domain.RoleRepository; +import org.mifosng.platform.user.domain.UserDomainService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class WritePlatformServiceJpaRepositoryImpl implements + WritePlatformService { + + private final static Logger logger = LoggerFactory + .getLogger(WritePlatformServiceJpaRepositoryImpl.class); + + private final OrganisationRepository organisationRepository; + private final OfficeRepository officeRepository; + private final UserDomainService userDomainService; + private final PlatformUserRepository platformUserRepository; + private final PlatformPasswordEncoder platformPasswordEncoder; + private final ClientRepository clientRepository; + private final ApplicationCurrencyRepository applicationCurrencyRepository; + private final LoanProductRepository loanProductRepository; + private final LoanRepository loanRepository; + private final LoanStatusRepository loanStatusRepository; + private final CalculationPlatformService calculationPlatformService; + private final RoleRepository roleRepository; + private final PermissionRepository permissionRepository; + private final AppUserRepository appUserRepository; + private final NoteRepository noteRepository; + private final LoanTransactionRepository loanTransactionRepository; + + @Autowired + public WritePlatformServiceJpaRepositoryImpl( + final OrganisationRepository organisationRepository, + final OfficeRepository officeRepository, + final UserDomainService userDomainService, + final PlatformUserRepository platformUserRepository, + final PlatformPasswordEncoder platformPasswordEncoder, + final ClientRepository clientRepository, + final NoteRepository noteRepository, + final ApplicationCurrencyRepository applicationCurrencyRepository, + final LoanProductRepository loanProductRepository, + final LoanRepository loanRepository, + final LoanTransactionRepository loanTransactionRepository, + final LoanStatusRepository loanStatusRepository, + final CalculationPlatformService calculationPlatformService, + final AppUserRepository appUserRepository, + final RoleRepository roleRepository, + final PermissionRepository permissionRepository) { + this.organisationRepository = organisationRepository; + this.officeRepository = officeRepository; + this.userDomainService = userDomainService; + this.platformUserRepository = platformUserRepository; + this.platformPasswordEncoder = platformPasswordEncoder; + this.clientRepository = clientRepository; + this.noteRepository = noteRepository; + this.applicationCurrencyRepository = applicationCurrencyRepository; + this.loanProductRepository = loanProductRepository; + this.loanRepository = loanRepository; + this.loanTransactionRepository = loanTransactionRepository; + this.loanStatusRepository = loanStatusRepository; + this.calculationPlatformService = calculationPlatformService; + this.appUserRepository = appUserRepository; + this.roleRepository = roleRepository; + this.permissionRepository = permissionRepository; + } + + @Transactional + @Override + public Long createUser(final UserCommand command) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + UserValidator validator = new UserValidator(command); + validator.validate(); + + Set allRoles = new HashSet(); + for (String roleId : command.getRoleIds()) { + Role role = this.roleRepository.findOne(Long.valueOf(roleId)); + allRoles.add(role); + } + + Office office = this.officeRepository.findOne(command.getOfficeId()); + + AppUser appUser = AppUser.createNew(currentUser.getOrganisation(), office, + allRoles, command.getUsername(), command.getEmail(), + command.getFirstname(), command.getLastname(), + command.getPassword()); + + this.userDomainService.create(appUser); + return appUser.getId(); + } catch (DataIntegrityViolationException e) { + throw new UsernameAlreadyExistsException(e); + } + } + + @Transactional + @Override + public Long updateUser(UserCommand command) { + try { + AppUser currentUser = extractAuthenticatedUser(); + + UserValidator validator = new UserValidator(command); + validator.validate(); + + List dataValidationErrors = new ArrayList(); + if (command.getId() == null) { + ErrorResponse error = new ErrorResponse("validation.msg.user.id.cannot.be.blank", "id"); + dataValidationErrors.add(error); + } + + if (!dataValidationErrors.isEmpty()) { + throw new NewDataValidationException(dataValidationErrors, "Data validation error exist."); + } + + Set allRoles = new HashSet(); + for (String roleId : command.getRoleIds()) { + Role role = this.roleRepository.findOne(Long.valueOf(roleId)); + allRoles.add(role); + } + + Office office = this.officeRepository.findOne(command.getOfficeId()); + + AppUser userToUpdate = this.appUserRepository.findOne(usersThatMatch(currentUser.getOrganisation(), command.getId())); + userToUpdate.update(allRoles, office, command.getUsername(), command.getFirstname(), command.getLastname(), command.getEmail()); + + return userToUpdate.getId(); + } catch (DataIntegrityViolationException e) { + throw new UsernameAlreadyExistsException(e); + } + } + + @Transactional + @Override + public Long updateCurrentUser(UserCommand command) { + AppUser currentUser = extractAuthenticatedUser(); + + UserValidator validator = new UserValidator(command); + validator.validateAccountSettingDetails(); + + AppUser userToUpdate = this.appUserRepository.findOne(currentUser.getId()); + + userToUpdate.update(command.getUsername(), command.getFirstname(), command.getLastname(), command.getEmail()); + + this.appUserRepository.save(userToUpdate); + + return userToUpdate.getId(); + } + + @Transactional + @Override + public Long updateCurrentUserPassword(ChangePasswordCommand command) { + AppUser currentUser = extractAuthenticatedUser(); + + ChangePasswordCommandValidator validator = new ChangePasswordCommandValidator(command); + validator.validate(); + + AppUser userToUpdate = this.appUserRepository.findOne(currentUser.getId()); + + PlatformUser dummyPlatformUser = new BasicPasswordEncodablePlatformUser( + ((AppUser) userToUpdate).getId(), + userToUpdate.getUsername(), command.getPassword()); + + String newPasswordEncoded = this.platformPasswordEncoder + .encode(dummyPlatformUser); + + userToUpdate.updatePasswordOnFirstTimeLogin(newPasswordEncoded); + + return userToUpdate.getId(); + } + + @Transactional + @Override + public void deleteUser(Long userId) { + this.appUserRepository.delete(userId); + } + + @Transactional + @Override + public Long createRole(final RoleCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + RoleValidator validator = new RoleValidator(command); + validator.validateForCreate(); + + List selectedPermissionIds = new ArrayList(); + for (String selectedId : command.getPermissionIds()) { + selectedPermissionIds.add(Long.valueOf(selectedId)); + } + + List selectedPermissions = new ArrayList(); + Collection allPermissions = this.permissionRepository + .findAll(); + for (Permission permission : allPermissions) { + if (selectedPermissionIds.contains(permission.getId())) { + selectedPermissions.add(permission); + } + } + + Role entity = new Role(currentUser.getOrganisation(), command.getName(), command.getDescription(), selectedPermissions); + + this.roleRepository.save(entity); + + return entity.getId(); + } + + @Transactional + @Override + public Long updateRole(RoleCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + RoleValidator validator = new RoleValidator(command); + validator.validateForUpdate(); + + List selectedPermissionIds = new ArrayList(); + for (String selectedId : command.getPermissionIds()) { + selectedPermissionIds.add(Long.valueOf(selectedId)); + } + + List selectedPermissions = new ArrayList(); + Collection allPermissions = this.permissionRepository + .findAll(); + for (Permission permission : allPermissions) { + if (selectedPermissionIds.contains(permission.getId())) { + selectedPermissions.add(permission); + } + } + + Role role = this.roleRepository.findOne(rolesThatMatch(currentUser.getOrganisation(), command.getId())); + role.update(command.getName(), command.getDescription(), selectedPermissions); + + this.roleRepository.save(role); + + return role.getId(); + } + + @Transactional + @Override + public Long createOffice(final OfficeCommand command) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + OfficeValidator validator = new OfficeValidator(command.getName(), command.getParentId(), command.getOpeningDate(), command.getExternalId()); + validator.validate(); + + Office parent = validateUserPriviledgeOnOfficeAndRetrieve(currentUser, command.getParentId()); + + Office office = Office.createNew(currentUser.getOrganisation(), parent, command.getName(), command.getOpeningDate(), command.getExternalId()); + + // pre save to generate id for use in office hierarchy + this.officeRepository.save(office); + + office.generateHierarchy(); + + this.officeRepository.save(office); + + return office.getId(); + } catch (DataIntegrityViolationException dve) { + List dataValidationErrors = handleOfficeDataIntegrityIssues(command, dve); + throw new ApplicationDomainRuleException(dataValidationErrors, "Errors exist."); + } + } + + @Transactional + @Override + public Long updateOffice(final OfficeCommand command) { + + try { + AppUser currentUser = extractAuthenticatedUser(); + + OfficeValidator validator = new OfficeValidator(command.getName(), command.getParentId(), command.getOpeningDate(), command.getExternalId()); + validator.validate(); + + Office office = validateUserPriviledgeOnOfficeAndRetrieve(currentUser, command.getId()); + + office.update(command.getName(), command.getExternalId(), command.getOpeningDate()); + + this.officeRepository.save(office); + + return office.getId(); + } catch (DataIntegrityViolationException dve) { + List dataValidationErrors = handleOfficeDataIntegrityIssues(command, dve); + throw new ApplicationDomainRuleException(dataValidationErrors, "Errors exist."); + } + } + + private List handleOfficeDataIntegrityIssues(final OfficeCommand command, DataIntegrityViolationException dve) { + + List dataValidationErrors = new ArrayList(); + Throwable realCause = dve.getMostSpecificCause(); + if (realCause.getMessage().contains("externalid_org")) { + ErrorResponse error = new ErrorResponse("error.msg.office.duplicate.externalId", "externalId", command.getExternalId()); + dataValidationErrors.add(error); + } else if (realCause.getMessage().contains("name_org")) { + ErrorResponse error = new ErrorResponse("error.msg.office.duplicate.name", "name", command.getName()); + dataValidationErrors.add(error); + } else { + logger.error(dve.getMessage(), dve); + ErrorResponse error = new ErrorResponse("error.msg.office.unknown.data.integretity.issue", "id"); + dataValidationErrors.add(error); + } + return dataValidationErrors; + } + + /* + * used to restrict modifying operations to office that are either the users office or lower (child) in the office hierarchy + */ + private Office validateUserPriviledgeOnOfficeAndRetrieve(AppUser currentUser, Long officeId) { + + Office userOffice = this.officeRepository.findOne(officesThatMatch(currentUser.getOrganisation(), currentUser.getOffice().getId())); + + if (userOffice.doesNotHaveAnOfficeInHierarchyWithId(officeId)) { + ErrorResponse error = new ErrorResponse("error.msg.office.not.authorized", "id", officeId.toString()); + + throw new ApplicationDomainRuleException(Arrays.asList(error), "Errors exist."); + } + + Office officeToReturn = userOffice; + if (!userOffice.identifiedBy(officeId)) { + officeToReturn = this.officeRepository.findOne(officesThatMatch(currentUser.getOrganisation(), officeId)); + } + + return officeToReturn; + } + + @Transactional + @Override + public Long signup(final SignupCommand command) { + + try { + Organisation organisation = new Organisation( + command.getOrganisationName(), command.getOpeningDate(), + command.getContactEmail(), command.getContactName(), + new ArrayList()); + this.organisationRepository.save(organisation); + + String name = command.getOrganisationName() + " Head Office"; + String externalId = null; + Office headOffice = Office.headOffice(organisation, name, command.getOpeningDate(), externalId); + this.officeRepository.save(headOffice); + + this.userDomainService.createDefaultAdminUser(organisation, + headOffice); + return organisation.getId(); + } catch (DataIntegrityViolationException e) { + throw new InvalidSignupException(e); + } + } + + @Transactional + @Override + public EntityIdentifier createLoanProduct(final CreateLoanProductCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanProductValidator validator = new LoanProductValidator(); + validator.validateForCreate(command); + + // assemble LoanProduct from data + InterestMethod interestMethod = InterestMethod + .fromInt(command.getInterestMethod()); + + AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + + PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType + .fromInt(command.getRepaymentFrequency()); + + PeriodFrequencyType interestFrequencyType = PeriodFrequencyType + .fromInt(command.getInterestRateFrequencyMethod()); + + MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + + // apr calculator + BigDecimal annualInterestRate = BigDecimal.ZERO; + switch (interestFrequencyType) { + case DAYS: + break; + case WEEKS: + annualInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(52)); + break; + case MONTHS: + annualInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(12)); + break; + case YEARS: + annualInterestRate = command.getInterestRatePerPeriod().multiply(BigDecimal.valueOf(1)); + break; + case INVALID: + break; + } + + LoanProduct loanproduct = new LoanProduct(currentUser.getOrganisation(), command.getName(), command.getDescription(), + currency, command.getPrincipal(), + command.getInterestRatePerPeriod(), interestFrequencyType, annualInterestRate, interestMethod, + command.getRepaymentEvery(), repaymentFrequencyType, command.getNumberOfRepayments(), amortizationMethod, command.getInArrearsToleranceAmount(), + command.isFlexibleRepaymentSchedule(), command.isInterestRebateAllowed()); + + this.loanProductRepository.save(loanproduct); + + return new EntityIdentifier(loanproduct.getId()); + } + + @Transactional + @Override + public EntityIdentifier updateLoanProduct(UpdateLoanProductCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanProductValidator validator = new LoanProductValidator(); + validator.validateForUpdate(command); + + LoanProduct product = this.loanProductRepository.findOne(productThatMatches(currentUser.getOrganisation(), command.getId())); + product.update(command); + + this.loanProductRepository.save(product); + + return new EntityIdentifier(Long.valueOf(product.getId())); + } + + @Transactional + @Override + public void updateUsernamePasswordOnFirstTimeLogin(final UpdateUsernamePasswordCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + try { + PlatformUser platformUser = ((AppUserRepository) this.platformUserRepository).findOne(usersThatMatch(currentUser.getOrganisation(), command.getOldUsername())); + + PlatformUser dummyPlatformUser = new BasicPasswordEncodablePlatformUser( + ((AppUser) platformUser).getId(), + platformUser.getUsername(), command.getPassword()); + + String encodePassword = this.platformPasswordEncoder.encode(dummyPlatformUser); + + if (command.isUsernameToBeChanged()) { + platformUser.updateUsernamePasswordOnFirstTimeLogin( + command.getUsername(), encodePassword); + } else { + platformUser.updatePasswordOnFirstTimeLogin(encodePassword); + } + + ((AppUserRepository) this.platformUserRepository).save((AppUser) platformUser); + } catch (DataIntegrityViolationException e) { + throw new UsernameAlreadyExistsException(e); + } + } + + @Transactional + @Override + public Long enrollClient(final EnrollClientCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + EnrollClientCommandValidator validator = new EnrollClientCommandValidator(command); + validator.validate(); + + Office clientOffice = this.officeRepository.findOne(officesThatMatch( + currentUser.getOrganisation(), command.getOfficeId())); + + String firstname = command.getFirstname(); + String lastname = command.getLastname(); + if (StringUtils.isNotBlank(command.getFullname())) { + lastname = command.getFullname(); + firstname = null; + } + + Client newClient = Client.newClient(currentUser.getOrganisation(), clientOffice, firstname, lastname, command.getJoiningDate(), command.getExternalId()); + + this.clientRepository.save(newClient); + + return newClient.getId(); + } + + @Transactional + @Override + public void updateOrganisationCurrencies(final UpdateOrganisationCurrencyCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + if (command.getCodes().isEmpty()) { + List dataValidationErrors = Arrays.asList(new ErrorResponse("validation.msg.organisation.allowed.currencies.cannot.be.blank", "selectedItems")); + throw new NewDataValidationException(dataValidationErrors, "Data validation errors exist."); + } + + Set allowedCurrencies = new HashSet(); + + for (String currencyCode : command.getCodes()) { + + ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(currencyCode); + + OrganisationCurrency allowedCurrency = new OrganisationCurrency( + currency.getCode(), currency.getName(), + currency.getDecimalPlaces(), currency.getNameCode(), currency.getDisplaySymbol()); + + allowedCurrencies.add(allowedCurrency); + } + + Organisation org = currentUser.getOrganisation(); + org.setAllowedCurrencies(allowedCurrencies); + this.organisationRepository.save(org); + } + + private AppUser extractAuthenticatedUser() { + AppUser currentUser = null; + SecurityContext context = SecurityContextHolder.getContext(); + if (context != null) { + Authentication auth = context.getAuthentication(); + if (auth != null) { + currentUser = (AppUser) auth.getPrincipal(); + } + } + + if (currentUser == null) { + throw new ClientNotAuthenticatedException(); + } + + return currentUser; + } + + @Transactional + @Override + public EntityIdentifier submitLoanApplication(final SubmitLoanApplicationCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + SubmitLoanApplicationCommandValidator validator = new SubmitLoanApplicationCommandValidator(command); + validator.validate(); + + LocalDate submittedOn = command.getSubmittedOnDate(); + if (this.isBeforeToday(submittedOn) && currentUser.hasNotPermissionForAnyOf("CAN_SUBMIT_HISTORIC_LOAN_APPLICATION_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE")) { + throw new NoAuthorizationException("Cannot add backdated loan."); + } + + LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId()); + Client client = this.clientRepository.findOne(command.getApplicantId()); + + MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + + final BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(command + .getInterestRatePerPeriod().doubleValue()); + final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + + // apr calculator + BigDecimal defaultAnnualNominalInterestRate = BigDecimal.ZERO; + switch (interestPeriodFrequencyType) { + case DAYS: + break; + case WEEKS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod() + .multiply(BigDecimal.valueOf(52)); + break; + case MONTHS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod() + .multiply(BigDecimal.valueOf(12)); + break; + case YEARS: + defaultAnnualNominalInterestRate = command + .getInterestRatePerPeriod().multiply(BigDecimal.valueOf(1)); + break; + case INVALID: + break; + } + + final InterestMethod interestMethod = InterestMethod.DECLINING_BALANCE; + + final Integer repayEvery = command.getRepaymentEvery(); + final PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType + .fromInt(command.getRepaymentFrequency()); + final Integer defaultNumberOfInstallments = command.getNumberOfRepayments(); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + final boolean flexibleRepaymentSchedule = command.isFlexibleRepaymentSchedule(); + final boolean interestRebateAllowed = command.isInterestRebateAllowed(); + + LoanProductRelatedDetail loanRepaymentScheduleDetail = new LoanProductRelatedDetail(currency, + command.getPrincipal(), defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, + repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, command.getInArrearsToleranceAmount(), + flexibleRepaymentSchedule, interestRebateAllowed); + + LoanSchedule loanSchedule = command.getLoanSchedule(); + List loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments(); + + Loan loan = new LoanBuilder() + .with(currentUser.getOrganisation()) + .with(loanProduct).with(client) + .with(loanRepaymentScheduleDetail) + .build(); + + for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) { + + MoneyData readPrincipalDue = scheduledLoanInstallment + .getPrincipalDue(); + + MoneyData readInterestDue = scheduledLoanInstallment + .getInterestDue(); + + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + loan, scheduledLoanInstallment.getInstallmentNumber(), + scheduledLoanInstallment.getPeriodStart(), + scheduledLoanInstallment.getPeriodEnd(), readPrincipalDue.getAmount(), + readInterestDue.getAmount()); + loan.addRepaymentScheduleInstallment(installment); + } + + loan.submitApplication(submittedOn, command.getExpectedDisbursementDate(), command.getRepaymentsStartingFromDate(), command.getInterestCalculatedFromDate(), defaultLoanLifecycleStateMachine()); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getSubmittedOnNote())) { + Note note = Note.loanNote(currentUser.getOrganisation(), loan, command.getSubmittedOnNote()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getId()); + } + + private boolean isBeforeToday(final LocalDate date) { + return date.isBefore(new LocalDate()); + } + + private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { + List allowedLoanStatuses = this.loanStatusRepository.findAll(); + return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses); + } + + @Transactional + @Override + public EntityIdentifier deleteLoan(Long loanId) { + + AppUser currentUser = extractAuthenticatedUser(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), loanId)); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(loanId), "No loan exists with id: " + loanId); + } + + if (loan.isNotSubmittedAndPendingApproval()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.cannot.delete.loan.in.its.present.state", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse), "Loan must be in pending approval state."); + } + + Long clientId = loan.getClient().getId(); + + List relatedNotes = this.noteRepository.findByLoanId(loan.getId()); + this.noteRepository.deleteInBatch(relatedNotes); + + this.loanRepository.delete(loanId); + + return new EntityIdentifier(clientId); + } + + private List loanIdentifierDoesNotExistError(Long loanId) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.loan.with.identifier.exists", "id", loanId); + return Arrays.asList(errorResponse); + } + + @Transactional + @Override + public EntityIdentifier approveLoanApplication(final LoanStateTransitionCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch(currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (this.isBeforeToday(command.getEventDate()) && currentUser.canNotApproveLoanInPast()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission.to.approve.loan.in.past", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse)); + } + + loan.approve(command.getEventDate(), defaultLoanLifecycleStateMachine()); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanNote(currentUser.getOrganisation(), loan, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier undoLoanApproval(final UndoLoanApprovalCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + loan.undoApproval(defaultLoanLifecycleStateMachine()); + this.loanRepository.save(loan); + + Note note = Note.loanNote(currentUser.getOrganisation(), loan, "Undo of approval."); + this.noteRepository.save(note); + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier rejectLoan(final LoanStateTransitionCommand command) { + + AppUser currentUser = (AppUser) SecurityContextHolder.getContext() + .getAuthentication().getPrincipal(); + + LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (this.isBeforeToday(command.getEventDate()) && currentUser.canNotRejectLoanInPast()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission.to.reject.loan.in.past", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse)); + } + + loan.reject(command.getEventDate(), defaultLoanLifecycleStateMachine()); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanNote(currentUser.getOrganisation(), loan, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier withdrawLoan(final LoanStateTransitionCommand command) { + AppUser currentUser = extractAuthenticatedUser(); + + LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (this.isBeforeToday(command.getEventDate()) && currentUser.canNotWithdrawByClientLoanInPast()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission.to.withdraw.loan.in.past", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse)); + } + + loan.withdraw(command.getEventDate(), defaultLoanLifecycleStateMachine()); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanNote(currentUser.getOrganisation(), loan, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier disburseLoan(final LoanStateTransitionCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (this.isBeforeToday(command.getEventDate()) && currentUser.canNotDisburseLoanInPast()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission.to.disburse.loan.in.past", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse)); + } + + LocalDate disbursedOn = command.getEventDate(); + String comment = command.getComment(); + + LocalDate actualDisbursementDate = new LocalDate(disbursedOn); + + if (loan.isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { + + LocalDate repaymentsStartingFromDate = loan + .getExpectedFirstRepaymentOnDate(); + + LocalDate interestCalculatedFromDate = loan.getInterestCalculatedFromDate(); + + Number principalAsDecimal = loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getAmount(); + String currencyCode = loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyCode(); + int currencyDigits = loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrencyDigitsAfterDecimal(); + + Number interestRatePerYear = loan.getLoanRepaymentScheduleDetail() + .getAnnualNominalInterestRate(); + Integer numberOfInstallments = loan + .getLoanRepaymentScheduleDetail().getNumberOfRepayments(); + + Integer repaidEvery = loan.getLoanRepaymentScheduleDetail() + .getRepayEvery(); + Integer selectedRepaymentFrequency = loan + .getLoanRepaymentScheduleDetail() + .getRepaymentPeriodFrequencyType().getValue(); + Integer selectedRepaymentSchedule = loan + .getLoanRepaymentScheduleDetail() + .getAmortizationMethod().getValue(); + boolean flexibleRepaymentSchedule = loan + .isFlexibleRepaymentSchedule(); + + Number interestRatePerPeriod = interestRatePerYear; + Integer interestRateFrequencyMethod = PeriodFrequencyType.YEARS.getValue(); + Integer interestMethod = InterestMethod.DECLINING_BALANCE.getValue(); + boolean interestRebateAllowed = false; + + CalculateLoanScheduleCommand calculateCommand = new CalculateLoanScheduleCommand(currencyCode, currencyDigits, principalAsDecimal, + interestRatePerPeriod, interestRateFrequencyMethod, interestMethod, repaidEvery, selectedRepaymentFrequency, numberOfInstallments, + selectedRepaymentSchedule, flexibleRepaymentSchedule, interestRebateAllowed, actualDisbursementDate, repaymentsStartingFromDate, interestCalculatedFromDate); + + LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(calculateCommand); + + List modifiedLoanRepaymentSchedule = new ArrayList(); + + for (ScheduledLoanInstallment scheduledLoanInstallment : loanSchedule + .getScheduledLoanInstallments()) { + + final MonetaryCurrency monetaryCurrency = new MonetaryCurrency( + scheduledLoanInstallment.getPrincipalDue().getCurrencyCode(), + scheduledLoanInstallment.getPrincipalDue().getCurrencyDigitsAfterDecimal()); + + Money principal = Money.of(monetaryCurrency, + scheduledLoanInstallment.getPrincipalDue().getAmount()); + + Money interest = Money.of(monetaryCurrency, + scheduledLoanInstallment.getInterestDue().getAmount()); + + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment( + loan, scheduledLoanInstallment.getInstallmentNumber(), + scheduledLoanInstallment.getPeriodStart(), + scheduledLoanInstallment.getPeriodEnd(), principal.getAmount(), + interest.getAmount()); + modifiedLoanRepaymentSchedule.add(installment); + } + loan.disburseWithModifiedRepaymentSchedule(disbursedOn, comment, + modifiedLoanRepaymentSchedule, defaultLoanLifecycleStateMachine()); + } else { + loan.disburse(disbursedOn, defaultLoanLifecycleStateMachine()); + } + + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanNote(currentUser.getOrganisation(), loan, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier undloLoanDisbursal( + final UndoLoanDisbursalCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (loan.isActualDisbursedOnDateEarlierOrLaterThanExpected()) { + // recalculate loan schedule using original settings. + } + + loan.undoDisbursal(defaultLoanLifecycleStateMachine()); + + this.loanRepository.save(loan); + + // TODO - this may not be wanted. + Note note = Note.loanNote(currentUser.getOrganisation(), loan, "Undo of disbursal."); + this.noteRepository.save(note); + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier makeLoanRepayment(final LoanTransactionCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanTransactionValidator validator = new LoanTransactionValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch(currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + if (this.isBeforeToday(command.getPaymentDate()) && currentUser.canNotMakeRepaymentOnLoanInPast()) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission.to.make.repayment.on.loan.in.past", "eventDate"); + throw new ApplicationDomainRuleException(Arrays.asList(errorResponse)); + } + + Money repayment = Money.of(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrency(), + command.getPaymentAmount()); + + LoanTransaction loanRepayment = LoanTransaction.repayment(repayment, command.getPaymentDate()); + loan.makeRepayment(loanRepayment, defaultLoanLifecycleStateMachine()); + this.loanTransactionRepository.save(loanRepayment); + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanTransactionNote(currentUser.getOrganisation(), loan, loanRepayment, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + AdjustLoanTransactionCommandValidator validator = new AdjustLoanTransactionCommandValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch( + currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException( + loanIdentifierDoesNotExistError(command.getLoanId()), + "No loan exists with id: " + command.getLoanId()); + } + + LoanTransaction transactionToAdjust = this.loanTransactionRepository + .findOne(command.getRepaymentId()); + + Money transactionAmount = Money.of(loan + .getLoanRepaymentScheduleDetail().getPrincipal() + .getCurrency(), command.getPaymentAmount()); + + // adjustment is only supported for repayments and waivers at present + LoanTransaction newTransactionDetail = LoanTransaction.repayment(transactionAmount, command.getPaymentDate()); + if (transactionToAdjust.isWaiver()) { + newTransactionDetail = LoanTransaction.waiver(transactionAmount, command.getPaymentDate()); + } + + loan.adjustExistingTransaction(transactionToAdjust, newTransactionDetail, defaultLoanLifecycleStateMachine()); + + this.loanTransactionRepository.save(newTransactionDetail); + + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanTransactionNote(currentUser.getOrganisation(), + loan, newTransactionDetail, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier waiveLoanAmount(LoanTransactionCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + LoanTransactionValidator validator = new LoanTransactionValidator(command); + validator.validate(); + + Loan loan = this.loanRepository.findOne(loansThatMatch(currentUser.getOrganisation(), command.getLoanId())); + + if (loan == null) { + throw new ApplicationDomainRuleException(loanIdentifierDoesNotExistError(command.getLoanId()), "No loan exists with id: " + command.getLoanId()); + } + + Money waived = Money.of(loan.getLoanRepaymentScheduleDetail() + .getPrincipal().getCurrency(), + command.getPaymentAmount()); + + LoanTransaction waiver = LoanTransaction.waiver(waived, command.getPaymentDate()); + + loan.waive(waiver, defaultLoanLifecycleStateMachine()); + + this.loanTransactionRepository.save(waiver); + + this.loanRepository.save(loan); + + if (StringUtils.isNotBlank(command.getComment())) { + Note note = Note.loanTransactionNote(currentUser.getOrganisation(), loan, waiver, command.getComment()); + this.noteRepository.save(note); + } + + return new EntityIdentifier(loan.getClient().getId()); + } + + @Transactional + @Override + public EntityIdentifier addClientNote(NoteCommand command) { + + AppUser currentUser = extractAuthenticatedUser(); + + Client clientForUpdate = this.clientRepository.findOne(command.getClientId()); + + Note note = Note.clientNote(currentUser.getOrganisation(), clientForUpdate, command.getNote()); + + this.noteRepository.save(note); + + return new EntityIdentifier(note.getId()); + } + + @Transactional + @Override + public EntityIdentifier updateNote(NoteCommand command) { + + Note noteForUpdate = this.noteRepository.findOne(command.getId()); + + noteForUpdate.update(command.getNote()); + + return new EntityIdentifier(noteForUpdate.getId()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Client.java b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Client.java new file mode 100644 index 000000000..a5eea881f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Client.java @@ -0,0 +1,88 @@ +package org.mifosng.platform.client.domain; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "portfolio_client", uniqueConstraints = @UniqueConstraint(columnNames = {"org_id", "external_id" })) +public class Client extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "office_id", nullable = false) + private final Office office; + + @SuppressWarnings("unused") + @Column(name = "firstname", length=50) + private final String firstName; + + @SuppressWarnings("unused") + @Column(name = "lastname", length=50) + private final String lastName; + + @SuppressWarnings("unused") + @Column(name = "joining_date") + @Temporal(TemporalType.DATE) + private final Date joiningDate; + + @Column(name = "external_id", length=100) + private final String externalId; + + public static Client newClient(Organisation organisation, + Office clientOffice, String firstname, String lastname, + LocalDate joiningDate, String externalId) { + return new Client(organisation, clientOffice, firstname, lastname, + joiningDate, externalId); + } + + public Client() { + this.organisation = null; + this.office = null; + this.joiningDate = null; + this.firstName = null; + this.lastName = null; + this.externalId = null; + } + + public Client(final Organisation organisation, final Office office, final String firstName, + final String lastName, final LocalDate openingDate, final String externalId) { + this.organisation = organisation; + this.office = office; + if (StringUtils.isNotBlank(externalId)) { + this.externalId = externalId.trim(); + } else { + this.externalId = null; + } + this.joiningDate = openingDate.toDateMidnight().toDate(); + if (StringUtils.isNotBlank(firstName)) { + this.firstName = firstName.trim(); + } else { + this.firstName = null; + } + this.lastName = lastName.trim(); + } + + public boolean identifiedBy(final String identifier) { + return identifier.equalsIgnoreCase(this.externalId); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/ClientRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/ClientRepository.java new file mode 100644 index 000000000..32d6f1fb2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/ClientRepository.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.client.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ClientRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Note.java b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Note.java new file mode 100644 index 000000000..af698f3cd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/Note.java @@ -0,0 +1,116 @@ +package org.mifosng.platform.client.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.loan.domain.Loan; +import org.mifosng.platform.loan.domain.LoanTransaction; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "portfolio_note") +public class Note extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private final Client client; + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "loan_id", nullable = true) + private Loan loan; + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "loan_transaction_id", nullable = true) + private LoanTransaction loanTransaction; + + @SuppressWarnings("unused") + @Column(name = "note", length=1000) + private String note; + + @SuppressWarnings("unused") + @Column(name = "note_type_enum") + private Integer noteTypeId; + + public enum NoteType { + CLIENT(100), LOAN(200), LOAN_TRANSACTION(300); + + private int value; + + NoteType(int value) { this.value = value; } + + public int getValue() { return value; } + + public static NoteType parse(int id) { + NoteType right = null; // Default + for (NoteType item : NoteType.values()) { + if (item.getValue()==id) { + right = item; + break; + } + } + return right; + } + } + + public static Note clientNote(Organisation organisation, Client client, String note) { + return new Note(organisation, client, note); + } + + public static Note loanNote(Organisation organisation, Loan loan, String note) { + return new Note(organisation, loan, note); + } + + public static Note loanTransactionNote(Organisation organisation, Loan loan, LoanTransaction loanTransaction, String note) { + return new Note(organisation, loan, loanTransaction, note); + } + + private Note(Organisation organisation, Client client, String note) { + this.organisation = organisation; + this.client = client; + this.note = note; + this.noteTypeId = NoteType.CLIENT.getValue(); + } + + private Note(Organisation organisation, Loan loan, String note) { + this.organisation = organisation; + this.loan = loan; + this.client = loan.getClient(); + this.note = note; + this.noteTypeId = NoteType.LOAN.getValue(); + } + + private Note(Organisation organisation, Loan loan, LoanTransaction loanTransaction, String note) { + this.organisation = organisation; + this.loan = loan; + this.loanTransaction = loanTransaction; + this.client = loan.getClient(); + this.note = note; + this.noteTypeId = NoteType.LOAN_TRANSACTION.getValue(); + } + + protected Note() { + this.organisation = null; + this.client = null; + this.loan = null; + this.loanTransaction = null; + this.note = null; + this.noteTypeId = null; + } + + public void update(final String note) { + this.note = note; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/NoteRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/NoteRepository.java new file mode 100644 index 000000000..17ec072f3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/client/domain/NoteRepository.java @@ -0,0 +1,11 @@ +package org.mifosng.platform.client.domain; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface NoteRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByLoanId(Long id); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrency.java b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrency.java new file mode 100644 index 000000000..9e217fab6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrency.java @@ -0,0 +1,63 @@ +package org.mifosng.platform.currency.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "ref_currency") +public class ApplicationCurrency extends AbstractPersistable { + + @Column(name = "code", nullable = false, length=3) + private final String code; + + @Column(name = "decimal_places", nullable = false) + private final Integer decimalPlaces; + + @Column(name = "name", nullable = false, length=50) + private final String name; + + @Column(name = "internationalized_name_code", nullable = false, length=50) + private final String nameCode; + + @Column(name = "display_symbol", nullable = true, length=10) + private final String displaySymbol; + + protected ApplicationCurrency() { + this.code = null; + this.name = null; + this.decimalPlaces = null; + this.nameCode = null; + this.displaySymbol = null; + } + + public ApplicationCurrency(final String code, final String name, final int decimalPlaces, final String nameCode, final String displaySymbol) { + this.code = code; + this.name = name; + this.decimalPlaces = decimalPlaces; + this.nameCode = nameCode; + this.displaySymbol = displaySymbol; + } + + public String getCode() { + return this.code; + } + + public String getName() { + return this.name; + } + + public Integer getDecimalPlaces() { + return this.decimalPlaces; + } + + public String getNameCode() { + return nameCode; + } + + public String getDisplaySymbol() { + return displaySymbol; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrencyRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrencyRepository.java new file mode 100644 index 000000000..2c481b48c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/ApplicationCurrencyRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.currency.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ApplicationCurrencyRepository extends JpaRepository, JpaSpecificationExecutor { + + ApplicationCurrency findOneByCode(String currencyCode); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/MonetaryCurrency.java b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/MonetaryCurrency.java new file mode 100644 index 000000000..b58ced8e2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/MonetaryCurrency.java @@ -0,0 +1,38 @@ +package org.mifosng.platform.currency.domain; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class MonetaryCurrency implements Serializable { + + @Column(name = "currency_code", length = 3, nullable=false) + private final String code; + + @Column(name = "currency_digits", nullable=false) + private final int digitsAfterDecimal; + + protected MonetaryCurrency() { + this.code = null; + this.digitsAfterDecimal = 0; + } + + public MonetaryCurrency(final String code, final int digitsAfterDecimal) { + this.code = code; + this.digitsAfterDecimal = digitsAfterDecimal; + } + + public MonetaryCurrency copy() { + return new MonetaryCurrency(this.code, this.digitsAfterDecimal); + } + + public String getCode() { + return code; + } + + public int getDigitsAfterDecimal() { + return digitsAfterDecimal; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/Money.java b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/Money.java new file mode 100644 index 000000000..bb3a7cc1f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/currency/domain/Money.java @@ -0,0 +1,240 @@ +package org.mifosng.platform.currency.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Iterator; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class Money implements Comparable, Serializable { + + @Column(name = "currency_code", length=3) + private final String currencyCode; + + @Column(name = "currency_digits") + private final int currencyDigitsAfterDecimal; + + @Column(name = "amount", scale=6, precision=19) + private final BigDecimal amount; + + public static Money total(final Money... monies) { + if (monies.length == 0) { throw new IllegalArgumentException("Money array must not be empty"); } + Money total = monies[0]; + for (int i = 1; i < monies.length; i++) { + total = total.plus(monies[i]); + } + return total; + } + + public static Money total(final Iterable monies) { + Iterator it = monies.iterator(); + if (it.hasNext() == false) { throw new IllegalArgumentException("Money iterator must not be empty"); } + Money total = it.next(); + while (it.hasNext()) { + total = total.plus(it.next()); + } + return total; + } + + public static Money of(MonetaryCurrency currency, BigDecimal newAmount) { + return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), newAmount); + } + + public static Money zero(final MonetaryCurrency currency) { + return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO); + } + + protected Money() { + this.currencyCode = null; + this.currencyDigitsAfterDecimal = 0; + this.amount = null; + } + + private Money(final String currencyCode, final int digitsAfterDecimal, final BigDecimal amount) { + this.currencyCode = currencyCode; + this.currencyDigitsAfterDecimal = digitsAfterDecimal; + BigDecimal amountStripped = amount.stripTrailingZeros(); + this.amount = amountStripped.setScale(this.currencyDigitsAfterDecimal, RoundingMode.HALF_EVEN); + } + + public Money copy() { + return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros()); + } + + public Money plus(final Iterable moniesToAdd) { + BigDecimal total = this.amount; + for (Money moneyProvider : moniesToAdd) { + Money money = this.checkCurrencyEqual(moneyProvider); + total = total.add(money.amount); + } + return Money.of(monetaryCurrency(), total); + } + + public Money plus(final Money moneyToAdd) { + Money toAdd = this.checkCurrencyEqual(moneyToAdd); + return this.plus(toAdd.getAmount()); + } + + public Money plus(final BigDecimal amountToAdd) { + if (amountToAdd.compareTo(BigDecimal.ZERO) == 0) { return this; } + BigDecimal newAmount = this.amount.add(amountToAdd); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money plus(final double amountToAdd) { + if (amountToAdd == 0) { return this; } + BigDecimal newAmount = this.amount.add(BigDecimal.valueOf(amountToAdd)); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money minus(final Money moneyToSubtract) { + Money toSubtract = this.checkCurrencyEqual(moneyToSubtract); + return this.minus(toSubtract.getAmount()); + } + + public Money minus(final BigDecimal amountToSubtract) { + if (amountToSubtract.compareTo(BigDecimal.ZERO) == 0) { return this; } + BigDecimal newAmount = this.amount.subtract(amountToSubtract); + return Money.of(monetaryCurrency(), newAmount); + } + + private Money checkCurrencyEqual(final Money money) { + if (this.isSameCurrency(money) == false) { throw new UnsupportedOperationException("currencies are different."); } + return money; + } + + public boolean isSameCurrency(final Money money) { + return this.currencyCode.equals(money.getCurrencyCode()); + } + + public Money dividedBy(final BigDecimal valueToDivideBy, final RoundingMode roundingMode) { + if (valueToDivideBy.compareTo(BigDecimal.ONE) == 0) { return this; } + BigDecimal newAmount = this.amount.divide(valueToDivideBy, roundingMode); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money dividedBy(final double valueToDivideBy, final RoundingMode roundingMode) { + if (valueToDivideBy == 1) { return this; } + BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money dividedBy(final long valueToDivideBy, final RoundingMode roundingMode) { + if (valueToDivideBy == 1) { return this; } + BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money multipliedBy(final BigDecimal valueToMultiplyBy) { + if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) { return this; } + BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money multipliedBy(final double valueToMultiplyBy) { + if (valueToMultiplyBy == 1) { return this; } + BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy)); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money multipliedBy(final long valueToMultiplyBy) { + if (valueToMultiplyBy == 1) { return this; } + BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy)); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money multiplyRetainScale(final BigDecimal valueToMultiplyBy, final RoundingMode roundingMode) { + if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) { return this; } + BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy); + newAmount = newAmount.setScale(this.currencyDigitsAfterDecimal, roundingMode); + return Money.of(monetaryCurrency(), newAmount); + } + + public Money multiplyRetainScale(final double valueToMultiplyBy, final RoundingMode roundingMode) { + return this.multiplyRetainScale(BigDecimal.valueOf(valueToMultiplyBy), roundingMode); + } + + @Override + public int compareTo(final Money other) { + Money otherMoney = other; + if (this.currencyCode.equals(otherMoney.currencyCode) == false) { + throw new UnsupportedOperationException("currencies arent different"); + } + return this.amount.compareTo(otherMoney.amount); + } + + public boolean isZero() { + return this.isEqualTo(Money.zero(this.getCurrency())); + } + + public boolean isEqualTo(final Money other) { + return this.compareTo(other) == 0; + } + + public boolean isNotEqualTo(final Money other) { + return !isEqualTo(other); + } + + public boolean isGreaterThanOrEqualTo(final Money other) { + return this.isGreaterThan(other) || this.isEqualTo(other); + } + + public boolean isGreaterThan(final Money other) { + return this.compareTo(other) > 0; + } + + public boolean isGreaterThanZero() { + return this.isGreaterThan(Money.zero(this.getCurrency())); + } + + public boolean isLessThan(final Money other) { + return this.compareTo(other) < 0; + } + + public boolean isLessThanZero() { + return this.isLessThan(Money.zero(this.getCurrency())); + } + + public String getCurrencyCode() { + return this.currencyCode; + } + + public int getCurrencyDigitsAfterDecimal() { + return this.currencyDigitsAfterDecimal; + } + + public BigDecimal getAmount() { + return this.amount; + } + + @Override + public String toString() { + return new StringBuilder() + .append(this.currencyCode) + .append(' ') + .append(this.amount.toPlainString()) + .toString(); + } + + public Money negated() { + if (this.isZero()) { + return this; + } + return Money.of(monetaryCurrency(), this.amount.negate()); + } + + public Money abs() { + return this.isLessThanZero() ? this.negated() : this; + } + + public MonetaryCurrency getCurrency() { + return monetaryCurrency(); + } + + private MonetaryCurrency monetaryCurrency() { + return new MonetaryCurrency(this.currencyCode, this.currencyDigitsAfterDecimal); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ApplicationDomainRuleException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ApplicationDomainRuleException.java new file mode 100644 index 000000000..b5bb93255 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ApplicationDomainRuleException.java @@ -0,0 +1,29 @@ +package org.mifosng.platform.exceptions; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.data.ErrorResponse; + +public class ApplicationDomainRuleException extends RuntimeException { + + private final List errors; + + public ApplicationDomainRuleException(final String message) { + super(message); + this.errors = new ArrayList(); + } + + public ApplicationDomainRuleException(List errors, final String message) { + super(message); + this.errors = errors; + } + + public ApplicationDomainRuleException(List errors) { + this.errors = errors; + } + + public List getErrors() { + return errors; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ClientNotAuthenticatedException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ClientNotAuthenticatedException.java new file mode 100644 index 000000000..32305f733 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/ClientNotAuthenticatedException.java @@ -0,0 +1,5 @@ +package org.mifosng.platform.exceptions; + +public class ClientNotAuthenticatedException extends RuntimeException { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidSignupException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidSignupException.java new file mode 100644 index 000000000..13b1bb5d8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidSignupException.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.exceptions; + +public class InvalidSignupException extends RuntimeException { + + public InvalidSignupException(final Throwable e) { + super(e); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NewDataValidationException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NewDataValidationException.java new file mode 100644 index 000000000..608554d78 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NewDataValidationException.java @@ -0,0 +1,25 @@ +package org.mifosng.platform.exceptions; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.data.ErrorResponse; + +public class NewDataValidationException extends RuntimeException { + + private final List validationErrors; + + public NewDataValidationException(final String message) { + super(message); + this.validationErrors = new ArrayList(); + } + + public NewDataValidationException(List errors, final String message) { + super(message); + this.validationErrors = errors; + } + + public List getValidationErrors() { + return validationErrors; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NoAuthorizationException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NoAuthorizationException.java new file mode 100644 index 000000000..d2c73e71f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/NoAuthorizationException.java @@ -0,0 +1,25 @@ +package org.mifosng.platform.exceptions; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.data.ErrorResponse; + +public class NoAuthorizationException extends RuntimeException { + + private final List authorizationErrors; + + public NoAuthorizationException(final String message) { + super(message); + this.authorizationErrors = new ArrayList(); + } + + public NoAuthorizationException(List authorizationErrors, final String message) { + super(message); + this.authorizationErrors = authorizationErrors; + } + + public List getAuthorizationErrors() { + return authorizationErrors; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AbstractAuditableCustom.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AbstractAuditableCustom.java new file mode 100644 index 000000000..1bc0b6d14 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AbstractAuditableCustom.java @@ -0,0 +1,142 @@ +package org.mifosng.platform.infrastructure; + +import java.io.Serializable; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.JoinColumn; +import javax.persistence.MappedSuperclass; +import javax.persistence.OneToOne; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.DateTime; +import org.springframework.data.domain.Auditable; +import org.springframework.data.jpa.domain.AbstractAuditable; +import org.springframework.data.jpa.domain.AbstractPersistable; + +/** + * A custom copy of {@link AbstractAuditable} to override the column names used on database. + * + * Abstract base class for auditable entities. Stores the audition values in + * persistent fields. + * + * @param the auditing type. Typically some kind of user. + * @param the type of the auditing type's identifier + */ +@MappedSuperclass +public abstract class AbstractAuditableCustom extends AbstractPersistable implements Auditable { + + private static final long serialVersionUID = 141481953116476081L; + + @OneToOne + @JoinColumn(name="createdby_id") + private U createdBy; + + @Column(name="created_date") + @Temporal(TemporalType.TIMESTAMP) + private Date createdDate; + + @OneToOne + @JoinColumn(name="lastmodifiedby_id") + private U lastModifiedBy; + + @Column(name="lastmodified_date") + @Temporal(TemporalType.TIMESTAMP) + private Date lastModifiedDate; + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Auditable#getCreatedBy() + */ + @Override + public U getCreatedBy() { + + return this.createdBy; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.domain.Auditable#setCreatedBy(java.lang.Object) + */ + @Override + public void setCreatedBy(final U createdBy) { + + this.createdBy = createdBy; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Auditable#getCreatedDate() + */ + @Override + public DateTime getCreatedDate() { + + return null == this.createdDate ? null : new DateTime(this.createdDate); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.domain.Auditable#setCreatedDate(org.joda.time + * .DateTime) + */ + @Override + public void setCreatedDate(final DateTime createdDate) { + + this.createdDate = null == createdDate ? null : createdDate.toDate(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Auditable#getLastModifiedBy() + */ + @Override + public U getLastModifiedBy() { + + return this.lastModifiedBy; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.domain.Auditable#setLastModifiedBy(java.lang + * .Object) + */ + @Override + public void setLastModifiedBy(final U lastModifiedBy) { + + this.lastModifiedBy = lastModifiedBy; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Auditable#getLastModifiedDate() + */ + @Override + public DateTime getLastModifiedDate() { + + return null == this.lastModifiedDate ? null : new DateTime(this.lastModifiedDate); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.domain.Auditable#setLastModifiedDate(org.joda + * .time.DateTime) + */ + @Override + public void setLastModifiedDate(final DateTime lastModifiedDate) { + + this.lastModifiedDate = null == lastModifiedDate ? null : lastModifiedDate.toDate(); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AuditorAwareImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AuditorAwareImpl.java new file mode 100644 index 000000000..156b5c585 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/AuditorAwareImpl.java @@ -0,0 +1,37 @@ +package org.mifosng.platform.infrastructure; + +import org.mifosng.platform.user.domain.AppUser; +import org.mifosng.platform.user.domain.AppUserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +public class AuditorAwareImpl implements AuditorAware { + + @Autowired + private AppUserRepository userRepository; + + @Override + public AppUser getCurrentAuditor() { + + AppUser currentUser = null; + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext != null) { + Authentication authentication = securityContext.getAuthentication(); + if (authentication != null) { + currentUser = (AppUser) authentication.getPrincipal(); + } else { + currentUser = this.retrieveSuperUser(); + } + } else { + currentUser = this.retrieveSuperUser(); + } + return currentUser; + } + + private AppUser retrieveSuperUser() { + return this.userRepository.findOne(Long.valueOf("1")); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java new file mode 100644 index 000000000..ded4022f0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java @@ -0,0 +1,73 @@ +package org.mifosng.platform.infrastructure; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; + +public class BasicPasswordEncodablePlatformUser implements PlatformUser { + + private final Long id; + private final String username; + private final String password; + + public BasicPasswordEncodablePlatformUser(final Long id, final String username, + final String password) { + this.id = id; + this.username = username; + this.password = password; + } + + public Long getId() { + return this.id; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public boolean isFirstTimeLoginRemaining() { + return false; + } + + @Override + public void updateUsernamePasswordOnFirstTimeLogin(final String newUsername, final String newPasswordEncoded) { + // dummy method + } + + @Override + public void updatePasswordOnFirstTimeLogin(final String newPasswordEncoded) { + // dummy method + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/CustomAuthenticationFailureHandler.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/CustomAuthenticationFailureHandler.java new file mode 100644 index 000000000..9c3092d49 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/CustomAuthenticationFailureHandler.java @@ -0,0 +1,124 @@ +package org.mifosng.platform.infrastructure; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.util.Assert; + +public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { + + protected final Log logger = LogFactory.getLog(getClass()); + + private String defaultFailureUrl; + private boolean forwardToDestination = false; + private boolean allowSessionCreation = true; + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + public CustomAuthenticationFailureHandler() { + } + + /** + * Performs the redirect or forward to the {@code defaultFailureUrl} if set, otherwise returns a 401 error code. + *

+ * If redirecting or forwarding, {@code saveException} will be called to cache the exception for use in + * the target view. + */ + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + + if (defaultFailureUrl == null) { + logger.debug("No failure URL set, sending 401 Unauthorized error"); + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage()); + } else { + saveException(request, exception); + + if (forwardToDestination) { + logger.debug("Forwarding to " + defaultFailureUrl); + + request.getRequestDispatcher(defaultFailureUrl).forward(request, response); + } else { + logger.debug("Redirecting to " + defaultFailureUrl); + + + String oauthToken = request.getParameter("oauth_token"); + request.setAttribute("oauth_token", oauthToken); + String url = defaultFailureUrl + "?oauth_token=" + oauthToken; + redirectStrategy.sendRedirect(request, response, url); + } + } + } + + /** + * Caches the {@code AuthenticationException} for use in view rendering. + *

+ * If {@code forwardToDestination} is set to true, request scope will be used, otherwise it will attempt to store + * the exception in the session. If there is no session and {@code allowSessionCreation} is {@code true} a session + * will be created. Otherwise the exception will not be stored. + */ + protected final void saveException(HttpServletRequest request, AuthenticationException exception) { + if (forwardToDestination) { + request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); + } else { + HttpSession session = request.getSession(false); + + if (session != null || allowSessionCreation) { + request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); + } + } + } + + /** + * The URL which will be used as the failure destination. + * + * @param defaultFailureUrl the failure URL, for example "/loginFailed.jsp". + */ + public void setDefaultFailureUrl(String defaultFailureUrl) { + Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), + "'" + defaultFailureUrl + "' is not a valid redirect URL"); + this.defaultFailureUrl = defaultFailureUrl; + } + + protected boolean isUseForward() { + return forwardToDestination; + } + + /** + * If set to true, performs a forward to the failure destination URL instead of a redirect. Defaults to + * false. + */ + public void setUseForward(boolean forwardToDestination) { + this.forwardToDestination = forwardToDestination; + } + + /** + * Allows overriding of the behaviour when redirecting to a target URL. + */ + public void setRedirectStrategy(RedirectStrategy redirectStrategy) { + this.redirectStrategy = redirectStrategy; + } + + protected RedirectStrategy getRedirectStrategy() { + return redirectStrategy; + } + + protected boolean isAllowSessionCreation() { + return allowSessionCreation; + } + + public void setAllowSessionCreation(boolean allowSessionCreation) { + this.allowSessionCreation = allowSessionCreation; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/DefaultPlatformPasswordEncoder.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/DefaultPlatformPasswordEncoder.java new file mode 100644 index 000000000..fe3a3f781 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/DefaultPlatformPasswordEncoder.java @@ -0,0 +1,26 @@ +package org.mifosng.platform.infrastructure; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.authentication.dao.SaltSource; +import org.springframework.security.authentication.encoding.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service(value = "applicationPasswordEncoder") +@Scope("singleton") +public class DefaultPlatformPasswordEncoder implements PlatformPasswordEncoder { + + private final PasswordEncoder passwordEncoder; + private final SaltSource saltSource; + + @Autowired + public DefaultPlatformPasswordEncoder(final PasswordEncoder passwordEncoder, final SaltSource saltSource) { + this.passwordEncoder = passwordEncoder; + this.saltSource = saltSource; + } + + @Override + public String encode(final PlatformUser appUser) { + return this.passwordEncoder.encodePassword(appUser.getPassword(), this.saltSource.getSalt(appUser)); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/EmailDetail.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/EmailDetail.java new file mode 100644 index 000000000..6646c1eb8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/EmailDetail.java @@ -0,0 +1,32 @@ +package org.mifosng.platform.infrastructure; + +public class EmailDetail { + + private final String organisationName; + private final String username; + private final String contactName; + private final String address; + + public EmailDetail(String organisationName, String contactName, String address, String username) { + this.organisationName = organisationName; + this.contactName = contactName; + this.address = address; + this.username = username; + } + + public String getOrganisationName() { + return this.organisationName; + } + + public String getUsername() { + return this.username; + } + + public String getContactName() { + return this.contactName; + } + + public String getAddress() { + return this.address; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginController.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginController.java new file mode 100644 index 000000000..adb4e3c7d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginController.java @@ -0,0 +1,106 @@ +package org.mifosng.platform.infrastructure; + +import java.security.Principal; + +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.command.UpdateUsernamePasswordCommand; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.user.domain.PasswordMustBeDifferentException; +import org.mifosng.platform.user.domain.UsernameMustBeDifferentException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +@Controller +@SessionAttributes("firstTimeLoginFormBean") +public class FirstTimeLoginController { + + @Autowired + private WritePlatformService signupPlatformService; + + @ModelAttribute("firstTimeLoginFormBean") + @RequestMapping(value = "/firsttimelogin", method = RequestMethod.GET) + public FirstTimeLoginFormBean loadSignupForm(final Principal principal, + final HttpSession httpSession) { + + String successView = "/index"; + String savedRequestView = (String) httpSession + .getAttribute("firsttimeloginview"); + if (StringUtils.isNotBlank(savedRequestView)) { + successView = savedRequestView; + } + + FirstTimeLoginFormBean bean = new FirstTimeLoginFormBean(); + bean.setOldUsername(principal.getName()); + bean.setSuccessView(successView); + + return bean; + } + + @ModelAttribute("firstTimeLoginFormBean") + @RequestMapping(value = "/firsttimelogin", method = RequestMethod.POST) + public ModelAndView handleSignup( + @ModelAttribute("firstTimeLoginFormBean") final FirstTimeLoginFormBean firstTimeLoginFormBean, + final BindingResult result, final SessionStatus status) { + + if (result.hasErrors()) { + return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean", + firstTimeLoginFormBean); + } + + try { + UpdateUsernamePasswordCommand command = new UpdateUsernamePasswordCommand( + firstTimeLoginFormBean.getOldUsername(), + firstTimeLoginFormBean.getUsername(), + firstTimeLoginFormBean.getPassword()); + + this.signupPlatformService + .updateUsernamePasswordOnFirstTimeLogin(command); + + status.setComplete(); + + RedirectView redirectView = new RedirectView( + firstTimeLoginFormBean.getSuccessView()); + + if (!firstTimeLoginFormBean.getSuccessView().startsWith("http")) { + redirectView.setContextRelative(true); + } + + return new ModelAndView(redirectView); + } catch (UsernameAlreadyExistsException e) { + ObjectError error = new ObjectError("firstTimeLoginFormBean", + new String[] { "username.already.exists" }, + new Object[] { firstTimeLoginFormBean.getUsername() }, + "An user with username {0} already exists."); + result.addError(error); + return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean", + firstTimeLoginFormBean); + } catch (UsernameMustBeDifferentException e) { + ObjectError error = new ObjectError("firstTimeLoginFormBean", + new String[] { "new.username.must.be.different" }, + new Object[] {}, + "The new username cannot be the same as existing username."); + result.addError(error); + return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean", + firstTimeLoginFormBean); + } catch (PasswordMustBeDifferentException e) { + ObjectError error = new ObjectError("firstTimeLoginFormBean", + new String[] { "new.password.must.be.different" }, + new Object[] {}, + "The new password cannot be the same as existing password."); + result.addError(error); + return new ModelAndView("firsttimelogin", "firstTimeLoginFormBean", + firstTimeLoginFormBean); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginDetectionAuthenticationSuccessHandler.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginDetectionAuthenticationSuccessHandler.java new file mode 100644 index 000000000..54c4227a0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginDetectionAuthenticationSuccessHandler.java @@ -0,0 +1,96 @@ +package org.mifosng.platform.infrastructure; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth.common.OAuthConsumerParameter; +import org.springframework.security.oauth.provider.OAuthProviderSupport; +import org.springframework.security.oauth.provider.filter.CoreOAuthProviderSupport; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenServices; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; + +public class FirstTimeLoginDetectionAuthenticationSuccessHandler extends + SimpleUrlAuthenticationSuccessHandler { + + private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport(); + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private String firstTimeLoginUrl; + private final OAuthProviderTokenServices tokenServices; + + public FirstTimeLoginDetectionAuthenticationSuccessHandler(final OAuthProviderTokenServices oauthProviderTokenServices) { + this.tokenServices = oauthProviderTokenServices; + } + + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, + final HttpServletResponse response, + final Authentication authentication) throws ServletException, + IOException { + + // hardcoded success url + String authSuccessUrl = "/oauth/confirm_access"; // request.getParameter("successUrl"); + + Map oauthParams = this.providerSupport.parseParameters(request); + String oauthToken = retrieveOAuthToken(oauthParams); + if (StringUtils.isBlank(oauthToken)) { + // throw exception + } + + OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.tokenServices.getToken(oauthToken); + + PlatformUser loggedInUser = (PlatformUser) authentication.getPrincipal(); + + token.setUserAuthentication(new UsernamePasswordAuthenticationToken(loggedInUser, loggedInUser, loggedInUser.getAuthorities())); + + String targetUrl = this.determineTargetUrl(request, response); + + if (loggedInUser.isFirstTimeLoginRemaining()) { +// request.getSession().setAttribute("firsttimeloginview", targetUrl); + request.setAttribute("firsttimeloginview", targetUrl); + targetUrl = this.firstTimeLoginUrl; + } + + clearAuthenticationAttributes(request); + + getRedirectStrategy().sendRedirect(request, response, authSuccessUrl + "?oauth_token=" + oauthToken); + } + + private String retrieveOAuthToken(Map oauthParams) { + return oauthParams.get(OAuthConsumerParameter.oauth_token.toString()); + } + + @Override + protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException { + + String targetUrl = this.determineTargetUrl(request, response); + PlatformUser loggedInUser = (PlatformUser) authentication.getPrincipal(); + if (loggedInUser.isFirstTimeLoginRemaining()) { + targetUrl = this.firstTimeLoginUrl; + } + + if (response.isCommitted()) { + this.logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); + return; + } + + this.redirectStrategy.sendRedirect(request, response, targetUrl); + } + + public String getFirstTimeLoginUrl() { + return this.firstTimeLoginUrl; + } + + public void setFirstTimeLoginUrl(final String firstTimeLoginUrl) { + this.firstTimeLoginUrl = firstTimeLoginUrl; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginFormBean.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginFormBean.java new file mode 100644 index 000000000..ac60e042e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/FirstTimeLoginFormBean.java @@ -0,0 +1,77 @@ +package org.mifosng.platform.infrastructure; + +import org.apache.commons.lang.StringUtils; +import org.springframework.binding.message.MessageBuilder; +import org.springframework.binding.message.MessageContext; +import org.springframework.binding.validation.ValidationContext; + +public class FirstTimeLoginFormBean { + + private String oldUsername; + private String username; + private String password; + private String successView; + + public void validateUpdateUsernamePasswordDetails(final ValidationContext validationContext) { + MessageContext messageContext = validationContext.getMessageContext(); + + if (StringUtils.isBlank(this.username)) { + MessageBuilder builder = new MessageBuilder().error().source("username") + .codes(new String[] { "username.blank" }) + .defaultText("Username cannot be empty.") + .args(new Object[] {}); + + messageContext.addMessage(builder.build()); + } + + if (this.oldUsername.trim().equalsIgnoreCase(this.username.trim())) { + MessageBuilder builder = new MessageBuilder().error().source("username") + .codes(new String[] { "username.same.as.old.username" }) + .defaultText("The new username cannot be the same as the old username.") + .args(new Object[] {}); + + messageContext.addMessage(builder.build()); + } + + if (StringUtils.isBlank(this.password)) { + MessageBuilder builder = new MessageBuilder().error().source("password") + .codes(new String[] { "password.blank" }) + .defaultText("Password cannot be empty.") + .args(new Object[] {}); + + messageContext.addMessage(builder.build()); + } + } + + public String getOldUsername() { + return this.oldUsername; + } + + public void setOldUsername(final String oldUsername) { + this.oldUsername = oldUsername; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(final String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(final String password) { + this.password = password; + } + + public String getSuccessView() { + return this.successView; + } + + public void setSuccessView(final String successView) { + this.successView = successView; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/GmailBackedPlatformEmailService.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/GmailBackedPlatformEmailService.java new file mode 100644 index 000000000..aeec92975 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/GmailBackedPlatformEmailService.java @@ -0,0 +1,45 @@ +package org.mifosng.platform.infrastructure; + +import org.apache.commons.mail.DefaultAuthenticator; +import org.apache.commons.mail.Email; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.SimpleEmail; +import org.springframework.stereotype.Service; + +@Service +public class GmailBackedPlatformEmailService implements PlatformEmailService { + + @Override + public void sendToUserAccount(final EmailDetail emailDetail, final String unencodedPassword) { + Email email = new SimpleEmail(); + + String authuserName = "support@cloudmicrofinance.com"; + + String authuser = "support@cloudmicrofinance.com"; + String authpwd = "support80"; + + // Very Important, Don't use email.setAuthentication() + email.setAuthenticator(new DefaultAuthenticator(authuser, authpwd)); + email.setDebug(false); // true if you want to debug + email.setHostName("smtp.gmail.com"); + try { + email.getMailSession().getProperties().put("mail.smtp.starttls.enable", "true"); + email.setFrom(authuser, authuserName); + + StringBuilder subjectBuilder = new StringBuilder().append("MifosX Prototype Demo: ").append(emailDetail.getContactName()).append(" user account creation."); + + email.setSubject(subjectBuilder.toString()); + + String sendToEmail = emailDetail.getAddress(); + + StringBuilder messageBuilder = new StringBuilder().append("You are receiving this email as your email account: ").append(sendToEmail).append(" has being used to create a user account for an organisation named [").append(emailDetail.getOrganisationName()).append("] on MifosX Prototype Demo.").append("You can login using the following credentials: username: ").append(emailDetail.getUsername()).append(" password: ").append(unencodedPassword); + + email.setMsg(messageBuilder.toString()); + + email.addTo(sendToEmail, emailDetail.getContactName()); + email.send(); + } catch (EmailException e) { + throw new PlatformEmailSendException(e); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PentahoReportingController.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PentahoReportingController.java new file mode 100644 index 000000000..f68e6e560 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PentahoReportingController.java @@ -0,0 +1,167 @@ +package org.mifosng.platform.infrastructure; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +//@Controller +public class PentahoReportingController //implements ServletContextAware +{ + + // private int reportNum = 0; // used for building HTML reports + // private ServletContext myServletContext; + + //private final static Logger logger = LoggerFactory.getLogger(PentahoReportingController.class); + + public PentahoReportingController() { + +// JPW commented out for now ClassicEngineBoot.getInstance().start(); + } + + @RequestMapping(value = "/pentahoreport", method = RequestMethod.GET) + public void handlePentahoReport(final HttpServletRequest request, + HttpServletResponse response) throws IOException { +/* JPW commented out for now + try { + // load report definition + ResourceManager manager = new ResourceManager(); + manager.registerDefaults(); + + // various ways of pointing to the report directory - hardcoded here + // for testing so if you put the test.prpt in a directory + // c:\dev\PentahoTestReports it should find it + + // String reportPath = "file:" + + // this.getServletContext().getRealPath(request.getParameter("pentahoReportName")); + // String reportPath = "file:" + request.getServletPath(); + // String reportPath = + // "http://ec2-46-137-17-252.eu-west-1.compute.amazonaws.com:8080/PentahoReports/" + // + request.getParameter("pentahoReportName") + ".prpt"; + // String reportPath = "file:///C:/dev/mifosbiGithub/bi/reports/standardReports/prpts/" + request.getParameter("pentahoReportName") + ".prpt"; + + String reportPath = "file:///C:/dev/PentahoTestReports/" + + request.getParameter("pentahoReportName") + ".prpt"; + + logger.info("pentahoReportName: " + + request.getParameter("pentahoReportName")); + logger.info("Report path: " + reportPath); + + Resource res = manager.createDirectly(new URL(reportPath), + MasterReport.class); + MasterReport report = (MasterReport) res.getResource(); + ReportParameterValues rptParamValues = report.getParameterValues(); + + String name; + String value; + @SuppressWarnings("unchecked") + Enumeration parms = request.getParameterNames(); + while (parms.hasMoreElements()) { + // Get the name of the request parameter + name = (String) parms.nextElement(); + logger.info("parm name: " + name); + + if (!((name.equals("pentahoReportName")) || (name + .equals("output-type")))) { + // Get the value of the request parameter + value = request.getParameter(name); + rptParamValues.put(name, value); + logger.info("parm name: " + name + " parm value: " + + value); + } + + } + + String outputType = request.getParameter("output-type"); + + if ("PDF".equals(outputType)) { + response.setContentType("application/pdf"); + PdfReportUtil.createPDF(report, response.getOutputStream()); + } else if ("XLS".equals(outputType)) { + response.setContentType("application/vnd.ms-excel"); + try { + ExcelReportUtil.createXLS(report, + response.getOutputStream()); + } catch (ReportProcessingException e) { + e.printStackTrace(); + } + } else if ("HTML".equals(outputType)) { + String reportLoc = "report_" + reportNum++; + + logger.info("request context path: " + request.getContextPath()); + logger.info("request.getServletPath(): " + + request.getServletPath()); + + logger.info("hopoe: " + + myServletContext.getRealPath(request.getContextPath())); + String path = myServletContext + .getRealPath("../ROOT/PentahoHTMLReports/" + reportLoc); + logger.info("The path is: " + path); + + File folder = new File(path); + folder.mkdir(); + try { + HtmlReportUtil.createDirectoryHTML(report, path + + File.separator + "index.html"); + } catch (ReportProcessingException e) { + e.printStackTrace(); + } + + String redirectLink = "/PentahoHTMLReports/" + reportLoc + + "/index.html"; + logger.info(redirectLink); + response.sendRedirect(redirectLink); + } else { + response.setContentType("application/rtf"); + try { + RTFReportUtil.createRTF(report, response.getOutputStream()); + } catch (ReportProcessingException e) { + e.printStackTrace(); + } + } + } catch (ResourceException e) { + e.printStackTrace(); + }*/ + } + + /* jpw comment out + @Override + public void setServletContext(ServletContext servletContext) { + myServletContext = servletContext; + String pentahoHTMLFolderName = myServletContext + .getRealPath("../ROOT/PentahoHTMLReports"); + + if (pentahoHTMLFolderName == null) { + logger.info("pentaho HTML directory doesn't exist for non-Tomcat use"); + } else { + File pentahoHTMLFolder = new File(pentahoHTMLFolderName); + if (!(deleteDir(pentahoHTMLFolder))) + logger.info("Failed to delete folder: " + + pentahoHTMLFolder.getName()); + } + + }*/ + + // Deletes all files and subdirectories under dir. + // Returns true if all deletions were successful. + // If a deletion fails, the method stops attempting to delete and returns + // false. +// private static boolean deleteDir(File dir) { +// if (dir.isDirectory()) { +// String[] children = dir.list(); +// for (int i = 0; i < children.length; i++) { +// boolean success = deleteDir(new File(dir, children[i])); +// if (!success) { +// return false; +// } +// } +// } +// +// // The directory is now empty so delete it +// return dir.delete(); +// } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailSendException.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailSendException.java new file mode 100644 index 000000000..1ccbb7ede --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailSendException.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.infrastructure; + +public class PlatformEmailSendException extends RuntimeException { + + public PlatformEmailSendException(final Throwable e) { + super(e); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailService.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailService.java new file mode 100644 index 000000000..c2130f3ac --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformEmailService.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.infrastructure; + + +public interface PlatformEmailService { + + void sendToUserAccount(EmailDetail emailDetail, String unencodedPassword); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformPasswordEncoder.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformPasswordEncoder.java new file mode 100644 index 000000000..29aacef0a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformPasswordEncoder.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.infrastructure; + + +public interface PlatformPasswordEncoder { + + String encode(PlatformUser appUser); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java new file mode 100644 index 000000000..b9ee5658d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java @@ -0,0 +1,15 @@ +package org.mifosng.platform.infrastructure; + +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Interface to protect platform from implementation detail of spring security. + */ +public interface PlatformUser extends UserDetails { + + boolean isFirstTimeLoginRemaining(); + + void updateUsernamePasswordOnFirstTimeLogin(String newUsername, String newPasswordEncoded); + + void updatePasswordOnFirstTimeLogin(String newPasswordEncoded); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/RandomPasswordGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/RandomPasswordGenerator.java new file mode 100644 index 000000000..3cff8bae5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/RandomPasswordGenerator.java @@ -0,0 +1,19 @@ +package org.mifosng.platform.infrastructure; + +public class RandomPasswordGenerator { + + private final int numberOfCharactersInPassword; + + public RandomPasswordGenerator(final int numberOfCharactersInPassword) { + this.numberOfCharactersInPassword = numberOfCharactersInPassword; + } + + public String generate() { + + StringBuilder passwordBuilder = new StringBuilder(this.numberOfCharactersInPassword); + for (int i = 0; i < this.numberOfCharactersInPassword; i++) { + passwordBuilder.append((char) ((int) (Math.random() * 26) + 97)); + } + return passwordBuilder.toString(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupController.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupController.java new file mode 100644 index 000000000..c83f79186 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupController.java @@ -0,0 +1,130 @@ +package org.mifosng.platform.infrastructure; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.command.SignupCommand; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.InvalidSignupException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.servlet.ModelAndView; + +/** + * @deprecated - remove signup functionality for now. + */ +@Deprecated +@Controller +@SessionAttributes("signupFormBean") +public class SignupController { + + private final WritePlatformService signupPlatformService; + + @Autowired + public SignupController(WritePlatformService signupPlatformService) { + this.signupPlatformService = signupPlatformService; + } + + @RequestMapping(value = "/signup", method = RequestMethod.GET) + public ModelAndView loadSignupForm() { + + SignupFormBean bean = new SignupFormBean(); + return new ModelAndView("/tenant/signup", "signupFormBean", bean); + } + + @RequestMapping(value = "/signup", method = RequestMethod.POST) + public ModelAndView handleSignup( + @ModelAttribute("signupFormBean") final SignupFormBean signupFormBean, + final BindingResult result, final SessionStatus status) { + + final String signupFormView = "/tenant/signup"; + + if (result.hasErrors()) { + return new ModelAndView(signupFormView, "signupFormBean", + signupFormBean); + } + + if (StringUtils.isBlank(signupFormBean.getOrganisationName())) { + ObjectError error = new ObjectError("signupFormBean", + new String[] { "organisationName.blank" }, new Object[] {}, + "Organisation name cannot be empty."); + + result.addError(error); + } + + if (signupFormBean.getOpeningDate() == null) { + ObjectError error = new ObjectError("signupFormBean", + new String[] { "openingDate.blank" }, new Object[] {}, + "Organisation Founded date cannot be empty."); + + result.addError(error); + } + + if (StringUtils.isBlank(signupFormBean.getContactEmail())) { + + ObjectError error = new ObjectError("signupFormBean", + new String[] { "contactEmail.blank" }, new Object[] {}, + "Contact email cannot be empty."); + + result.addError(error); + } + + if (StringUtils.isBlank(signupFormBean.getContactName())) { + ObjectError error = new ObjectError("signupFormBean", + new String[] { "contactName.blank" }, new Object[] {}, + "Contact name cannot be empty."); + + result.addError(error); + } + + if (result.hasErrors()) { + return new ModelAndView(signupFormView, "signupFormBean", + signupFormBean); + } + + SignupCommand command = new SignupCommand( + signupFormBean.getOrganisationName(), + signupFormBean.getContactEmail(), + signupFormBean.getContactName(), + signupFormBean.getOpeningDate()); + + ModelAndView mav = null; + try { + signupPlatformService.signup(command); + + status.setComplete(); + mav = new ModelAndView("/tenant/signupsuccess", "signupFormBean", + signupFormBean); + } catch (InvalidSignupException e) { + // FIXME - InvalidSignupException is really + // TenantAlreadyExistsException as a tenant already exists with + // given organisationName + + Object[] errorArgs = new Object[] { signupFormBean.getOrganisationName() }; + ObjectError error = new ObjectError("signupFormBean", + new String[] { "organisation.name.already.exists" }, errorArgs, + "An organisation with name {0} already exists."); + result.addError(error); + } catch (PlatformEmailSendException e) { + Object[] errorArgs = new Object[] { signupFormBean.getContactEmail() }; + ObjectError error = new ObjectError( + "signupFormBean", + new String[] { "email.failed.to.send"}, + errorArgs, + "The provided email address [{0}] is not valid or our email service is presently not working, if you believe your email to be correct, please try again in a few minutes."); + result.addError(error); + } + + if (result.hasErrors()) { + mav = new ModelAndView(signupFormView, "signupFormBean", + signupFormBean); + } + + return mav; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupFormBean.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupFormBean.java new file mode 100644 index 000000000..a70d505ff --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/SignupFormBean.java @@ -0,0 +1,49 @@ +package org.mifosng.platform.infrastructure; + +import org.joda.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +public class SignupFormBean { + private String contactEmail; + private String contactName; + private String organisationName; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate openingDate = new LocalDate(); + + protected SignupFormBean() { + // no arg constructor + } + + public String getContactEmail() { + return this.contactEmail; + } + + public void setContactEmail(final String contactEmail) { + this.contactEmail = contactEmail; + } + + public String getContactName() { + return this.contactName; + } + + public void setContactName(final String contactName) { + this.contactName = contactName; + } + + public String getOrganisationName() { + return this.organisationName; + } + + public void setOrganisationName(final String organisationName) { + this.organisationName = organisationName; + } + + public LocalDate getOpeningDate() { + return this.openingDate; + } + + public void setOpeningDate(final LocalDate openingDate) { + this.openingDate = openingDate; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/UsernameAlreadyExistsException.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/UsernameAlreadyExistsException.java new file mode 100644 index 000000000..ba48873e8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/UsernameAlreadyExistsException.java @@ -0,0 +1,7 @@ +package org.mifosng.platform.infrastructure; + +public class UsernameAlreadyExistsException extends RuntimeException { + public UsernameAlreadyExistsException(final Throwable e) { + super(e); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AmortizationMethod.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AmortizationMethod.java new file mode 100644 index 000000000..f9058ee60 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AmortizationMethod.java @@ -0,0 +1,36 @@ +package org.mifosng.platform.loan.domain; + +public enum AmortizationMethod { + EQUAL_PRINCIPAL(0), EQUAL_INSTALLMENTS(1), INVALID(2); + + private final Integer value; + + private AmortizationMethod(final Integer value) { + this.value = value; + } + + public Integer getValue() { + return this.value; + } + + public static AmortizationMethod fromInt(final Integer selectedMethod) { + + if (selectedMethod == null) { + return null; + } + + AmortizationMethod repaymentMethod = null; + switch (selectedMethod) { + case 0: + repaymentMethod = AmortizationMethod.EQUAL_PRINCIPAL; + break; + case 1: + repaymentMethod = AmortizationMethod.EQUAL_INSTALLMENTS; + break; + default: + repaymentMethod = AmortizationMethod.INVALID; + break; + } + return repaymentMethod; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentDecliningBalanceInterestRebateCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentDecliningBalanceInterestRebateCalculator.java new file mode 100644 index 000000000..7c4a30d00 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentDecliningBalanceInterestRebateCalculator.java @@ -0,0 +1,130 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DailyEquivalentDecliningBalanceInterestRebateCalculator implements + InterestRebateCalculator { + + private final static Logger logger = LoggerFactory + .getLogger(DailyEquivalentDecliningBalanceInterestRebateCalculator.class); + + @Override + public Money calculate( + final LocalDate actualDisbursementDate, + final LocalDate paidInFullDate, + final Money loanPrincipal, + final BigDecimal interestRatePerAnnum, + final List repaymentScheduleInstallments, + final List loanRepayments) { + + MonetaryCurrency currency = loanPrincipal.getCurrency(); + + Money totalOriginalInterest = getTotalInterestOnLoan(currency, repaymentScheduleInstallments); + + BigDecimal dailyEquivalentPeriodicInterestRate = interestRatePerAnnum + .divide(BigDecimal.valueOf(36500), 9, RoundingMode.HALF_EVEN); + + Money totalRecalculatedPrincipalPaid = Money.zero(loanPrincipal.getCurrency()); + Money totalRecalculatedInterestPaid = Money.zero(loanPrincipal.getCurrency()); + LocalDate lastBalanceDate = actualDisbursementDate; + Money outstandingBalance = loanPrincipal; + for (LoanTransaction loanRepayment : loanRepayments) { + + int daysAtBalance = Days.daysBetween(lastBalanceDate, + loanRepayment.getTransactionDate()).getDays(); + + BigDecimal periodInterestRateForPaymentPeriod = dailyEquivalentPeriodicInterestRate.multiply(BigDecimal.valueOf(daysAtBalance)); + BigDecimal interestDueOnPaymentDateAmount = outstandingBalance.getAmount().multiply(periodInterestRateForPaymentPeriod); + + Money interestDueOnPaymentDate = Money.of(loanPrincipal.getCurrency(),interestDueOnPaymentDateAmount); + Money principalDueOnPaymentDate = loanRepayment.getAmount(currency).minus(interestDueOnPaymentDate); + + totalRecalculatedPrincipalPaid = totalRecalculatedPrincipalPaid.plus(principalDueOnPaymentDate); + totalRecalculatedInterestPaid = totalRecalculatedInterestPaid.plus(interestDueOnPaymentDate); + + logger.warn("Installment: " + lastBalanceDate + " - " + + loanRepayment.getTransactionDate() + " principal: " + + principalDueOnPaymentDate + "interest: " + + interestDueOnPaymentDate + " total: " + + loanRepayment.getAmount()); + + lastBalanceDate = loanRepayment.getTransactionDate(); + outstandingBalance = outstandingBalance + .minus(principalDueOnPaymentDate); + } + + // finally + // add up total paid to date - work out interest due on that + + int daysAtBalance = Days.daysBetween(lastBalanceDate, paidInFullDate) + .getDays(); + + BigDecimal periodInterestRateForPaymentPeriod = dailyEquivalentPeriodicInterestRate + .multiply(BigDecimal.valueOf(daysAtBalance)); + BigDecimal interestDueOnPaymentDateAmount = outstandingBalance + .getAmount().multiply(periodInterestRateForPaymentPeriod); + + Money interestDueOnPaymentDate = Money.of( + loanPrincipal.getCurrency(), + interestDueOnPaymentDateAmount); + + Money principalDueOnPaymentDate = outstandingBalance; + + Money netOutstandingOnPaidInFullDate = principalDueOnPaymentDate + .plus(interestDueOnPaymentDate); + + totalRecalculatedPrincipalPaid = totalRecalculatedPrincipalPaid.plus(principalDueOnPaymentDate); + totalRecalculatedInterestPaid = totalRecalculatedInterestPaid.plus(interestDueOnPaymentDate); + + logger.warn("Installment: " + lastBalanceDate + " - " + paidInFullDate + + " principal: " + principalDueOnPaymentDate + "interest: " + + interestDueOnPaymentDate + " total: " + + netOutstandingOnPaidInFullDate); + + lastBalanceDate = paidInFullDate; + outstandingBalance = outstandingBalance + .minus(principalDueOnPaymentDate); + + Money rebate = Money.zero(loanPrincipal.getCurrency()); + + // Interest can be greater for following reasons: + // 1. calculated interest may be slightly ahead based on daily + // equivalent + // method of calculating interest compared to original method. + + // 2. payments were under expected amount or after expected date + // resulting in larger outstanding balance being used to calculate + // interest + // at each repayment point in time. + if (totalRecalculatedInterestPaid.isLessThan(totalOriginalInterest) + && totalRecalculatedInterestPaid.isGreaterThanZero()) { + rebate = totalOriginalInterest.minus(totalRecalculatedInterestPaid); + } + + return rebate; + } + + private Money getTotalInterestOnLoan( + final MonetaryCurrency currency, + final List repaymentScheduleInstallments) { + + Money cumulativeInterest = Money.zero(currency); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentScheduleInstallments) { + cumulativeInterest = cumulativeInterest.plus(scheduledRepayment + .getInterest(currency)); + } + + return cumulativeInterest; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentFlatInterestRebateCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentFlatInterestRebateCalculator.java new file mode 100644 index 000000000..b339a325d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentFlatInterestRebateCalculator.java @@ -0,0 +1,70 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; + +public class DailyEquivalentFlatInterestRebateCalculator implements + InterestRebateCalculator { + + @Override + public Money calculate( + final LocalDate actualDisbursementDate, + final LocalDate paidInFullDate, + final Money loanPrincipal, + final BigDecimal interestRatePerAnnum, + final List repaymentScheduleInstallments, + final List loanRepayments) { + + MonetaryCurrency currency = loanPrincipal.getCurrency(); + + LocalDate maturityDate = repaymentScheduleInstallments.get( + repaymentScheduleInstallments.size() - 1).getDueDate(); + + int daysEquivalentOfLoanTerm = Days.daysBetween(actualDisbursementDate, + maturityDate).getDays(); + Money totalOriginalInterest = getTotalInterestOnLoan( + currency, + repaymentScheduleInstallments); + + int actualLoanTermInDays = Days.daysBetween(actualDisbursementDate, + paidInFullDate).getDays(); + + BigDecimal divisor = loanPrincipal.getAmount().multiply( + BigDecimal.valueOf(daysEquivalentOfLoanTerm)); + + BigDecimal periodicInterestRate = totalOriginalInterest.getAmount().divide( + divisor, 9, RoundingMode.HALF_EVEN); + + Money totalRecalculatedInterest = loanPrincipal.multipliedBy( + periodicInterestRate).multipliedBy(actualLoanTermInDays); + + Money rebate = Money.zero(loanPrincipal.getCurrency()); + + if (totalRecalculatedInterest.isLessThan(totalOriginalInterest) + && totalRecalculatedInterest.isGreaterThanZero()) { + rebate = totalOriginalInterest.minus(totalRecalculatedInterest); + } + + return rebate; + } + + private Money getTotalInterestOnLoan( + final MonetaryCurrency currency, + final List repaymentScheduleInstallments) { + + Money cumulativeInterest = Money.zero(currency); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentScheduleInstallments) { + cumulativeInterest = cumulativeInterest.plus(scheduledRepayment + .getInterest(currency)); + } + + return cumulativeInterest; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentInterestRebateCalculatorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentInterestRebateCalculatorFactory.java new file mode 100644 index 000000000..780a72950 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DailyEquivalentInterestRebateCalculatorFactory.java @@ -0,0 +1,34 @@ +package org.mifosng.platform.loan.domain; + +public class DailyEquivalentInterestRebateCalculatorFactory implements + InterestRebateCalculatorFactory { + + @Override + public InterestRebateCalculator createCalcualtor( + final InterestMethod loanRepaymentScheduleMethod, final AmortizationMethod amortizationMethod) { + + InterestRebateCalculator interestRebateCalculator = null; + + switch (loanRepaymentScheduleMethod) { + case DECLINING_BALANCE: + switch (amortizationMethod) { + case EQUAL_INSTALLMENTS: + interestRebateCalculator = new DailyEquivalentDecliningBalanceInterestRebateCalculator(); + break; + default: + interestRebateCalculator = new DailyEquivalentDecliningBalanceInterestRebateCalculator(); + break; + } + break; + case FLAT: + interestRebateCalculator = new DailyEquivalentFlatInterestRebateCalculator(); + break; + default: + interestRebateCalculator = new DailyEquivalentFlatInterestRebateCalculator(); + break; + } + + return interestRebateCalculator; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java new file mode 100644 index 000000000..24f77bc10 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DefaultLoanLifecycleStateMachine.java @@ -0,0 +1,113 @@ +package org.mifosng.platform.loan.domain; + +import java.util.List; + +public class DefaultLoanLifecycleStateMachine implements + LoanLifecycleStateMachine { + + private final List allowedLoanStatuses; + + public DefaultLoanLifecycleStateMachine(List allowedLoanStatuses) { + this.allowedLoanStatuses = allowedLoanStatuses; + } + + @Override + public LoanStatus transition(LoanEvent loanEvent, LoanStatus from) { + + LoanStatus newState = null; + + switch (loanEvent) { + case LOAN_CREATED: + if (from == null) { + newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL, + allowedLoanStatuses); + } + break; + case LOAN_REJECTED: + if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) { + newState = stateOf(LoanStatus.REJECTED, allowedLoanStatuses); + } + break; + case LOAN_APPROVED: + if (from.hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL)) { + newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses); + } + break; + case LOAN_WITHDRAWN: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITED_AND_PENDING_APPROVAL, LoanStatus.APPROVED)) { + newState = stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, allowedLoanStatuses); + } + break; + case LOAN_DISBURSED: + if (from.hasStateOf(LoanStatus.APPROVED)) { + newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses); + } + break; + case LOAN_APPROVAL_UNDO: + if (from.hasStateOf(LoanStatus.APPROVED)) { + newState = stateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL, allowedLoanStatuses); + } + break; + case LOAN_DISBURSAL_UNDO: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses); + } + break; + case LOAN_REPAYMENT: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses); + } else { + newState = from; + } + break; + case REPAID_IN_FULL: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); + } + break; + case LOAN_WRITE_OFF: + if (this.anyOfAllowedWhenComingFrom(from,LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); + } + break; + case LOAN_RESCHEDULE: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { + newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); + } + break; + case INTERST_REBATE_OWED: + if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED)) { + newState = stateOf(LoanStatus.CLOSED, allowedLoanStatuses); + } + break; + } + + return newState; + } + + private LoanStatus stateOf(Integer state, List allowedLoanStatuses) { + LoanStatus match = null; + for (LoanStatus loanStatus : allowedLoanStatuses) { + if (loanStatus.hasStateOf(state)) { + match = loanStatus; + break; + } + } + return match; + } + + private boolean anyOfAllowedWhenComingFrom(final LoanStatus state, + final Integer... allowedStates) { + boolean allowed = false; + + for (Integer allowedState : allowedStates) { + if (state.hasStateOf(allowedState)) { + allowed = true; + break; + } + } + + return allowed; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessor.java new file mode 100644 index 000000000..6fa31ea43 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessor.java @@ -0,0 +1,489 @@ +package org.mifosng.platform.loan.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.DerivedLoanData; +import org.mifosng.data.LoanAccountSummaryData; +import org.mifosng.data.LoanRepaymentData; +import org.mifosng.data.LoanRepaymentDataComparator; +import org.mifosng.data.LoanRepaymentPeriodData; +import org.mifosng.data.LoanRepaymentScheduleData; +import org.mifosng.data.MoneyData; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; + +// FIXME - The derived details of the loan schedule are now updated on the fly when someone does a transaction against a loan. So this is not fully needed. +// - we still use this to figure out how much of a given transactions amount was principal versus interest but might not need this and we should probably +// - derive and update that on the database on the fly +public class DerivedLoanDataProcessor { + + public DerivedLoanData process(List repaymentSchedulePeriods, List loanTransactions, + CurrencyData currencyDetail, Money arrearsTolerance) { + + // 1. derive repayment schedule + LoanRepaymentScheduleData repaymentScheduleDetails = generateRepaymentScheduleData(repaymentSchedulePeriods, currencyDetail); + List loanRepaymentPeriods = repaymentScheduleDetails.getPeriods(); + + // break up payments based on principal and interest + List loanRepayments = new ArrayList(); + int repaymentScheduleIndex = 0; + for (LoanTransaction transaction : loanTransactions) { + if (transaction.isRepayment() || transaction.isWaiver()) { + + MonetaryCurrency currency = new MonetaryCurrency(currencyDetail.getCode(), currencyDetail.getDecimalPlaces()); + + if (transaction.getAmount(currency).isGreaterThanZero()) { + LoanRepaymentPeriodData scheduledRepaymentPeriod = loanRepaymentPeriods + .get(repaymentScheduleIndex); + + if (transaction.isRepayment()) { + LoanRepaymentData repaymentData = processLoanRepayment( + transaction, scheduledRepaymentPeriod, + repaymentScheduleIndex, loanRepaymentPeriods, + arrearsTolerance, currencyDetail); + loanRepayments.add(repaymentData); + } + + if (transaction.isWaiver()) { + LoanRepaymentData repaymentData = processWaiver( + transaction, scheduledRepaymentPeriod, + repaymentScheduleIndex, loanRepaymentPeriods, + arrearsTolerance, currencyDetail); + loanRepayments.add(repaymentData); + } + + if (scheduledRepaymentPeriod.isFullyPaid()) { + repaymentScheduleIndex++; + } + } + } + } + + loanRepaymentPeriods = calculateArrears(loanRepaymentPeriods, arrearsTolerance, currencyDetail); + + LoanRepaymentScheduleData processRepaymentScheduleData = new LoanRepaymentScheduleData(loanRepaymentPeriods); + LoanAccountSummaryData summaryData = calculateTotalsFromRepaymentScheduleData(loanRepaymentPeriods, currencyDetail); + + Collections.sort(loanRepayments, new LoanRepaymentDataComparator()); + + return new DerivedLoanData(processRepaymentScheduleData, summaryData, loanRepayments); + } + + private LoanRepaymentData processWaiver(LoanTransaction transaction, + LoanRepaymentPeriodData scheduledRepaymentPeriod, + int repaymentScheduleIndex, + List loanRepaymentPeriods, + Money arrearsTolerance, CurrencyData currencyData) { + + MonetaryCurrency currency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces()); + Money totalToWaive = transaction.getAmount(currency); + + Money interestToWaive = moneyFrom(scheduledRepaymentPeriod.getInterestOutstanding()); + Money principalToWaive = moneyFrom(scheduledRepaymentPeriod.getPrincipalOutstanding()); + + Money interestWaived = Money.zero(currency); + + if (totalToWaive.isGreaterThanOrEqualTo(interestToWaive)) { + totalToWaive = totalToWaive.minus(interestToWaive); + interestWaived = interestToWaive; + } else { + interestWaived = totalToWaive; + totalToWaive = Money.zero(currency); + } + + Money principalWaived = Money.zero(currency); + if (totalToWaive.isGreaterThanOrEqualTo(principalToWaive)) { + totalToWaive = totalToWaive.minus(principalToWaive); + principalWaived = principalToWaive; + } else { + principalWaived = totalToWaive; + totalToWaive = Money.zero(currency); + } + + MoneyData zero = moneyDataFrom(currencyData, Money.zero(currency)); + + Money totalWaivedInPeriodToDate = Money.zero(currency); + if (scheduledRepaymentPeriod.getTotalWaived() != null) { + totalWaivedInPeriodToDate = moneyFrom(scheduledRepaymentPeriod.getTotalWaived()); + } + Money totalWaivedInTransaction = interestWaived.plus(principalWaived); + totalWaivedInPeriodToDate = totalWaivedInPeriodToDate.plus(totalWaivedInTransaction); + scheduledRepaymentPeriod.setTotalWaived(moneyDataFrom(currencyData, totalWaivedInPeriodToDate)); + + MoneyData totalWaived = moneyDataFrom(currencyData, totalWaivedInTransaction); + + Money principalOutstanding = moneyFrom(scheduledRepaymentPeriod.getPrincipalOutstanding()); + Money remainingPrincipalOutstanding = principalOutstanding.minus(principalWaived); + scheduledRepaymentPeriod.setPrincipalOutstanding(moneyDataFrom(currencyData, remainingPrincipalOutstanding)); + + Money interestOutstanding = moneyFrom(scheduledRepaymentPeriod.getInterestOutstanding()); + Money remainingInterestOutstanding = interestOutstanding.minus(interestWaived); + scheduledRepaymentPeriod.setInterestOutstanding(moneyDataFrom(currencyData, remainingInterestOutstanding)); + + Money totalOutstanding = moneyFrom(scheduledRepaymentPeriod.getTotalOutstanding()); + Money remainingOutstanding = totalOutstanding.minus(moneyFrom(totalWaived)); + scheduledRepaymentPeriod.setTotalOutstanding(moneyDataFrom(currencyData, remainingOutstanding)); + + LoanRepaymentData loanRepaymentData = new LoanRepaymentData(transaction.getId(), transaction.getTransactionDate(), moneyDataFrom(currencyData, principalWaived), + moneyDataFrom(currencyData, interestWaived), totalWaived, zero); + loanRepaymentData.setTotalWaived(totalWaived); + return loanRepaymentData; + } + + private List calculateArrears( + List loanRepaymentPeriods, + Money arrearsTolerance, CurrencyData currencyData) { + + LocalDate today = new LocalDate(); + int repaymentIndex = 0; + for (LoanRepaymentPeriodData currentRepaymentSchedule : loanRepaymentPeriods) { + if (currentRepaymentSchedule.isInArrearsWithToleranceOf( + moneyDataFrom(currencyData, arrearsTolerance), today)) { + currentRepaymentSchedule + .setTotalArrears(currentRepaymentSchedule + .getTotalOutstanding()); + + LocalDate arrearsFrom = determineInArrearsFrom(currentRepaymentSchedule, loanRepaymentPeriods, repaymentIndex); + + LocalDate arrearsTo = determineInArrearsTo(currentRepaymentSchedule, loanRepaymentPeriods, repaymentIndex); + + currentRepaymentSchedule.setArrearsFrom(arrearsFrom); + currentRepaymentSchedule.setArrearsTo(arrearsTo); + } + + repaymentIndex++; + } + + return loanRepaymentPeriods; + } + + private LocalDate determineInArrearsTo( + LoanRepaymentPeriodData currentRepaymentSchedule, + List loanRepaymentPeriods, + int repaymentIndex) { + + LocalDate today = new LocalDate(); + LocalDate arrearsDate = null; + + if (loanRepaymentPeriods.size() - 1 > repaymentIndex) { + LoanRepaymentPeriodData nextRepaymentSchedule = loanRepaymentPeriods + .get(repaymentIndex); + + arrearsDate = nextRepaymentSchedule.getDate().minusDays(1); + } + + if (loanRepaymentPeriods.size() - 1 == repaymentIndex) { + arrearsDate = today; + } + + return arrearsDate; + } + + private LocalDate determineInArrearsFrom( + LoanRepaymentPeriodData currentRepaymentSchedule, + List loanRepaymentPeriods, + int repaymentIndex) { + + + int previousScheduleIndex = repaymentIndex - 1; + LocalDate arrearsDate = null; + if (currentRepaymentSchedule.getLastAffectingPaymentOn() != null) { + + if (currentRepaymentSchedule.getLastAffectingPaymentOn().isAfter( + currentRepaymentSchedule.getDate())) { + + if (loanRepaymentPeriods.size() - 1 > previousScheduleIndex && previousScheduleIndex > -1) { + LoanRepaymentPeriodData lastRepaymentSchedule = loanRepaymentPeriods + .get(previousScheduleIndex); + arrearsDate = lastRepaymentSchedule.getDate().plusDays(1); + } else { + arrearsDate = currentRepaymentSchedule.getDate().minusMonths(1); + } + + } else { + arrearsDate = currentRepaymentSchedule.getLastAffectingPaymentOn(); + } + } else { + if (loanRepaymentPeriods.size() - 1 > previousScheduleIndex && previousScheduleIndex > -1) { + LoanRepaymentPeriodData lastRepaymentSchedule = loanRepaymentPeriods + .get(previousScheduleIndex); + arrearsDate = lastRepaymentSchedule.getDate().plusDays(1); + } else { + arrearsDate = currentRepaymentSchedule.getDate().minusMonths(1); + } + } + + return arrearsDate; + } + + private LoanAccountSummaryData calculateTotalsFromRepaymentScheduleData( + List loanRepaymentPeriods, CurrencyData currencyData) { + + MonetaryCurrency monetaryCurrency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces()); + + Money principalExpected = Money.zero(monetaryCurrency); + Money principalPaid = Money.zero(monetaryCurrency); + Money principalOutstanding = Money.zero(monetaryCurrency); + + Money interestExpected = Money.zero(monetaryCurrency); + Money interestPaid = Money.zero(monetaryCurrency); + Money interestOutstanding = Money.zero(monetaryCurrency); + + Money totalExpected = Money.zero(monetaryCurrency); + Money totalPaid = Money.zero(monetaryCurrency); + Money totalOutstanding = Money.zero(monetaryCurrency); + + Money totalInArrears = Money.zero(monetaryCurrency); + Money totalWaived = Money.zero(monetaryCurrency); + + + for (LoanRepaymentPeriodData period : loanRepaymentPeriods) { + + principalExpected = principalExpected.plus(moneyFrom(period.getPrincipal())); + principalPaid = principalPaid.plus(moneyFrom(period.getPrincipalPaid())); + principalOutstanding = principalOutstanding.plus(moneyFrom(period.getPrincipalOutstanding())); + + interestExpected = interestExpected.plus(moneyFrom(period.getInterest())); + interestPaid = interestPaid.plus(moneyFrom(period.getInterestPaid())); + interestOutstanding = interestOutstanding.plus(moneyFrom(period.getInterestOutstanding())); + + totalExpected = totalExpected.plus(moneyFrom(period.getTotal())); + totalPaid = totalPaid.plus(moneyFrom(period.getTotalPaid())); + totalOutstanding = totalOutstanding.plus(moneyFrom(period.getTotalOutstanding())); + + if (period.getTotalWaived() != null) { + totalWaived = totalWaived.plus(moneyFrom(period.getTotalWaived())); + } + + if (period.getTotalArrears() != null) { + totalInArrears = totalInArrears.plus(moneyFrom(period.getTotalArrears())); + } + } + + return new LoanAccountSummaryData(moneyDataFrom(currencyData, principalExpected), moneyDataFrom(currencyData, principalPaid), + moneyDataFrom(currencyData, principalOutstanding), moneyDataFrom(currencyData, interestExpected), + moneyDataFrom(currencyData, interestPaid), moneyDataFrom(currencyData, interestOutstanding), + moneyDataFrom(currencyData, totalExpected), moneyDataFrom(currencyData, totalPaid), moneyDataFrom(currencyData, totalOutstanding), + moneyDataFrom(currencyData, totalInArrears), moneyDataFrom(currencyData, totalWaived)); + } + + /** + * TODO - pays off interest first then principal so should make this configurable when generating 'derived view of loan account' + */ + private LoanRepaymentData processLoanRepayment(LoanTransaction repayment, + LoanRepaymentPeriodData scheduledRepayment, + int repaymentScheduleIndex, + List repaymentSchedulePeriods, + Money arrearsTolerance, CurrencyData currencyDetail) { + + MoneyData zero = MoneyData.zero(currencyDetail); + MoneyData cumulativeInterestPaid = MoneyData.zero(currencyDetail); + MoneyData cumulativePrincipalPaid = MoneyData.zero(currencyDetail); + + LoanRepaymentPeriodData currentRepaymentSchedule = scheduledRepayment; + int currentRepaymentScheduleIndex = repaymentScheduleIndex; + MoneyData overpaid = null; + + MonetaryCurrency currency = new MonetaryCurrency(currencyDetail.getCode(), currencyDetail.getDecimalPlaces()); + + Money remaining = repayment.getAmount(currency); + while (remaining.isGreaterThanZero()) { + + Money interestOutstanding = moneyFrom(currentRepaymentSchedule.getInterestOutstanding()); + Money principalOutstanding = moneyFrom(currentRepaymentSchedule.getPrincipalOutstanding()); + + // pay off interest component + if (remaining.isGreaterThanOrEqualTo(interestOutstanding)) { + Money previousInterestPaid = moneyFrom(cumulativeInterestPaid); + cumulativeInterestPaid = moneyDataFrom(currencyDetail, previousInterestPaid.plus(interestOutstanding)); + + MoneyData interestPaid = moneyDataFrom(currencyDetail, interestOutstanding); + + Money paidToDate = moneyFrom(currentRepaymentSchedule.getInterestPaid()); + Money amountPaid = moneyFrom(interestPaid); + + paidToDate = paidToDate.plus(amountPaid); + + MoneyData totalInterest = moneyDataFrom(currencyDetail, paidToDate); + currentRepaymentSchedule.setInterestPaid(totalInterest); + + currentRepaymentSchedule.setInterestOutstanding(zero); + + Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid()); + totalPaidToDate = totalPaidToDate.plus(interestOutstanding); + + currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate)); + + Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid())); + currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding)); + + if (interestOutstanding.isGreaterThanZero()) { + currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate()); + } + + remaining = remaining.minus(interestOutstanding); + } else { + // partial payment of principal + Money previousInterestPaid = moneyFrom(cumulativeInterestPaid); + cumulativeInterestPaid = moneyDataFrom(currencyDetail, previousInterestPaid.plus(remaining)); + + MoneyData interestPaid = moneyDataFrom(currencyDetail, remaining); + + Money paidToDate = moneyFrom(currentRepaymentSchedule.getInterestPaid()); + Money amountPaid = moneyFrom(interestPaid); + + paidToDate = paidToDate.plus(amountPaid); + + MoneyData totalInterest = moneyDataFrom(currencyDetail, paidToDate); + currentRepaymentSchedule.setInterestPaid(totalInterest); + + interestOutstanding = interestOutstanding.minus(amountPaid); + + currentRepaymentSchedule.setInterestOutstanding(moneyDataFrom(currencyDetail, interestOutstanding)); + + Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid()); + totalPaidToDate = totalPaidToDate.plus(amountPaid); + + currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate)); + + Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid())); + currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding)); + + if (interestPaid.isGreaterThanZero()) { + currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate()); + } + + remaining = remaining.minus(amountPaid); + } + + // pay off principal component + if (remaining.isGreaterThanOrEqualTo(principalOutstanding)) { + Money previousPrincipalPaid = moneyFrom(cumulativePrincipalPaid); + cumulativePrincipalPaid = moneyDataFrom(currencyDetail, previousPrincipalPaid.plus(principalOutstanding)); + + MoneyData principalPaid = moneyDataFrom(currencyDetail, principalOutstanding); + + Money paidToDate = moneyFrom(currentRepaymentSchedule.getPrincipalPaid()); + Money amountPaid = moneyFrom(principalPaid); + + paidToDate = paidToDate.plus(amountPaid); + + MoneyData totalPrincipal = moneyDataFrom(currencyDetail, paidToDate); + currentRepaymentSchedule.setPrincipalPaid(totalPrincipal); + currentRepaymentSchedule.setPrincipalOutstanding(zero); + + Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid()); + totalPaidToDate = totalPaidToDate.plus(principalOutstanding); + + currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate)); + + Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid())); + currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding)); + + remaining = remaining.minus(principalOutstanding); + } else { + // partial payment of principal + Money previousPrincipalPaid = moneyFrom(cumulativePrincipalPaid); + cumulativePrincipalPaid = moneyDataFrom(currencyDetail, previousPrincipalPaid.plus(remaining)); + + MoneyData principalPaid = moneyDataFrom(currencyDetail, remaining); + + Money paidToDate = moneyFrom(currentRepaymentSchedule.getPrincipalPaid()); + Money amountPaid = moneyFrom(principalPaid); + + paidToDate = paidToDate.plus(amountPaid); + + MoneyData totalPrincipal = moneyDataFrom(currencyDetail, paidToDate); + currentRepaymentSchedule.setPrincipalPaid(totalPrincipal); + + principalOutstanding = principalOutstanding.minus(amountPaid); + currentRepaymentSchedule.setPrincipalOutstanding(moneyDataFrom(currencyDetail, principalOutstanding)); + + Money totalPaidToDate = moneyFrom(currentRepaymentSchedule.getTotalPaid()); + totalPaidToDate = totalPaidToDate.plus(amountPaid); + + currentRepaymentSchedule.setTotalPaid(moneyDataFrom(currencyDetail, totalPaidToDate)); + Money outstanding = moneyFrom(currentRepaymentSchedule.getTotal()).minus(moneyFrom(currentRepaymentSchedule.getTotalPaid())); + currentRepaymentSchedule.setTotalOutstanding(moneyDataFrom(currencyDetail, outstanding)); + + if (principalPaid.isGreaterThanZero()) { + currentRepaymentSchedule.setLastAffectingPaymentOn(repayment.getTransactionDate()); + } + + remaining = remaining.minus(amountPaid); + } + + // mark schedule as paid in full + if (currentRepaymentSchedule.isFullyPaid()) { + currentRepaymentSchedule.setPaidInFullOn(repayment.getTransactionDate()); + } + + // check if this installment has + if (currentRepaymentSchedule.isInArrearsWithToleranceOf(moneyDataFrom(currencyDetail, arrearsTolerance), repayment.getTransactionDate())) { + currentRepaymentSchedule.setTotalArrears(currentRepaymentSchedule.getTotalOutstanding()); + } else { + currentRepaymentSchedule.setTotalArrears(MoneyData.zero(currencyDetail)); + } + + if (remaining.isGreaterThanZero()) { + currentRepaymentScheduleIndex++; + if (repaymentSchedulePeriods.size() > currentRepaymentScheduleIndex) { + currentRepaymentSchedule = repaymentSchedulePeriods.get(currentRepaymentScheduleIndex); + } else { + // loan has been overpaid by an amount so + overpaid = moneyDataFrom(currencyDetail, remaining); + // note - maybe should set the overpayment 'amount' against repayment period. + remaining = Money.zero(remaining.getCurrency()); + } + } + } + + Money totalInterestComponent = moneyFrom(cumulativeInterestPaid); + Money totalPrincipalComponent= moneyFrom(cumulativePrincipalPaid); + + MoneyData totalPaid = moneyDataFrom(currencyDetail, totalPrincipalComponent.plus(totalInterestComponent)); + + LoanRepaymentData loanRepaymentData = new LoanRepaymentData(repayment.getId(), repayment.getTransactionDate(), cumulativePrincipalPaid, cumulativeInterestPaid, totalPaid, overpaid); + return loanRepaymentData; + } + + private LoanRepaymentScheduleData generateRepaymentScheduleData( + List repaymentSchedulePeriods, + CurrencyData currencyData) { + + MoneyData zero = MoneyData.zero(currencyData); + + MonetaryCurrency currency = new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces()); + + List loanScheduleDetails = new ArrayList(); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedulePeriods) { + + MoneyData principal = MoneyData.of(currencyData, scheduledRepayment.getPrincipal(currency).getAmount()); + MoneyData interest = MoneyData.of(currencyData, scheduledRepayment.getInterest(currency).getAmount()); + MoneyData total = MoneyData.of(currencyData, scheduledRepayment.getTotal(currency).getAmount()); + + LoanRepaymentPeriodData periodData = new LoanRepaymentPeriodData( + scheduledRepayment.getInstallmentNumber(), + scheduledRepayment.getDueDate(), zero, principal, interest, + total, total); + + loanScheduleDetails.add(periodData); + } + + return new LoanRepaymentScheduleData(loanScheduleDetails); + } + + private Money moneyFrom(MoneyData money) { + MonetaryCurrency monetaryCurrency = new MonetaryCurrency(money.getCurrencyCode(), money.getCurrencyDigitsAfterDecimal()); + return Money.of(monetaryCurrency, money.getAmount()); + } + + private MoneyData moneyDataFrom(CurrencyData currencyData, Money money) { + return MoneyData.of(currencyData, money.getAmount()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestMethod.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestMethod.java new file mode 100644 index 000000000..4ac0a0725 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestMethod.java @@ -0,0 +1,32 @@ +package org.mifosng.platform.loan.domain; + +public enum InterestMethod { + DECLINING_BALANCE(0), FLAT(1), INVALID(2); + + private final Integer value; + + private InterestMethod(final Integer value) { + this.value = value; + } + + public Integer getValue() { + return this.value; + } + + public static InterestMethod fromInt(final Integer selectedMethod) { + + InterestMethod repaymentMethod = null; + switch (selectedMethod) { + case 0: + repaymentMethod = InterestMethod.DECLINING_BALANCE; + break; + case 1: + repaymentMethod = InterestMethod.FLAT; + break; + default: + repaymentMethod = InterestMethod.INVALID; + break; + } + return repaymentMethod; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculator.java new file mode 100644 index 000000000..38bda1c3a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculator.java @@ -0,0 +1,19 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.Money; + +public interface InterestRebateCalculator { + + Money calculate( + LocalDate actualDisbursementDate, + LocalDate paidInFullDate, + Money loanPrincipal, + BigDecimal interestRatePerAnnum, + List repaymentScheduleInstallments, + List loanRepayments); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculatorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculatorFactory.java new file mode 100644 index 000000000..c0d7a15f6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InterestRebateCalculatorFactory.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.loan.domain; + +public interface InterestRebateCalculatorFactory { + + InterestRebateCalculator createCalcualtor( + InterestMethod loanRepaymentScheduleMethod, + AmortizationMethod amortizationMethod); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InvalidLoanTimelineDate.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InvalidLoanTimelineDate.java new file mode 100644 index 000000000..6caca5886 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/InvalidLoanTimelineDate.java @@ -0,0 +1,15 @@ +package org.mifosng.platform.loan.domain; + +public class InvalidLoanTimelineDate extends RuntimeException { + + private final String errorCode; + + public InvalidLoanTimelineDate(final String message, final String errorCode) { + super(message); + this.errorCode = errorCode; + } + + public String getErrorCode() { + return this.errorCode; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java new file mode 100644 index 000000000..a67b2af0f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -0,0 +1,947 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; +import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.DerivedLoanData; +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "portfolio_loan", uniqueConstraints = @UniqueConstraint(columnNames = {"org_id", "external_id" })) +public class Loan extends AbstractAuditableCustom { + + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private final Client client; + + @ManyToOne + @JoinColumn(name = "product_id") + private final LoanProduct loanProduct; + + @Column(name = "external_id") + private String externalId; + + @Embedded + private final LoanProductRelatedDetail loanRepaymentScheduleDetail; + + @ManyToOne + @JoinColumn(name = "loan_status_id", nullable = false) + private LoanStatus loanStatus; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "submittedon_date") + private Date submittedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "rejectedon_date") + private Date rejectedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "withdrawnon_date") + private Date withdrawnOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "approvedon_date") + private Date approvedOnDate; + + @Temporal(TemporalType.DATE) + @Column(name = "expected_disbursedon_date") + private Date expectedDisbursedOnDate; + + @Temporal(TemporalType.DATE) + @Column(name = "expected_firstrepaymenton_date") + private Date expectedFirstRepaymentOnDate; + + @Temporal(TemporalType.DATE) + @Column(name = "interest_calculated_from_date") + private Date interestCalculatedFromDate; + + @Temporal(TemporalType.DATE) + @Column(name = "disbursedon_date") + private Date disbursedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "closedon_date") + private Date closedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "writtenoffon_date") + private Date writtenOffOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "rescheduledon_date") + private Date rescheduledOnDate; + + @Temporal(TemporalType.DATE) + @Column(name = "expected_maturedon_date") + private Date expectedMaturityDate; + + @Temporal(TemporalType.DATE) + @Column(name = "maturedon_date") + private Date maturedOnDate; + + // see + // http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) + private final List repaymentScheduleInstallments = new ArrayList(); + + // see + // http://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags + @OrderBy(value = "dateOf, id") + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) + private final List loanTransactions = new ArrayList(); + + @Column(name = "interest_rebate_amount", scale = 6, precision = 19) + private BigDecimal interestRebateOwed; + + @Transient + private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory(); + + public Loan() { + this.organisation = null; + this.client = null; + this.loanProduct = null; + this.loanRepaymentScheduleDetail = null; + } + + public Loan(final Organisation organisation, final Client client, + final LoanProduct loanProduct, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, + final LoanStatus loanStatus) { + this.organisation = organisation; + this.client = client; + this.loanProduct = loanProduct; + this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; + this.loanStatus = loanStatus; + this.interestRebateOwed = BigDecimal.ZERO; + } + + public Organisation getOrganisation() { + return this.organisation; + } + + public Client getClient() { + return this.client; + } + + public LoanProduct getLoanProduct() { + return this.loanProduct; + } + + public LoanProductRelatedDetail getLoanRepaymentScheduleDetail() { + return this.loanRepaymentScheduleDetail; + } + + public void submitApplication(final LocalDate submittedOn, final LocalDate expectedDisbursementDate, + LocalDate repaymentsStartingFromDate, LocalDate interestCalculatedFromDate, LoanLifecycleStateMachine lifecycleStateMachine) { + + this.loanStatus = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this.loanStatus); + + this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate(); + + this.expectedMaturityDate = this.repaymentScheduleInstallments + .get(this.repaymentScheduleInstallments.size() - 1) + .getDueDate().toDateMidnight().toDate(); + if (expectedDisbursementDate != null) { + // can be null during bulk upload of loans + this.expectedDisbursedOnDate = expectedDisbursementDate.toDateMidnight().toDate(); + } + + if (repaymentsStartingFromDate != null) { + this.expectedFirstRepaymentOnDate = repaymentsStartingFromDate.toDateMidnight().toDate(); + } + + if (interestCalculatedFromDate != null) { + this.interestCalculatedFromDate = interestCalculatedFromDate.toDateMidnight().toDate(); + } + + if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The loan application submit date cannot be in the future.", + "invalid.submit.date.as.date.is.in.future"); + } + if (new LocalDate(this.submittedOnDate).isAfter(new LocalDate( + this.expectedDisbursedOnDate))) { + throw new InvalidLoanTimelineDate( + "The loan application submit date cannot be after its expected disbursement date.", + "invalid.submit.date.as.date.is.after.expected.disbursement.date"); + } + } + + public void reject(final LocalDate rejectedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) { + + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, this.loanStatus); + + this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); + this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); + if (new LocalDate(this.rejectedOnDate).isBefore(new LocalDate( + this.submittedOnDate))) { + throw new InvalidLoanTimelineDate( + "The loan rejection date cannot be before its submittal date.", + "invalid.rejection.date.as.date.is.before.submittal.date"); + } + if (new LocalDate(this.rejectedOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The loan application rejection date cannot be in the future.", + "invalid.rejection.date.as.date.is.in.future"); + } + } + + public void withdraw(final LocalDate withdrawnOn, LoanLifecycleStateMachine loanLifecycleStateMachine) { + + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, this.loanStatus); + + this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); + this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); + if (new LocalDate(this.withdrawnOnDate).isBefore(new LocalDate( + this.submittedOnDate))) { + throw new InvalidLoanTimelineDate( + "The date of when loan is withdrawn cannot be before its submittal date.", + "invalid.withdrawal.date.as.date.is.before.submittal.date"); + } + if (new LocalDate(this.withdrawnOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The loan application withdraw date cannot be in the future.", + "invalid.withdrawal.date.as.date.is.in.future"); + } + } + + public void approve(final LocalDate approvedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, this.loanStatus); + this.approvedOnDate = approvedOn.toDateTimeAtCurrentTime().toDate(); + if (new LocalDate(this.approvedOnDate).isBefore(new LocalDate( + this.submittedOnDate))) { + throw new InvalidLoanTimelineDate( + "The date of when loan is approved cannot be before its submittal date.", + "invalid.approval.date.as.date.is.before.submittal.date"); + } + if (new LocalDate(this.approvedOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The date of when loan is approved cannot be in the future.", + "invalid.approval.date.as.date.is.in.future"); + } + } + + public void undoApproval(LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, this.loanStatus); + this.approvedOnDate = null; + } + + public void disburseWithModifiedRepaymentSchedule( + final LocalDate disbursedOn, + final String comment, + final List modifiedLoanRepaymentSchedule, LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.repaymentScheduleInstallments.clear(); + for (LoanRepaymentScheduleInstallment modifiedInstallment : modifiedLoanRepaymentSchedule) { + modifiedInstallment.updateOrgnaisation(this.organisation); + modifiedInstallment.updateLoan(this); + this.repaymentScheduleInstallments.add(modifiedInstallment); + } + disburse(disbursedOn, loanLifecycleStateMachine); + } + + public void disburse(final LocalDate disbursedOn, LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this.loanStatus); + this.disbursedOnDate = disbursedOn.toDateTimeAtCurrentTime().toDate(); + this.expectedMaturityDate = this.repaymentScheduleInstallments + .get(this.repaymentScheduleInstallments.size() - 1) + .getDueDate().toDateMidnight().toDate(); + + LoanTransaction loanTransaction = LoanTransaction.disbursement( + this.loanRepaymentScheduleDetail.getPrincipal(), disbursedOn); + loanTransaction.updateLoan(this); + loanTransaction.updateOrganisation(organisation); + this.loanTransactions.add(loanTransaction); + + if (disbursedOn.isBefore(new LocalDate(this.approvedOnDate))) { + throw new InvalidLoanTimelineDate( + "The date of when loan is disbursed cannot be before its approval date.", + "invalid.disbursal.as.disbursement.date.is.before.approved.date"); + } + if (disbursedOn.isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The date of when loan is disbursed cannot be in the future.", + "invalid.disbursal.as.disbursement.date.is.in.future"); + } + LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments + .get(0).getDueDate(); + if (disbursedOn.isAfter(firstRepaymentDueDate)) { + throw new InvalidLoanTimelineDate( + "The date of when loan is disbursed cannot be after the first expected repayment.", + "invalid.disbursal.as.disbursement.date.is.after.first.repayment.due.date"); + } + } + + public void undoDisbursal(LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, this.loanStatus); + this.loanTransactions.clear(); + this.disbursedOnDate = null; + } + + public void waive(LoanTransaction loanTransaction, LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus); + loanTransaction.updateLoan(this); + loanTransaction.updateOrganisation(organisation); + this.loanTransactions.add(loanTransaction); + + if (loanTransaction.getTransactionDate().isBefore( + this.getDisbursementDate())) { + throw new InvalidLoanTimelineDate( + "The repayment date cannot be before the loan disbursement date.", + "invalid.repayment.date.as.date.is.before.disbursement.date"); + } + if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The repayment date cannot be in the future.", + "invalid.repayment.date.as.date.is.in.future"); + } + + if (this.isRepaidInFull()) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus); + this.closedOnDate = loanTransaction.getTransactionDate().toDate(); + this.maturedOnDate = loanTransaction.getTransactionDate().toDate(); + + if (isInterestRebateAllowed()) { + Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction + .getTransactionDate()); + if (rebateDue.isGreaterThanZero()) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus); + this.interestRebateOwed = rebateDue.getAmount(); + } + } + } + } + + public void makeRepayment(final LoanTransaction loanTransaction, LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, this.loanStatus); + loanTransaction.updateLoan(this); + loanTransaction.updateOrganisation(organisation); + this.loanTransactions.add(loanTransaction); + + deriveLoanRepaymentScheduleCompletedData(); + + if (loanTransaction.isNotRepayment()) { + throw new IllegalArgumentException( + "Only repayment transactions can be passed to makeRepayment."); + } + + if (loanTransaction.getTransactionDate().isBefore( + this.getDisbursementDate())) { + throw new InvalidLoanTimelineDate( + "The repayment date cannot be before the loan disbursement date.", + "invalid.repayment.date.as.date.is.before.disbursement.date"); + } + if (loanTransaction.getTransactionDate().isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The repayment date cannot be in the future.", + "invalid.repayment.date.as.date.is.in.future"); + } + if (this.isRepaidInFull()) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this.loanStatus); + this.closedOnDate = loanTransaction.getTransactionDate().toDate(); + this.maturedOnDate = loanTransaction.getTransactionDate().toDate(); + + if (isInterestRebateAllowed()) { + Money rebateDue = calculateRebateWhenPaidInFullOn(loanTransaction + .getTransactionDate()); + if (rebateDue.isGreaterThanZero()) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.INTERST_REBATE_OWED, this.loanStatus); + this.interestRebateOwed = rebateDue.getAmount(); + } + } + } + } + + private void deriveLoanRepaymentScheduleCompletedData() { + + Money totalRepaidOrWaivedAgainstLoan = calculateTotalPaidOrWaived(); + + Money remainingToPayoffAgainstLoanSchedule = totalRepaidOrWaivedAgainstLoan; + Money totalOverpaid = Money.zero(totalRepaidOrWaivedAgainstLoan.getCurrency()); + int repaymentInstallmentIndex = 0; + while (remainingToPayoffAgainstLoanSchedule.isGreaterThanZero()) { + + if (repaymentInstallmentIndex == this.repaymentScheduleInstallments.size()) { + totalOverpaid = remainingToPayoffAgainstLoanSchedule; + + // to exit while loop + remainingToPayoffAgainstLoanSchedule = remainingToPayoffAgainstLoanSchedule.minus(totalOverpaid); + } else { + LoanRepaymentScheduleInstallment scheduledRepaymentInstallment = this.repaymentScheduleInstallments.get(repaymentInstallmentIndex); + remainingToPayoffAgainstLoanSchedule = scheduledRepaymentInstallment.updateDerivedComponents(remainingToPayoffAgainstLoanSchedule); + repaymentInstallmentIndex++; + } + } + } + + private Money calculateTotalPaidOrWaived() { + + Money totalRepaidOrWaived = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency()); + + for (LoanTransaction transaction : this.loanTransactions) { + if (transaction.isRepayment() || transaction.isWaiver()) { + totalRepaidOrWaived = totalRepaidOrWaived.plus(transaction.getAmount()); + } + } + + return totalRepaidOrWaived; + } + + public LocalDate possibleNextRepaymentDate() { + LocalDate earliestUnpaidInstallmentDate = new LocalDate(); + for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + if (installment.unpaid()) { + earliestUnpaidInstallmentDate = installment.getDueDate(); + break; + } + } + + LocalDate lastTransactionDate = null; + for (LoanTransaction transaction : this.loanTransactions) { + if (transaction.isRepayment() && transaction.isNonZero()) { + lastTransactionDate = transaction.getTransactionDate(); + } + } + + LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate; + if (lastTransactionDate != null && lastTransactionDate.isAfter(earliestUnpaidInstallmentDate)) { + possibleNextRepaymentDate = lastTransactionDate; + } + + return possibleNextRepaymentDate; + } + + public Money possibleNextRepaymentAmount() { + MonetaryCurrency currency = this.loanRepaymentScheduleDetail.getPrincipal().getCurrency(); + Money possibleNextRepaymentAmount = Money.zero(currency); + + for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + if (installment.unpaid()) { + possibleNextRepaymentAmount = installment.getTotalDue(currency); + break; + } + } + + return possibleNextRepaymentAmount; + } + + public void adjustExistingTransaction(LoanTransaction transactionForAdjustment, + LoanTransaction newTransactionDetail, LoanLifecycleStateMachine loanLifecycleStateMachine) { + + if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) { + throw new IllegalArgumentException("Only repayment and waiver transactions can be adjusted."); + } + + transactionForAdjustment.contra(); + if (newTransactionDetail.isRepayment()) { + makeRepayment(newTransactionDetail, loanLifecycleStateMachine); + } + + if (newTransactionDetail.isWaiver()) { + waive(newTransactionDetail, loanLifecycleStateMachine); + } + } + + private boolean isRepaidInFull() { + + Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + Money cumulativeTotal = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + Money cumulativeWaived = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { + cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment.getPrincipal(loanCurrency())); + cumulativeInterest = cumulativeInterest.plus(scheduledRepayment + .getInterest(loanCurrency())); + cumulativeTotal = cumulativeTotal.plus(scheduledRepayment + .getTotal(loanCurrency())); + } + + for (LoanTransaction transaction : this.loanTransactions) { + if (transaction.isRepayment()) { + cumulativePaid = cumulativePaid.plus(transaction.getAmount()); + } + if (transaction.isWaiver()) { + cumulativeWaived = cumulativeWaived.plus(transaction + .getAmount()); + } + } + + return cumulativePaid.plus(cumulativeWaived).isGreaterThanOrEqualTo( + cumulativeTotal); + } + + private MonetaryCurrency loanCurrency() { + return this.loanRepaymentScheduleDetail.getCurrency(); + } + + public void writeOff(final DateTime writtenOffOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WRITE_OFF, this.loanStatus); + this.closedOnDate = writtenOffOn.toDate(); + this.writtenOffOnDate = writtenOffOn.toDate(); + if (new LocalDate(this.writtenOffOnDate).isBefore(this + .getDisbursementDate())) { + throw new InvalidLoanTimelineDate( + "The date the loan is written off cannot be before the loan disbursement date.", + "invalid.writeoff.date.as.date.is.before.disbursement.date"); + } + if (new LocalDate(this.writtenOffOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The date the loan is written off cannot be in the future.", + "invalid.writeoff.date.as.date.is.in.future"); + } + } + + public void reschedule(final DateTime rescheduledOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + this.loanStatus = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this.loanStatus); + this.closedOnDate = rescheduledOn.toDate(); + this.rescheduledOnDate = rescheduledOn.toDate(); + if (new LocalDate(this.rescheduledOnDate).isBefore(this + .getDisbursementDate())) { + throw new InvalidLoanTimelineDate( + "The date the loan is rescheduled cannot be before the loan disbursement date.", + "invalid.reschedule.date.as.date.is.before.disbursement.date"); + } + if (new LocalDate(this.rescheduledOnDate).isAfter(new LocalDate())) { + throw new InvalidLoanTimelineDate( + "The date the loan is rescheduled cannot be in the future.", + "invalid.reschedule.date.as.date.is.in.future"); + } + } + + public boolean isNotSubmittedAndPendingApproval() { + return !isSubmittedAndPendingApproval(); + } + + public boolean isSubmittedAndPendingApproval() { + return this.loanStatus.isSubmittedAndPendingApproval(); + } + + public boolean isApproved() { + return this.loanStatus.isApproved(); + } + + public boolean isNotApproved() { + return !isApproved(); + } + + public boolean isWaitingForDisbursal() { + return this.isApproved() && this.isNotDisbursed(); + } + + public boolean isNotDisbursed() { + return !this.isDisbursed(); + } + + public boolean isDisbursed() { + return hasDisbursementTransaction(); + } + + public boolean isUndoDisbursalAllowed() { + return isDisbursed() && this.hasNoRepaymentTransaction(); + } + + public boolean isClosed() { + return this.loanStatus.isClosed() || this.isCancelled(); + } + + public boolean isCancelled() { + return this.isRejected() || this.isWithdrawn(); + } + + public boolean isWithdrawn() { + return this.loanStatus.isWithdrawnByClient(); + } + + public boolean isRejected() { + return this.loanStatus.isRejected(); + } + + public boolean isNotClosed() { + return !this.isClosed(); + } + + public boolean isOpen() { + return this.loanStatus.isActive(); + } + + public boolean isOpenWithNoRepaymentMade() { + return this.isOpen() && hasNoRepaymentTransaction(); + } + + private boolean hasNoRepaymentTransaction() { + return !hasRepaymentTransaction(); + } + + private boolean hasRepaymentTransaction() { + boolean hasRepaymentTransaction = false; + for (LoanTransaction loanTransaction : this.loanTransactions) { + if (loanTransaction.isRepayment()) { + hasRepaymentTransaction = true; + break; + } + } + return hasRepaymentTransaction; + } + + private boolean hasDisbursementTransaction() { + boolean hasRepaymentTransaction = false; + for (LoanTransaction loanTransaction : this.loanTransactions) { + if (loanTransaction.isDisbursement()) { + hasRepaymentTransaction = true; + break; + } + } + return hasRepaymentTransaction; + } + + public boolean isOpenWithRepaymentMade() { + return this.isOpen() && this.hasRepaymentTransaction(); + } + + public List getRepaymentScheduleInstallments() { + return this.repaymentScheduleInstallments; + } + + public DerivedLoanData deriveLoanData(CurrencyData currencyData) { + + List repaymentTransactions = new ArrayList(); + for (LoanTransaction loanTransaction : this.loanTransactions) { + if (loanTransaction.isRepayment() || loanTransaction.isWaiver()) { + repaymentTransactions.add(loanTransaction); + } + } + + Money arrearsTolerance = this.loanRepaymentScheduleDetail + .getInArrearsTolerance(); + + return new DerivedLoanDataProcessor().process( + new ArrayList( + this.repaymentScheduleInstallments), + repaymentTransactions, currencyData, arrearsTolerance); + } + + public String getExternalId() { + return this.externalId; + } + + public void setExternalId(final String externalSystemIdentifer) { + if (StringUtils.isNotBlank(externalSystemIdentifer)) { + this.externalId = externalSystemIdentifer.trim(); + } else { + this.externalId = externalSystemIdentifer; + } + } + + public LocalDate getSubmittedOnDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate( + this.submittedOnDate), null); + } + + public LocalDate getRejectedOnDate() { + return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate( + this.rejectedOnDate), null); + } + + public LocalDate getApprovedOnDate() { + LocalDate date = null; + if (this.approvedOnDate != null) { + date = new LocalDate(this.approvedOnDate); + } + return date; + } + + public LocalDate getDisbursedOnDate() { + LocalDate date = null; + if (this.disbursedOnDate != null) { + date = new LocalDate(this.disbursedOnDate); + } + return date; + } + + public LocalDate getClosedOnDate() { + LocalDate date = null; + if (this.closedOnDate != null) { + date = new LocalDate(this.closedOnDate); + } + return date; + } + + public LocalDate getWrittenOffOnDate() { + LocalDate date = null; + if (this.writtenOffOnDate != null) { + date = new LocalDate(this.writtenOffOnDate); + } + return date; + } + + public Date getExpectedDisbursedOnDate() { + return this.expectedDisbursedOnDate; + } + + public LocalDate getExpectedFirstRepaymentOnDate() { + LocalDate firstRepaymentDate = null; + if (this.expectedFirstRepaymentOnDate != null) { + firstRepaymentDate = new LocalDate(this.expectedFirstRepaymentOnDate); + } + return firstRepaymentDate; + } + + public LocalDate getDisbursementDate() { + LocalDate disbursementDate = new LocalDate(this.expectedDisbursedOnDate); + if (this.disbursedOnDate != null) { + disbursementDate = new LocalDate(this.disbursedOnDate); + } + return disbursementDate; + } + + public LocalDate getExpectedMaturityDate() { + LocalDate possibleMaturityDate = null; + if (this.expectedMaturityDate != null) { + possibleMaturityDate = new LocalDate(this.expectedMaturityDate); + } + return possibleMaturityDate; + } + + public LocalDate getActualMaturityDate() { + LocalDate possibleMaturityDate = null; + if (this.maturedOnDate != null) { + possibleMaturityDate = new LocalDate(this.maturedOnDate); + } + return possibleMaturityDate; + } + + public LocalDate getMaturityDate() { + LocalDate possibleMaturityDate = null; + + if (this.expectedMaturityDate != null) { + possibleMaturityDate = new LocalDate(this.expectedMaturityDate); + } + if (this.maturedOnDate != null) { + possibleMaturityDate = new LocalDate(this.maturedOnDate); + } + return possibleMaturityDate; + } + + public void addRepaymentScheduleInstallment(final LoanRepaymentScheduleInstallment installment) { + installment.updateOrgnaisation(this.organisation); + installment.updateLoan(this); + this.repaymentScheduleInstallments.add(installment); + } + + public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected() { + return isActualDisbursedOnDateEarlierOrLaterThanExpected(new LocalDate( + this.disbursedOnDate)); + } + + public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected( + final LocalDate actualDisbursedOnDate) { + return !new LocalDate(this.expectedDisbursedOnDate) + .isEqual(actualDisbursedOnDate); + } + + public boolean isFlexibleRepaymentSchedule() { + return this.loanProduct.isFlexibleRepaymentSchedule(); + } + + public boolean isInterestRebateAllowed() { + return this.loanProduct.isInterestRebateAllowed(); + } + + public boolean isRepaymentScheduleRegenerationRequiredForDisbursement( + final LocalDate actualDisbursementDate) { + + boolean regenerationRequired = false; + + if (isFlexibleRepaymentSchedule()) { + regenerationRequired = false; + } else { + if (isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate)) { + regenerationRequired = true; + } + } + + return regenerationRequired; + } + + public LoanPayoffSummary getPayoffSummaryOn( + final LocalDate projectedPayoffDate) { + + LocalDate acutalDisbursementDate = new LocalDate(this.disbursedOnDate); + + Money totalPaidToDate = this.getTotalPaid(); + + Money totalOutstandingBasedOnExpectedMaturityDate = this + .getTotalOutstanding(); + Money totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnExpectedMaturityDate; + + Money rebateGivenOnProjectedPayoffDate = Money + .zero(this.loanRepaymentScheduleDetail.getPrincipal() + .getCurrency()); + if (isInterestRebateAllowed()) { + rebateGivenOnProjectedPayoffDate = calculateRebateWhenPaidInFullOn(projectedPayoffDate); + totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnExpectedMaturityDate + .minus(rebateGivenOnProjectedPayoffDate); + } + + return new LoanPayoffSummary(this.getId(), acutalDisbursementDate, + this.getMaturityDate(), projectedPayoffDate, totalPaidToDate, + totalOutstandingBasedOnExpectedMaturityDate, + totalOutstandingBasedOnPayoffDate, + rebateGivenOnProjectedPayoffDate); + } + + public Money getTotalOutstanding() { + return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus( + getTotalPaid()); + } + + private Money getTotalPaid() { + Money cumulativePaid = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + + for (LoanTransaction repayment : this.loanTransactions) { + if (repayment.isRepayment()) { + cumulativePaid = cumulativePaid.plus(repayment.getAmount()); + } + } + + return cumulativePaid; + } + + public Money calculateRebateWhenPaidInFullOn(final LocalDate paidInFullDate) { + + Money loanPrincipal = this.loanRepaymentScheduleDetail.getPrincipal(); + Money rebate = Money.zero(loanPrincipal.getCurrency()); + + if (this.isDisbursed() + && !paidInFullDate.isBefore(this.getDisbursementDate())) { + + InterestRebateCalculator interestRebateCalculator = this.interestRebateCalculatorFactory + .createCalcualtor(this.loanRepaymentScheduleDetail + .getInterestMethod(), + this.loanRepaymentScheduleDetail + .getAmortizationMethod()); + + rebate = interestRebateCalculator.calculate(this + .getDisbursementDate(), paidInFullDate, loanPrincipal, + this.loanRepaymentScheduleDetail + .getAnnualNominalInterestRate(), + this.repaymentScheduleInstallments, this.loanTransactions); + } + + return rebate; + } + + private Money getTotalInterestOnLoan() { + Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { + cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterest(loanCurrency())); + } + + return cumulativeInterest; + } + + private Money getTotalPrincipalOnLoan() { + Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail + .getPrincipal().getCurrency()); + + for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { + cumulativePrincipal = cumulativePrincipal.plus(scheduledRepayment + .getPrincipal(loanCurrency())); + } + + return cumulativePrincipal; + } + + public Money getInterestRebateOwed() { + return Money.of(this.loanRepaymentScheduleDetail.getCurrency(), this.interestRebateOwed); + } + + public Money getInArrearsTolerance() { + return this.loanRepaymentScheduleDetail.getInArrearsTolerance(); + } + + public boolean identifiedBy(String identifier) { + return identifier.equalsIgnoreCase(this.externalId) + || identifier.equalsIgnoreCase(this.getId().toString()); + } + + public String getLoanStatusDisplayName() { + return this.loanStatus.getDisplayName(); + } + + public MonetaryCurrency getCurrency() { + return this.loanRepaymentScheduleDetail.getCurrency(); + } + + public LocalDate getInterestCalculatedFromDate() { + + LocalDate interestCalculatedFrom = null; + if (this.interestCalculatedFromDate != null) { + interestCalculatedFrom = new LocalDate(this.interestCalculatedFromDate); + } + + return interestCalculatedFrom; + } + + public LocalDate getLoanStatusSinceDate() { + + LocalDate statusSinceDate = new LocalDate(this.submittedOnDate); + if (isApproved()) { + statusSinceDate = new LocalDate(this.approvedOnDate); + } + + if (isDisbursed()) { + statusSinceDate = new LocalDate(this.disbursedOnDate); + } + + if (isClosed()) { + statusSinceDate = new LocalDate(this.closedOnDate); + } + + return statusSinceDate; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanBuilder.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanBuilder.java new file mode 100644 index 000000000..d315845e5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanBuilder.java @@ -0,0 +1,45 @@ +package org.mifosng.platform.loan.domain; + +import org.mifosng.platform.client.domain.Client; +import org.mifosng.platform.organisation.domain.Organisation; + +public class LoanBuilder { + + private Organisation organisation; + private Client client; + private LoanProduct loanProduct; + private LoanProductRelatedDetail loanRepaymentScheduleDetail; + private LoanStatus loanStatus; + private String externalSystemId; + + public Loan build() { + Loan loan = new Loan(this.organisation, this.client, this.loanProduct, this.loanRepaymentScheduleDetail, this.loanStatus); + loan.setExternalId(this.externalSystemId); + return loan; + } + + public LoanBuilder with(final Organisation withOrg) { + this.organisation = withOrg; + return this; + } + + public LoanBuilder with(final LoanProduct withLoanProduct) { + this.loanProduct = withLoanProduct; + return this; + } + + public LoanBuilder with(final Client withClient) { + this.client = withClient; + return this; + } + + public LoanBuilder with(final LoanProductRelatedDetail withLoanRepaymentScheduleDetail) { + this.loanRepaymentScheduleDetail = withLoanRepaymentScheduleDetail; + return this; + } + + public LoanBuilder withExternalSystemId(final String withExternalSystemId) { + this.externalSystemId = withExternalSystemId; + return this; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java new file mode 100644 index 000000000..759cf75b1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanEvent.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.loan.domain; + +public enum LoanEvent { + + LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT, + // + REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED; +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanLifecycleStateMachine.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanLifecycleStateMachine.java new file mode 100644 index 000000000..c56097b40 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanLifecycleStateMachine.java @@ -0,0 +1,7 @@ +package org.mifosng.platform.loan.domain; + +public interface LoanLifecycleStateMachine { + + LoanStatus transition(LoanEvent loanEvent, LoanStatus from); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanPayoffSummary.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanPayoffSummary.java new file mode 100644 index 000000000..d63876cc8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanPayoffSummary.java @@ -0,0 +1,76 @@ +package org.mifosng.platform.loan.domain; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.Money; + +public class LoanPayoffSummary { + + private final Long reference; + private final LocalDate acutalDisbursementDate; + private final LocalDate expectedMaturityDate; + private final LocalDate projectedMaturityDate; + private final Money totalPaidToDate; + private final Money totalOutstandingBasedOnExpectedMaturityDate; + private final Money totalOutstandingBasedOnPayoffDate; + private final Money rebateOwed; + + public LoanPayoffSummary(final Long reference, + final LocalDate acutalDisbursementDate, + final LocalDate expectedMaturityDate, + final LocalDate projectedMaturityDate, final Money totalPaidToDate, + final Money totalOutstandingBasedOnExpectedMaturityDate, + final Money totalOutstandingBasedOnPayoffDate, + final Money rebateOwed) { + this.reference = reference; + this.acutalDisbursementDate = acutalDisbursementDate; + this.expectedMaturityDate = expectedMaturityDate; + this.projectedMaturityDate = projectedMaturityDate; + this.totalPaidToDate = totalPaidToDate; + this.totalOutstandingBasedOnExpectedMaturityDate = totalOutstandingBasedOnExpectedMaturityDate; + this.totalOutstandingBasedOnPayoffDate = totalOutstandingBasedOnPayoffDate; + this.rebateOwed = rebateOwed; + } + + public Long getReference() { + return this.reference; + } + + public LocalDate getAcutalDisbursementDate() { + return this.acutalDisbursementDate; + } + + public LocalDate getExpectedMaturityDate() { + return this.expectedMaturityDate; + } + + public LocalDate getProjectedMaturityDate() { + return this.projectedMaturityDate; + } + + public Money getTotalPaidToDate() { + return this.totalPaidToDate; + } + + public Money getRebateOwed() { + return this.rebateOwed; + } + + public Integer getExpectedLoanTermInDays() { + return Days.daysBetween(this.acutalDisbursementDate, + this.expectedMaturityDate).getDays(); + } + + public Integer getProjectedLoanTermInDays() { + return Days.daysBetween(this.acutalDisbursementDate, + this.projectedMaturityDate).getDays(); + } + + public Money getTotalOutstandingBasedOnExpectedMaturityDate() { + return this.totalOutstandingBasedOnExpectedMaturityDate; + } + + public Money getTotalOutstandingBasedOnPayoffDate() { + return this.totalOutstandingBasedOnPayoffDate; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProduct.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProduct.java new file mode 100644 index 000000000..f0f6a7847 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProduct.java @@ -0,0 +1,140 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.apache.commons.lang.StringUtils; +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; + +/** + * Loan products allow for categorisation of an organisations loans into something meaningful to them. + * + * They provide a means of simplifying creation/maintenance of loans. + * They can also allow for product comparison to take place when reporting. + * + * They allow for constraints to be added at product level. + */ +@Entity +@Table(name = "portfolio_product_loan") +public class LoanProduct extends AbstractAuditableCustom { + + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description") + private String description; + + @Embedded + private final LoanProductRelatedDetail loanProductRelatedDetail; + + public LoanProduct() { + this.organisation = null; + this.name = null; + this.description = null; + this.loanProductRelatedDetail = null; + } + + public LoanProduct(final Organisation organisation, final String name, final String description, final MonetaryCurrency currency, final BigDecimal defaultPrincipal, + final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod, + final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfInstallments, final AmortizationMethod amortizationMethod, + final BigDecimal inArrearsTolerance, + final boolean flexibleRepaymentSchedule, + final boolean interestRebateAllowed) { + this.organisation = organisation; + this.name = name.trim(); + if (StringUtils.isNotBlank(description)) { + this.description = description.trim(); + } else { + this.description = null; + } + + this.loanProductRelatedDetail = new LoanProductRelatedDetail(currency, + defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, + repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, inArrearsTolerance, flexibleRepaymentSchedule, interestRebateAllowed); + } + + public Organisation getOrganisation() { + return this.organisation; + } + + public MonetaryCurrency getCurrency() { + return this.loanProductRelatedDetail.getCurrency(); + } + + public Money getInArrearsTolerance() { + return this.loanProductRelatedDetail.getInArrearsTolerance(); + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public Integer getRepayEvery() { + return this.loanProductRelatedDetail.getRepayEvery(); + } + + public boolean isFlexibleRepaymentSchedule() { + return this.loanProductRelatedDetail.isFlexibleRepaymentSchedule(); + } + + public boolean isInterestRebateAllowed() { + return this.loanProductRelatedDetail.isInterestRebateAllowed(); + } + + public boolean identifiedBy(final String identifier) { + return identifier.equalsIgnoreCase(this.name); + } + + public BigDecimal getDefaultNominalInterestRatePerPeriod() { + return this.loanProductRelatedDetail.getNominalInterestRatePerPeriod(); + } + + public PeriodFrequencyType getInterestPeriodFrequencyType() { + return this.loanProductRelatedDetail.getInterestPeriodFrequencyType(); + } + + public BigDecimal getDefaultAnnualNominalInterestRate() { + return this.loanProductRelatedDetail.getAnnualNominalInterestRate(); + } + + public InterestMethod getInterestMethod() { + return this.loanProductRelatedDetail.getInterestMethod(); + } + + public PeriodFrequencyType getRepaymentPeriodFrequencyType() { + return this.loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); + } + + public Integer getDefaultNumberOfRepayments() { + return this.loanProductRelatedDetail.getNumberOfRepayments(); + } + + public AmortizationMethod getAmortizationMethod() { + return this.loanProductRelatedDetail.getAmortizationMethod(); + } + + public void update(UpdateLoanProductCommand command) { + this.name = command.getName(); + this.description = command.getDescription(); + this.loanProductRelatedDetail.update(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java new file mode 100644 index 000000000..538cf1bdc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java @@ -0,0 +1,15 @@ +package org.mifosng.platform.loan.domain; + +import java.io.Serializable; + +/** + * Represents the bare minimum repayment details needed for activities related to generating repayment schedules. + */ +public interface LoanProductMinimumRepaymentScheduleRelatedDetail extends Serializable { + + Integer getRepayEvery(); + + PeriodFrequencyType getRepaymentPeriodFrequencyType(); + + Integer getNumberOfRepayments(); +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRelatedDetail.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRelatedDetail.java new file mode 100644 index 000000000..14d4fda45 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRelatedDetail.java @@ -0,0 +1,179 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; + +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; + +/** + * LoanRepaymentScheduleDetail encapsulates all the details of a + * {@link LoanProduct} that are also used and persisted by a {@link Loan}. + */ +@Embeddable +public class LoanProductRelatedDetail implements + LoanProductMinimumRepaymentScheduleRelatedDetail { + + @Embedded + private MonetaryCurrency currency; + + @Column(name = "principal_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal principal; + + @Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false) + private BigDecimal nominalInterestRatePerPeriod; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "interest_period_frequency_enum", nullable = false) + private PeriodFrequencyType interestPeriodFrequencyType; + + @Column(name = "annual_nominal_interest_rate", scale = 6, precision = 19, nullable = false) + private BigDecimal annualNominalInterestRate; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "interest_method_enum", nullable = false) + private InterestMethod interestMethod; + + @Column(name = "repay_every", nullable = false) + private Integer repayEvery; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "repayment_period_frequency_enum", nullable = false) + private PeriodFrequencyType repaymentPeriodFrequencyType; + + @Column(name = "number_of_repayments", nullable = false) + private Integer numberOfRepayments; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "amortization_method_enum", nullable = false) + private AmortizationMethod amortizationMethod; + + @Column(name = "flexible_repayment_schedule", nullable = false) + private boolean flexibleRepaymentSchedule; + + @Column(name = "interest_rebate", nullable = false) + private boolean interestRebateAllowed; + + @Column(name = "arrearstolerance_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal inArrearsTolerance; + + protected LoanProductRelatedDetail() { + this.principal = null; + this.nominalInterestRatePerPeriod = null; + this.interestPeriodFrequencyType = null; + this.annualNominalInterestRate = null; + this.interestMethod = null; + + this.repayEvery = null; + this.repaymentPeriodFrequencyType = null; + this.numberOfRepayments = null; + this.amortizationMethod = null; + this.inArrearsTolerance = null; + + this.flexibleRepaymentSchedule = false; + this.interestRebateAllowed = false; + } + + public LoanProductRelatedDetail(final MonetaryCurrency currency, + final BigDecimal defaultPrincipal, + final BigDecimal defaultNominalInterestRatePerPeriod, + final PeriodFrequencyType interestPeriodFrequencyType, + final BigDecimal defaultAnnualNominalInterestRate, + final InterestMethod interestMethod, final Integer repayEvery, + final PeriodFrequencyType repaymentFrequencyType, + final Integer defaultNumberOfRepayments, + final AmortizationMethod amortizationMethod, + final BigDecimal inArrearsTolerance, + final boolean flexibleRepaymentSchedule, + final boolean interestRebateAllowed) { + this.currency = currency; + this.principal = defaultPrincipal; + this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; + this.interestPeriodFrequencyType = interestPeriodFrequencyType; + this.annualNominalInterestRate = defaultAnnualNominalInterestRate; + this.interestMethod = interestMethod; + this.repayEvery = repayEvery; + this.repaymentPeriodFrequencyType = repaymentFrequencyType; + this.numberOfRepayments = defaultNumberOfRepayments; + this.amortizationMethod = amortizationMethod; + this.inArrearsTolerance = inArrearsTolerance; + this.flexibleRepaymentSchedule = flexibleRepaymentSchedule; + this.interestRebateAllowed = interestRebateAllowed; + } + + public MonetaryCurrency getCurrency() { + return this.currency.copy(); + } + + public Money getPrincipal() { + return Money.of(this.currency, this.principal); + } + + public Money getInArrearsTolerance() { + return Money.of(this.currency, this.inArrearsTolerance); + } + + public BigDecimal getNominalInterestRatePerPeriod() { + return BigDecimal.valueOf(Double.valueOf(this.nominalInterestRatePerPeriod.stripTrailingZeros().toString())) ; + } + + public PeriodFrequencyType getInterestPeriodFrequencyType() { + return interestPeriodFrequencyType; + } + + public BigDecimal getAnnualNominalInterestRate() { + return BigDecimal.valueOf(Double.valueOf(this.annualNominalInterestRate.stripTrailingZeros().toString())) ; + } + + public InterestMethod getInterestMethod() { + return interestMethod; + } + + @Override + public Integer getRepayEvery() { + return repayEvery; + } + + @Override + public PeriodFrequencyType getRepaymentPeriodFrequencyType() { + return repaymentPeriodFrequencyType; + } + + @Override + public Integer getNumberOfRepayments() { + return numberOfRepayments; + } + + public AmortizationMethod getAmortizationMethod() { + return amortizationMethod; + } + + public boolean isFlexibleRepaymentSchedule() { + return flexibleRepaymentSchedule; + } + + public boolean isInterestRebateAllowed() { + return interestRebateAllowed; + } + + public void update(UpdateLoanProductCommand command) { + + this.currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + this.principal = command.getPrincipal(); + this.repayEvery = command.getRepaymentEvery(); + this.repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getRepaymentFrequency()); + this.numberOfRepayments = command.getNumberOfRepayments(); + this.amortizationMethod = AmortizationMethod.fromInt(command.getAmortizationMethod()); + this.inArrearsTolerance = command.getInArrearsToleranceAmount(); + + this.nominalInterestRatePerPeriod = command.getInterestRatePerPeriod(); + this.interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyMethod()); + this.interestMethod = InterestMethod.fromInt(command.getInterestMethod()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRepository.java new file mode 100644 index 000000000..6651d0de7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanProductRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.loan.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanProductRepository extends + JpaRepository, JpaSpecificationExecutor { + // no behaviour added +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java new file mode 100644 index 000000000..2fc3d42e7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java @@ -0,0 +1,166 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "portfolio_loan_repayment_schedule") +public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private Organisation organisation; + + @ManyToOne(optional = false) + @JoinColumn(name = "loan_id") + private Loan loan; + + @Column(name = "installment") + private final Integer installmentNumber; + + @Column(name = "principal_amount", scale=6, precision=19) + private BigDecimal principal; + + @Column(name = "interest_amount", scale=6, precision=19) + private BigDecimal interest; + + @Column(name = "principal_completed_derived", scale=6, precision=19) + private BigDecimal principalCompleted; + + @Column(name = "interest_completed_derived", scale = 6, precision = 19) + private BigDecimal interestCompleted; + + @Column(name="completed_derived") + private boolean completed; + + @Temporal(TemporalType.DATE) + @Column(name = "duedate") + private final Date dueDate; + + protected LoanRepaymentScheduleInstallment() { + this.organisation = null; + this.loan = null; + this.installmentNumber = null; + this.dueDate = null; + this.principal = null; + this.interest = null; + this.principalCompleted = null; + this.interestCompleted = null; + this.completed = false; + } + + public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate from, + final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest) { + this.loan = loan; + this.installmentNumber = installmentNumber; + this.dueDate = dueDate.toDateMidnight().toDate(); + this.principal = principal; + this.principalCompleted = BigDecimal.ZERO; + this.interest = interest; + this.interestCompleted = BigDecimal.ZERO; + this.completed = false; + } + + public Loan getLoan() { + return this.loan; + } + + public Integer getInstallmentNumber() { + return this.installmentNumber; + } + + public LocalDate getDueDate() { + return new LocalDate(this.dueDate); + } + + public Money updateDerivedComponents(Money totalAvailable) { + + Money remaining = totalAvailable.copy(); + + boolean principalCompletedInFull = false; + boolean interestCompletedInFull = false; + + Money interest = getInterest(totalAvailable.getCurrency()); + Money principal = getPrincipal(totalAvailable.getCurrency()); + + // TODO - configuration around order components of loan are paid off in. + // pay off interest + if (remaining.isGreaterThanOrEqualTo(interest)) { + this.interestCompleted = interest.getAmount(); + interestCompletedInFull = true; + } else { + this.interestCompleted = remaining.getAmount(); + } + remaining = remaining.minus(this.interestCompleted); + + + // pay off principal + if (remaining.isGreaterThanOrEqualTo(principal)) { + this.principalCompleted = principal.getAmount(); + principalCompletedInFull = true; + } else { + this.principalCompleted = remaining.getAmount(); + } + remaining = remaining.minus(this.principalCompleted); + + this.completed = (principalCompletedInFull && interestCompletedInFull); + + return remaining; + } + + public Money getPrincipal(MonetaryCurrency currency) { + return Money.of(currency, this.principal); + } + + public Money getPrincipalCompleted(MonetaryCurrency currency) { + return Money.of(currency, this.principalCompleted); + } + + public Money getInterest(MonetaryCurrency currency) { + return Money.of(currency, this.interest); + } + + public Money getInterestCompleted(MonetaryCurrency currency) { + return Money.of(currency, this.interestCompleted); + } + + public Money getTotal(MonetaryCurrency currency) { + return getPrincipal(currency).plus(getInterest(currency)); + } + + public void updateOrgnaisation(Organisation organisation) { + this.organisation = organisation; + } + + public void updateLoan(final Loan loan) { + this.loan = loan; + } + + public boolean unpaid() { + return !this.completed; + } + + public Money getTotalDue(MonetaryCurrency currency) { + return getTotal(currency).minus(getTotalCompleted(currency)); + } + + private Money getTotalCompleted(MonetaryCurrency currency) { + return getPrincipalCompleted(currency).plus(getInterestCompleted(currency)); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepository.java new file mode 100644 index 000000000..2b773e68c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.loan.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanRepository extends JpaRepository, + JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java new file mode 100644 index 000000000..e5b12253f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatus.java @@ -0,0 +1,72 @@ +package org.mifosng.platform.loan.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.springframework.data.domain.Persistable; + +@Entity +@Table(name = "ref_loan_status") +public class LoanStatus implements Persistable { + + public static final Integer SUBMITED_AND_PENDING_APPROVAL = 100; + public static final Integer APPROVED = 200; + public static final Integer ACTIVE = 300; + public static final Integer WITHDRAWN_BY_CLIENT = 400; + public static final Integer REJECTED = 500; + public static final Integer CLOSED = 600; + + @Id + @Column(name = "id") + private Integer id; + + @Column(name = "display_name") + private String displayName; + + public LoanStatus() { + } + + public String getDisplayName() { + return this.displayName; + } + + @Override + public Integer getId() { + return null; + } + + @Override + public boolean isNew() { + return false; + } + + public boolean hasStateOf(Integer state) { + return this.id.equals(state); + } + + public boolean isSubmittedAndPendingApproval() { + return hasStateOf(LoanStatus.SUBMITED_AND_PENDING_APPROVAL); + } + + public boolean isApproved() { + return hasStateOf(LoanStatus.APPROVED); + } + + public boolean isClosed() { + return hasStateOf(LoanStatus.CLOSED); + } + + public boolean isWithdrawnByClient() { + return hasStateOf(LoanStatus.WITHDRAWN_BY_CLIENT); + } + + public boolean isRejected() { + return hasStateOf(LoanStatus.REJECTED); + } + + public boolean isActive() { + return hasStateOf(LoanStatus.ACTIVE); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatusRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatusRepository.java new file mode 100644 index 000000000..5563ebf65 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanStatusRepository.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.loan.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanStatusRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java new file mode 100644 index 000000000..e486d23e2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java @@ -0,0 +1,161 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Organisation; +import org.mifosng.platform.user.domain.AppUser; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * All monetary transactions against a loan are modelled through this entity. + * Disbursements, Repayments, Waivers, Write-off etc + */ +@Entity +@Table(name = "portfolio_loan_transaction") +public class LoanTransaction extends AbstractAuditableCustom { + + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private Organisation organisation; + + @ManyToOne(optional = false) + @JoinColumn(name = "loan_id", nullable=false) + private Loan loan; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private final BigDecimal amount; + + @Temporal(TemporalType.DATE) + @Column(name = "transaction_date", nullable=false) + private final Date dateOf; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "transaction_type_enum", nullable = false) + private LoanTransactionType typeOf; + + @OneToOne(optional=true, cascade={CascadeType.PERSIST}) + @JoinColumn(name="contra_id") + private LoanTransaction contra; + + protected LoanTransaction() { + this.organisation = null; + this.loan = null; + this.amount = null; + this.dateOf = null; + this.typeOf = null; + } + + public static LoanTransaction disbursement(Money amount, LocalDate disbursementDate) { + return new LoanTransaction(LoanTransactionType.DISBURSEMENT, amount.getAmount(), disbursementDate); + } + + public static LoanTransaction repayment(Money amount, LocalDate paymentDate) { + return new LoanTransaction(LoanTransactionType.REPAYMENT, amount.getAmount(), paymentDate); + } + + public static LoanTransaction waiver(Money waived, LocalDate waiveDate) { + return new LoanTransaction(LoanTransactionType.WAIVED, waived.getAmount(), waiveDate); + } + + private static LoanTransaction contra(LoanTransaction originalTransaction) { + + LoanTransaction contra = new LoanTransaction(LoanTransactionType.REVERSAL, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf())); + contra.updateContra(originalTransaction); + + return contra; + } + + public void updateContra(LoanTransaction transaction) { + this.contra = transaction; + } + + private LoanTransaction(LoanTransactionType type, final BigDecimal amount, final LocalDate date) { + this.typeOf = type; + this.amount = amount; + this.dateOf = date.toDateMidnight().toDate(); + } + + public BigDecimal getAmount() { + return this.amount; + } + + public Money getAmount(MonetaryCurrency currency) { + return Money.of(currency, this.amount); + } + + @DateTimeFormat(style="-M") + public LocalDate getTransactionDate() { + return new LocalDate(this.dateOf); + } + + public Date getDateOf() { + return dateOf; + } + + public LoanTransactionType getTypeOf() { + return typeOf; + } + + public boolean isRepayment() { + return LoanTransactionType.REPAYMENT.equals(typeOf) && isNotContra(); + } + + public boolean isNotRepayment() { + return !isRepayment(); + } + + public boolean isNotContra() { + return this.contra == null; + } + + public boolean isDisbursement() { + return LoanTransactionType.DISBURSEMENT.equals(typeOf); + } + + public boolean isWaiver() { + return LoanTransactionType.WAIVED.equals(typeOf) && isNotContra(); + } + + public boolean isNotWaiver() { + return !isWaiver(); + } + + public boolean isIdentifiedBy(Long identifier) { + return this.getId().equals(identifier); + } + + public void contra() { + this.contra = LoanTransaction.contra(this); + contra.updateLoan(this.loan); + contra.updateOrganisation(this.organisation); + } + + public void updateOrganisation(Organisation organisation) { + this.organisation = organisation; + } + + public void updateLoan(final Loan loan) { + this.loan = loan; + } + + public boolean isNonZero() { + return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionRepository.java new file mode 100644 index 000000000..cb4c1de13 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.loan.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanTransactionRepository extends JpaRepository, + JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java new file mode 100644 index 000000000..9e4145259 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java @@ -0,0 +1,42 @@ +package org.mifosng.platform.loan.domain; + +public enum LoanTransactionType { + UNKNOWN(0), DISBURSEMENT(1), REPAYMENT(2), REVERSAL(3), WAIVED(4); + + private final Integer value; + + private LoanTransactionType(final Integer value) { + this.value = value; + } + + public Integer getValue() { + return this.value; + } + + public static LoanTransactionType fromInt(final Integer transactionType) { + + if (transactionType == null) { + return LoanTransactionType.UNKNOWN; + } + + LoanTransactionType loanTransactionType = null; + switch (transactionType) { + case 1: + loanTransactionType = LoanTransactionType.DISBURSEMENT; + break; + case 2: + loanTransactionType = LoanTransactionType.REPAYMENT; + break; + case 3: + loanTransactionType = LoanTransactionType.REVERSAL; + break; + case 4: + loanTransactionType = LoanTransactionType.WAIVED; + break; + default: + loanTransactionType = LoanTransactionType.UNKNOWN; + break; + } + return loanTransactionType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/PeriodFrequencyType.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/PeriodFrequencyType.java new file mode 100644 index 000000000..37608689a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/PeriodFrequencyType.java @@ -0,0 +1,38 @@ +package org.mifosng.platform.loan.domain; + +public enum PeriodFrequencyType { + DAYS(0), WEEKS(1), MONTHS(2), YEARS(3), INVALID(4); + + private final Integer value; + + private PeriodFrequencyType(final Integer value) { + this.value = value; + } + + public Integer getValue() { + return this.value; + } + + public static PeriodFrequencyType fromInt(final Integer frequency) { + + PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.INVALID; + switch (frequency) { + case 0: + repaymentFrequencyType = PeriodFrequencyType.DAYS; + break; + case 1: + repaymentFrequencyType = PeriodFrequencyType.WEEKS; + break; + case 2: + repaymentFrequencyType = PeriodFrequencyType.MONTHS; + break; + case 3: + repaymentFrequencyType = PeriodFrequencyType.YEARS; + break; + default: + repaymentFrequencyType = PeriodFrequencyType.INVALID; + break; + } + return repaymentFrequencyType; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceEqualInstallmentsLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceEqualInstallmentsLoanScheduleGenerator.java new file mode 100644 index 000000000..6a3118996 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceEqualInstallmentsLoanScheduleGenerator.java @@ -0,0 +1,268 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public class DecliningBalanceEqualInstallmentsLoanScheduleGenerator implements + LoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @Override + public LoanSchedule generate( + final LoanProductRelatedDetail loanScheduleInfo, + final LocalDate disbursementDate, + final LocalDate firstRepaymentDate, + final LocalDate interestCalculatedFrom, CurrencyData currencyData) { + + List scheduledLoanInstallments = new ArrayList(); + + List scheduledDates = this.scheduledDateGenerator.generate( + loanScheduleInfo, disbursementDate, firstRepaymentDate); + + LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates); + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + BigDecimal paymentPeriodsInYear = BigDecimal + .valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType())); + + BigDecimal periodicInterestRate = loanScheduleInfo + .getAnnualNominalInterestRate() + .divide(paymentPeriodsInYear, mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); + + double interestRateFraction = periodicInterestRate.doubleValue(); + + double futureValue = 0; + double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); + double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); + double totalRepayment = paymentPerInstallment * numberOfPeriods; + + final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); + + Money totalDuePerInstallment = Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment)); + Money totalDue = Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); + + Money totalInterestDue = totalDue.minus(loanScheduleInfo.getPrincipal()); + Money outstandingBalance = loanScheduleInfo.getPrincipal(); + Money totalPrincipal = Money.zero(monetaryCurrency); + Money totalInterest = Money.zero(monetaryCurrency); + + double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator.calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), + loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, idealDisbursementDateBasedOnFirstRepaymentDate); + + LocalDate startDate = disbursementDate; + int installmentNumber = 1; + for (LocalDate scheduledDueDate : scheduledDates) { + + Money interestPerInstallment = outstandingBalance.multiplyRetainScale(periodicInterestRate, RoundingMode.HALF_EVEN); + Money principalPerInstallment = totalDuePerInstallment.minus(interestPerInstallment); + + if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { + Money graceOnInterestForRepaymentPeriod = interestPerInstallment; + interestPerInstallment = interestPerInstallment.minus(graceOnInterestForRepaymentPeriod); + totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); + interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - Integer.valueOf(1).doubleValue(); + } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { + Money graceOnInterestForRepaymentPeriod = interestPerInstallment.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); + interestPerInstallment = interestPerInstallment.minus(graceOnInterestForRepaymentPeriod); + totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); + interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); + } + + totalPrincipal = totalPrincipal.plus(principalPerInstallment); + totalInterest = totalInterest.plus(interestPerInstallment); + + if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) { + Money principalDifference = totalPrincipal.minus(loanScheduleInfo + .getPrincipal()); + if (principalDifference.isLessThanZero()) { + principalPerInstallment = principalPerInstallment + .plus(principalDifference.abs()); + } else if (principalDifference.isGreaterThanZero()) { + principalPerInstallment = principalPerInstallment + .minus(principalDifference.abs()); + } + + Money interestDifference = totalInterest.minus(totalInterestDue); + if (interestDifference.isLessThanZero()) { + interestPerInstallment = interestPerInstallment + .plus(interestDifference.abs()); + } else if (interestDifference.isGreaterThanZero()) { + interestPerInstallment = interestPerInstallment + .minus(interestDifference.abs()); + } + } + + Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment); + + outstandingBalance = outstandingBalance.minus(principalPerInstallment); + + MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount()); + MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount()); + MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount()); + MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount()); + + ScheduledLoanInstallment installment = new ScheduledLoanInstallment( + Integer.valueOf(installmentNumber), startDate, + scheduledDueDate, principalPerInstallmentValue, + interestPerInstallmentValue, + totalInstallmentDueValue, + outstandingBalanceValue); + + scheduledLoanInstallments.add(installment); + + startDate = scheduledDueDate; + + installmentNumber++; + } + + return new LoanSchedule(scheduledLoanInstallments); + } + + /** + * Future value of an amount given the number of payments, rate, amount of + * individual payment, present value and boolean value indicating whether + * payments are due at the beginning of period (false => payments are due at + * end of period) + * + * @param r + * rate + * @param n + * num of periods + * @param y + * pmt per period + * @param p + * future value + * @param t + * type (true=pmt at end of period, false=pmt at begining of + * period) + */ + public static double fv(double r, double n, double y, double p, boolean t) { + double retval = 0; + if (r == 0) { + retval = -1 * (p + (n * y)); + } else { + double r1 = r + 1; + retval = ((1 - Math.pow(r1, n)) * (t ? r1 : 1) * y) / r - p + * Math.pow(r1, n); + } + return retval; + } + + /** + * Present value of an amount given the number of future payments, rate, + * amount of individual payment, future value and boolean value indicating + * whether payments are due at the beginning of period (false => payments + * are due at end of period) + * + * @param r + * @param n + * @param y + * @param f + * @param t + */ + public static double pv(double r, double n, double y, double f, boolean t) { + double retval = 0; + if (r == 0) { + retval = -1 * ((n * y) + f); + } else { + double r1 = r + 1; + retval = (((1 - Math.pow(r1, n)) / r) * (t ? r1 : 1) * y - f) + / Math.pow(r1, n); + } + return retval; + } + + /** + * calculates the Net Present Value of a principal amount given the discount + * rate and a sequence of cash flows (supplied as an array). If the amounts + * are income the value should be positive, else if they are payments and + * not income, the value should be negative. + * + * @param r + * @param cfs + * cashflow amounts + */ + public static double npv(double r, double[] cfs) { + double npv = 0; + double r1 = r + 1; + double trate = r1; + for (int i = 0, iSize = cfs.length; i < iSize; i++) { + npv += cfs[i] / trate; + trate *= r1; + } + return npv; + } + + /** + * PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest). + * + * This monthly payment c depends upon + * the monthly interest rate r (expressed as a fraction, not a percentage, + * i.e., divide the quoted yearly percentage rate by 100 and by 12 to obtain + * the monthly interest rate), the number of monthly payments N called the + * loan's term, and the amount borrowed P known as the loan's principal; c + * is given by the formula: + * + * c = (r / (1 - (1 + r)^-N))P + * + * @param interestRateFraction + * @param numberOfPayments + * @param principal + * @param futureValue + * @param type + */ + public static double pmt(double interestRateFraction, double numberOfPayments, double principal, double futureValue, boolean type) { + double payment = 0; + if (interestRateFraction == 0) { + payment = -1 * (futureValue + principal) / numberOfPayments; + } else { + double r1 = interestRateFraction + 1; + payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction + / ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments))); + } + return payment; + } + + /** + * + * @param r + * @param y + * @param p + * @param f + * @param t + */ + public static double nper(double r, double y, double p, double f, boolean t) { + double retval = 0; + if (r == 0) { + retval = -1 * (f + p) / y; + } else { + double r1 = r + 1; + double ryr = (t ? r1 : 1) * y / r; + double a1 = ((ryr - f) < 0) ? Math.log(f - ryr) : Math.log(ryr - f); + double a2 = ((ryr - f) < 0) ? Math.log(-p - ryr) : Math + .log(p + ryr); + double a3 = Math.log(r1); + retval = (a1 - a2) / a3; + } + return retval; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceLoanScheduleGenerator.java new file mode 100644 index 000000000..3316856f0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DecliningBalanceLoanScheduleGenerator.java @@ -0,0 +1,107 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.exceptions.NoAuthorizationException; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public class DecliningBalanceLoanScheduleGenerator implements + LoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @Override + public LoanSchedule generate( + final LoanProductRelatedDetail loanScheduleInfo, + final LocalDate disbursementDate, final LocalDate firstRepaymentDate, + LocalDate interestCalculatedFrom, CurrencyData currencyData) { + + if (firstRepaymentDate != null + && disbursementDate.isAfter(firstRepaymentDate)) { + throw new NoAuthorizationException( + "cannot have disbursement date after first repayment date."); + } + + List scheduledLoanInstallments = new ArrayList(); + + List scheduledDates = this.scheduledDateGenerator.generate( + loanScheduleInfo, disbursementDate, firstRepaymentDate); + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + Money principalPerInstallment = loanScheduleInfo.getPrincipal() + .dividedBy(loanScheduleInfo.getNumberOfRepayments(), + RoundingMode.HALF_EVEN); + + BigDecimal paymentPeriodsInYear = BigDecimal + .valueOf(this.paymentPeriodsInOneYearCalculator + .calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType())); + BigDecimal periodicInterestRate = loanScheduleInfo + .getAnnualNominalInterestRate() + .divide(paymentPeriodsInYear, mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); + + Money outstandingBalance = loanScheduleInfo.getPrincipal(); + Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); + + LocalDate startDate = disbursementDate; + int installmentNumber = 1; + + for (LocalDate scheduledDueDate : scheduledDates) { + totalPrincipal = totalPrincipal.plus(principalPerInstallment); + + Money interestPerInstallment = outstandingBalance + .multiplyRetainScale(periodicInterestRate, + RoundingMode.HALF_EVEN); + + if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) { + Money difference = totalPrincipal.minus(loanScheduleInfo + .getPrincipal()); + if (difference.isLessThanZero()) { + principalPerInstallment = principalPerInstallment + .plus(difference.abs()); + } else if (difference.isGreaterThanZero()) { + principalPerInstallment = principalPerInstallment + .minus(difference.abs()); + } + } + + Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment); + + outstandingBalance = outstandingBalance + .minus(principalPerInstallment); + + MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount()); + MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount()); + MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount()); + MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount()); + + ScheduledLoanInstallment installment = new ScheduledLoanInstallment( + Integer.valueOf(installmentNumber), startDate, + scheduledDueDate, principalPerInstallmentValue, + interestPerInstallmentValue, + totalInstallmentDueValue, + outstandingBalanceValue); + + scheduledLoanInstallments.add(installment); + + startDate = scheduledDueDate; + + installmentNumber++; + } + + return new LoanSchedule(scheduledLoanInstallments); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java new file mode 100644 index 000000000..403aa0c97 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -0,0 +1,46 @@ +package org.mifosng.platform.loanschedule.domain; + +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.InterestMethod; + +public class DefaultLoanScheduleGeneratorFactory implements + LoanScheduleGeneratorFactory { + + @Override + public LoanScheduleGenerator create(final InterestMethod interestMethod, + AmortizationMethod amortizationMethod) { + + LoanScheduleGenerator loanScheduleGenerator = null; + + switch (amortizationMethod) { + case EQUAL_PRINCIPAL: + switch (interestMethod) { + case FLAT: + loanScheduleGenerator = new FlatLoanScheduleGenerator(); + break; + case DECLINING_BALANCE: + loanScheduleGenerator = new DecliningBalanceLoanScheduleGenerator(); + break; + case INVALID: + break; + } + break; + case EQUAL_INSTALLMENTS: + switch (interestMethod) { + case FLAT: + loanScheduleGenerator = new FlatLoanScheduleGenerator(); + break; + case DECLINING_BALANCE: + loanScheduleGenerator = new DecliningBalanceEqualInstallmentsLoanScheduleGenerator(); + break; + case INVALID: + break; + } + break; + case INVALID: + break; + } + + return loanScheduleGenerator; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java new file mode 100644 index 000000000..42d1a6d63 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java @@ -0,0 +1,97 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.util.List; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; + +public class DefaultPaymentPeriodsInOneYearCalculator implements + PaymentPeriodsInOneYearCalculator { + + @Override + public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { + + Integer paymentPeriodsInOneYear = Integer.valueOf(0); + switch (repaymentFrequencyType) { + case DAYS: + paymentPeriodsInOneYear = Integer.valueOf(365); + break; + case WEEKS: + paymentPeriodsInOneYear = Integer.valueOf(52); + break; + case MONTHS: + paymentPeriodsInOneYear = Integer.valueOf(12); + break; + case YEARS: + paymentPeriodsInOneYear = Integer.valueOf(1); + break; + case INVALID: + paymentPeriodsInOneYear = Integer.valueOf(0); + break; + } + return paymentPeriodsInOneYear; + } + + /** + * calculates the number of grace repayment periods and can be a fraction of a repayment period. + */ + @Override + public double calculateRepaymentPeriodAsAFractionOfDays( + PeriodFrequencyType repaymentPeriodFrequencyType, + Integer every, LocalDate interestCalculatedFrom, + List scheduledDates, LocalDate disbursementDate) { + + Double periodFraction = Double.valueOf("0"); + + if (interestCalculatedFrom != null && !interestCalculatedFrom.isBefore(disbursementDate)) { + Integer repaymentPeriodIndex = 0; + LocalDate lastRepaymentPeriod = disbursementDate; + for (LocalDate repaymentPeriodDueDate : scheduledDates) { + if (interestCalculatedFrom.isBefore(repaymentPeriodDueDate)) { + // fraction is + int numberOfDaysInterestCalculationGraceInPeriod = Days + .daysBetween( + lastRepaymentPeriod.toDateMidnight() + .toDateTime(), + interestCalculatedFrom.toDateMidnight() + .toDateTime()).getDays(); + periodFraction = calculateRepaymentPeriodFraction( + repaymentPeriodFrequencyType, every, + numberOfDaysInterestCalculationGraceInPeriod); + periodFraction = periodFraction + + repaymentPeriodIndex.doubleValue(); + break; + } + repaymentPeriodIndex++; + lastRepaymentPeriod = repaymentPeriodDueDate; + } + } + + return periodFraction; + } + + private double calculateRepaymentPeriodFraction(PeriodFrequencyType repaymentPeriodFrequencyType, Integer every, Integer numberOfDaysInterestCalculationGrace) { + + Double fraction = Double.valueOf("0"); + switch (repaymentPeriodFrequencyType) { + case DAYS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); + break; + case WEEKS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue()); + break; + case MONTHS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue()); + break; + case YEARS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue()); + break; + case INVALID: + fraction = Double.valueOf("0"); + break; + } + return fraction; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultScheduledDateGenerator.java new file mode 100644 index 000000000..cdc7f1439 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -0,0 +1,86 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.platform.loan.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { + + @Override + public List generate(final LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, final LocalDate disbursementDate, final LocalDate firstRepaymentPeriodDate) { + + List dueRepaymentPeriodDates = new ArrayList(loanScheduleDateInfo.getNumberOfRepayments()); + + LocalDate startDate = disbursementDate; + + for (int repaymentPeriod = 1; repaymentPeriod <= loanScheduleDateInfo.getNumberOfRepayments(); repaymentPeriod++) { + + LocalDate dueRepaymentPeriodDate = startDate; + + if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { + dueRepaymentPeriodDate = firstRepaymentPeriodDate; + } else { + switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { + case DAYS: + dueRepaymentPeriodDate = startDate.plusDays(loanScheduleDateInfo + .getRepayEvery()); + break; + case WEEKS: + dueRepaymentPeriodDate = startDate.plusWeeks(loanScheduleDateInfo + .getRepayEvery()); + break; + case MONTHS: + dueRepaymentPeriodDate = startDate.plusMonths(loanScheduleDateInfo + .getRepayEvery()); + break; + case YEARS: + dueRepaymentPeriodDate = startDate.plusYears(loanScheduleDateInfo + .getRepayEvery()); + break; + case INVALID: + break; + } + } + + dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); + startDate = dueRepaymentPeriodDate; + } + + return dueRepaymentPeriodDates; + } + + @Override + public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( + LoanProductRelatedDetail loanScheduleDateInfo, + List scheduledDates) { + + LocalDate firstRepaymentDate = scheduledDates.get(0); + + LocalDate idealDisbursementDate = null; + + switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { + case DAYS: + idealDisbursementDate = firstRepaymentDate.minusDays(loanScheduleDateInfo.getRepayEvery()); + break; + case WEEKS: + idealDisbursementDate = firstRepaymentDate.minusWeeks(loanScheduleDateInfo + .getRepayEvery()); + break; + case MONTHS: + idealDisbursementDate = firstRepaymentDate.minusMonths(loanScheduleDateInfo + .getRepayEvery()); + break; + case YEARS: + idealDisbursementDate = firstRepaymentDate.minusYears(loanScheduleDateInfo + .getRepayEvery()); + break; + case INVALID: + break; + } + + return idealDisbursementDate; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java new file mode 100644 index 000000000..dd1ab412d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/FlatLoanScheduleGenerator.java @@ -0,0 +1,96 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.MoneyData; +import org.mifosng.data.ScheduledLoanInstallment; +import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public class FlatLoanScheduleGenerator implements LoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @Override + public LoanSchedule generate( + final LoanProductRelatedDetail loanScheduleInfo, + final LocalDate disbursementDate, final LocalDate firstRepaymentDate, + final LocalDate interestCalculatedFrom, final CurrencyData currencyData) { + + List scheduledLoanInstallments = new ArrayList(); + + List scheduledDates = this.scheduledDateGenerator.generate( + loanScheduleInfo, disbursementDate, firstRepaymentDate); + + MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); + + Money principalPerInstallment = loanScheduleInfo.getPrincipal() + .dividedBy(loanScheduleInfo.getNumberOfRepayments(), + RoundingMode.HALF_EVEN); + + BigDecimal paymentPeriodsInYear = BigDecimal + .valueOf(this.paymentPeriodsInOneYearCalculator + .calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType())); + + BigDecimal periodicInterestRate = loanScheduleInfo + .getAnnualNominalInterestRate() + .divide(paymentPeriodsInYear, mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) + .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); + + Money interestPerInstallment = loanScheduleInfo.getPrincipal() + .multiplyRetainScale(periodicInterestRate, + RoundingMode.HALF_EVEN); + + Money outstandingBalance = loanScheduleInfo.getPrincipal(); + Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); + + LocalDate startDate = disbursementDate; + int installmentNumber = 1; + for (LocalDate scheduledDueDate : scheduledDates) { + totalPrincipal = totalPrincipal.plus(principalPerInstallment); + + if (installmentNumber == loanScheduleInfo.getNumberOfRepayments()) { + Money difference = totalPrincipal.minus(loanScheduleInfo + .getPrincipal()); + if (difference.isLessThanZero()) { + principalPerInstallment = principalPerInstallment + .plus(difference.abs()); + } else if (difference.isGreaterThanZero()) { + principalPerInstallment = principalPerInstallment + .minus(difference.abs()); + } + } + + Money totalInstallmentDue = principalPerInstallment.plus(interestPerInstallment); + outstandingBalance = outstandingBalance.minus(principalPerInstallment); + + MoneyData principalPerInstallmentValue = MoneyData.of(currencyData, principalPerInstallment.getAmount()); + MoneyData interestPerInstallmentValue = MoneyData.of(currencyData, interestPerInstallment.getAmount()); + MoneyData totalInstallmentDueValue = MoneyData.of(currencyData, totalInstallmentDue.getAmount()); + MoneyData outstandingBalanceValue = MoneyData.of(currencyData, outstandingBalance.getAmount()); + + ScheduledLoanInstallment installment = new ScheduledLoanInstallment( + Integer.valueOf(installmentNumber), startDate, + scheduledDueDate, principalPerInstallmentValue, + interestPerInstallmentValue, totalInstallmentDueValue, + outstandingBalanceValue); + + scheduledLoanInstallments.add(installment); + + startDate = scheduledDueDate; + + installmentNumber++; + } + + return new LoanSchedule(scheduledLoanInstallments); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java new file mode 100644 index 000000000..46b65f93a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGenerator.java @@ -0,0 +1,14 @@ +package org.mifosng.platform.loanschedule.domain; + +import org.joda.time.LocalDate; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.LoanSchedule; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public interface LoanScheduleGenerator { + + LoanSchedule generate(LoanProductRelatedDetail loanScheduleInfo, + LocalDate disbursementDate, LocalDate firstRepaymentDate, LocalDate interestCalculatedFrom, + CurrencyData currencyData); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGeneratorFactory.java new file mode 100644 index 000000000..48792e7b9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/LoanScheduleGeneratorFactory.java @@ -0,0 +1,10 @@ +package org.mifosng.platform.loanschedule.domain; + +import org.mifosng.platform.loan.domain.AmortizationMethod; +import org.mifosng.platform.loan.domain.InterestMethod; + +public interface LoanScheduleGeneratorFactory { + + LoanScheduleGenerator create(InterestMethod interestMethod, AmortizationMethod amortizationMethod); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java new file mode 100644 index 000000000..6f884aab8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java @@ -0,0 +1,17 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; + +public interface PaymentPeriodsInOneYearCalculator { + + Integer calculate(PeriodFrequencyType repaymentFrequencyType); + + double calculateRepaymentPeriodAsAFractionOfDays( + PeriodFrequencyType repaymentPeriodFrequencyType, + Integer every, LocalDate interestCalculatedFrom, + List scheduledDates, LocalDate disbursementDate); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/ScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/ScheduledDateGenerator.java new file mode 100644 index 000000000..ae440ef99 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanschedule/domain/ScheduledDateGenerator.java @@ -0,0 +1,19 @@ +package org.mifosng.platform.loanschedule.domain; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosng.platform.loan.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; +import org.mifosng.platform.loan.domain.LoanProductRelatedDetail; + +public interface ScheduledDateGenerator { + + List generate( + LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, + LocalDate disbursementDate, LocalDate firstRepaymentDate); + + LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( + LoanProductRelatedDetail loanScheduleInfo, + List scheduledDates); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetail.java b/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetail.java new file mode 100644 index 000000000..f4a0a6534 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetail.java @@ -0,0 +1,127 @@ +package org.mifosng.platform.oauthconsumer.domain; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.springframework.data.jpa.domain.AbstractPersistable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth.common.signature.SharedConsumerSecret; +import org.springframework.security.oauth.common.signature.SignatureSecret; +import org.springframework.security.oauth.provider.ResourceSpecificConsumerDetails; + +/** + * TODO - revisit when tackling better administration support for oauth consumer control with OAuth 2 implementation. + */ +@Entity +@Table(name = "admin_oauth_consumer_application") +public class OauthConsumerDetail extends AbstractPersistable implements + ResourceSpecificConsumerDetails { + + @Column(name = "app_key", nullable = false, unique = true, length=50) + private String consumerKey; + + @Column(name = "app_shared_secret", nullable = false, unique = true, length=50) + private String consumerSharedSecret; + + @Column(name = "app_name", nullable = false, unique = true, length=100) + private String consumerName; + + @SuppressWarnings("unused") + @Column(name = "app_description", nullable = true, length=500) + private String consumerDescription; + + @SuppressWarnings("unused") + @Column(name = "app_developedby", nullable = true, length=100) + private String consumerDevelopedBy; + + @SuppressWarnings("unused") + @Column(name = "app_location_url", nullable = true, length=200) + private String consumerLocationUrl; + + @Column(name = "resource_name", nullable = false) + private String resourceName; + + @Column(name = "resource_description", nullable = false) + private String resourceDescription; + + @Transient + private List authorities = new ArrayList(); + + @Transient + private boolean requiredToObtainAuthenticatedToken = true; + + // for jpa/hibernate + protected OauthConsumerDetail() { + // + } + + public OauthConsumerDetail(String consumerKey, String consumerName, String consumerDescription, String consumerDevelopedby, String consumerLocationUrl, + String resourceName, String resourceDesription) { + this.consumerKey = consumerKey; + this.consumerName = consumerName; + this.consumerDescription = consumerDescription; + this.consumerDevelopedBy = consumerDevelopedby; + this.consumerLocationUrl = consumerLocationUrl; + this.resourceName = resourceName; + resourceDescription = resourceDesription; + } + + @Override + public String getConsumerKey() { + return this.consumerKey; + } + + @Override + public String getConsumerName() { + return this.consumerName; + } + + @Override + public SignatureSecret getSignatureSecret() { + return new SharedConsumerSecret(this.consumerSharedSecret); + } + + @Override + public List getAuthorities() { + return this.authorities; + } + + @Override + public String getResourceName() { + return this.resourceName; + } + + @Override + public String getResourceDescription() { + return this.resourceDescription; + } + + /** + * Whether this consumer is required to obtain an authenticated oauth token. + * + * @return Whether this consumer is required to obtain an authenticated + * oauth token. + */ + public boolean isRequiredToObtainAuthenticatedToken() { + return requiredToObtainAuthenticatedToken; + } + + /** + * Whether this consumer is required to obtain an authenticated oauth token. + * + * @param requiredToObtainAuthenticatedToken + * Whether this consumer is required to obtain an authenticated + * oauth token. + */ + public void setRequiredToObtainAuthenticatedToken( + boolean requiredToObtainAuthenticatedToken) { + this.requiredToObtainAuthenticatedToken = requiredToObtainAuthenticatedToken; + } + + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetailRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetailRepository.java new file mode 100644 index 000000000..02b717eda --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/oauthconsumer/domain/OauthConsumerDetailRepository.java @@ -0,0 +1,11 @@ +package org.mifosng.platform.oauthconsumer.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface OauthConsumerDetailRepository extends + JpaRepository, + JpaSpecificationExecutor { + + OauthConsumerDetail findByConsumerKey(String consumerKey); +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Office.java b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Office.java new file mode 100644 index 000000000..994281e5b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Office.java @@ -0,0 +1,162 @@ +package org.mifosng.platform.organisation.domain; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang.StringUtils; +import org.joda.time.LocalDate; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "org_office", uniqueConstraints={ + @UniqueConstraint(columnNames = {"org_id", "name"}, name="name_org"), + @UniqueConstraint(columnNames = {"org_id", "external_id"}, name="externalid_org") +}) +public class Office extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "parent_id") + private final List children = new LinkedList(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private final Office parent; + + @Column(name = "name", nullable = false, length=100) + private String name; + + @Column(name = "hierarchy", nullable = false, length=50) + private String hierarchy; + + @SuppressWarnings("unused") + @Column(name = "opening_date", nullable = false) + @Temporal(TemporalType.DATE) + private Date openingDate; + + @Column(name = "external_id", length=100) + private String externalId; + + public static Office headOffice(final Organisation org, final String name, final LocalDate openingDate, final String externalId) { + return new Office(org, null, name, openingDate, externalId); + } + + public static Office createNew(Organisation organisation, Office parent, String name, LocalDate openingDate, String externalId) { + return new Office(organisation, parent, name, openingDate, externalId); + } + + protected Office() { + this.organisation = null; + this.openingDate = null; + this.parent = null; + this.name = null; + this.externalId = null; + } + + public Office(final Organisation organisation, final Office parent, final String name, final LocalDate openingDate, final String externalId) { + this.organisation = organisation; + this.parent = parent; + this.openingDate = openingDate.toDateMidnight().toDate(); + if (parent != null) { + this.parent.addChild(this); + } + + if (StringUtils.isNotBlank(name)) { + this.name = name.trim(); + } else { + this.name = null; + } + if (StringUtils.isNotBlank(externalId)) { + this.externalId = externalId.trim(); + } else { + this.externalId = null; + } + } + + private void addChild(final Office office) { + this.children.add(office); + } + + public String getName() { + return this.name; + } + + public boolean isHeadOffice() { + return this.parent == null; + } + + public void update(final String newNname, final String newExternalId, + final LocalDate newOpeningDate) { + this.name = newNname; + this.externalId = newExternalId; + this.openingDate = newOpeningDate.toDateMidnight().toDate(); + } + + public boolean identifiedBy(String identifier) { + return identifier.equalsIgnoreCase(this.name) || identifier.equalsIgnoreCase(this.externalId); + } + + public boolean identifiedBy(final Long id) { + return getId().equals(id); + } + + public boolean hasAnOfficeInHierarchyWithId(Long officeId) { + + boolean match = false; + + if (identifiedBy(officeId)) { + match = true; + } + + if (!match) { + for (Office child : this.children) { + boolean result = child.hasAnOfficeInHierarchyWithId(officeId); + + if (result) { + match = result; + break; + } + } + } + + return match; + } + + public boolean doesNotHaveAnOfficeInHierarchyWithId(Long officeId) { + return !this.hasAnOfficeInHierarchyWithId(officeId); + } + + public void generateHierarchy() { + + if (parent != null) { + this.hierarchy = this.parent.hierarchyOf(this.getId()); + } else { + this.hierarchy = "."; + } + } + + private String hierarchyOf(Long id) { + return this.hierarchy + id.toString() + "."; + } + + public String getHierarchy() { + return hierarchy; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OfficeRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OfficeRepository.java new file mode 100644 index 000000000..c98918892 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OfficeRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.organisation.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface OfficeRepository extends JpaRepository, + JpaSpecificationExecutor { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Organisation.java b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Organisation.java new file mode 100644 index 000000000..d64680431 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/Organisation.java @@ -0,0 +1,93 @@ +package org.mifosng.platform.organisation.domain; + +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.joda.time.LocalDate; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "org_organisation") +public class Organisation extends AbstractAuditableCustom { + + @Column(name = "name", nullable = false, unique=true, length=100) + private final String name; + + @Column(name = "opening_date", nullable = false) + @Temporal(TemporalType.DATE) + private final Date openingDate; + + @Column(name = "contact_email", nullable = false, length=100) + private final String contactEmail; + + @Column(name = "contact_name", nullable = false, length=100) + private final String contactName; + + @OneToMany(mappedBy = "organisation", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + private Set allowedCurrencies; + + protected Organisation() { + this.name = null; + this.openingDate = null; + this.contactEmail = null; + this.contactName = null; + this.allowedCurrencies = null; + } + + public Organisation(final String name, final LocalDate openingDate, + final String contactEmail, final String contactName, + final List currencies + ) { + this.name = name.trim(); + this.contactEmail = contactEmail.trim(); + this.contactName = contactName.trim(); + this.openingDate = openingDate.toDateMidnight().toDate(); + this.setAllowedCurrencies(new LinkedHashSet( + currencies)); + } + + public String defaultHeadOfficeName() { + return this.name + " Head Office"; + } + + public LocalDate openingDate() { + return new LocalDate(this.openingDate); + } + + public String getContactEmail() { + return this.contactEmail; + } + + public String getContactName() { + return this.contactName; + } + + public String getName() { + return this.name; + } + + public void addAllowedCurrency(final OrganisationCurrency currency) { + currency.updateOrganisation(this); + this.allowedCurrencies.add(currency); + } + + public void setAllowedCurrencies( + final Set allowedCurrencies) { + this.allowedCurrencies = allowedCurrencies; + for (OrganisationCurrency currency : allowedCurrencies) { + currency.updateOrganisation(this); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationCurrency.java b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationCurrency.java new file mode 100644 index 000000000..8d3b12040 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationCurrency.java @@ -0,0 +1,60 @@ +package org.mifosng.platform.organisation.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.user.domain.AppUser; + +@Entity +@Table(name = "org_organisation_currency") +public class OrganisationCurrency extends AbstractAuditableCustom { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private Organisation organisation; + + @SuppressWarnings("unused") + @Column(name = "code", nullable = false, length=3) + private final String code; + + @SuppressWarnings("unused") + @Column(name = "decimal_places", nullable = false) + private final Integer decimalPlaces; + + @SuppressWarnings("unused") + @Column(name = "name", nullable = false, length=50) + private final String name; + + @SuppressWarnings("unused") + @Column(name = "internationalized_name_code", nullable = false, length=50) + private final String nameCode; + + @SuppressWarnings("unused") + @Column(name = "display_symbol", nullable = true, length=10) + private final String displaySymbol; + + protected OrganisationCurrency() { + this.code = null; + this.name = null; + this.decimalPlaces = null; + this.nameCode = null; + this.displaySymbol = null; + } + + public OrganisationCurrency(final String code, final String name, final int decimalPlaces, final String nameCode, final String displaySymbol) { + this.code = code; + this.name = name; + this.decimalPlaces = decimalPlaces; + this.nameCode = nameCode; + this.displaySymbol = displaySymbol; + } + + public void updateOrganisation(final Organisation organisation) { + this.organisation = organisation; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationRepository.java new file mode 100644 index 000000000..bb4ba4522 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/organisation/domain/OrganisationRepository.java @@ -0,0 +1,7 @@ +package org.mifosng.platform.organisation.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrganisationRepository extends JpaRepository { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ApplicationConfigurationResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ApplicationConfigurationResource.java new file mode 100644 index 000000000..023ebf750 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ApplicationConfigurationResource.java @@ -0,0 +1,197 @@ +package org.mifosng.platform.rest; + +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.CurrencyData; +import org.mifosng.data.CurrencyList; +import org.mifosng.data.EnumOptionList; +import org.mifosng.data.EnumOptionReadModel; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.command.UpdateOrganisationCurrencyCommand; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/config") +@Component +@Scope("singleton") +public class ApplicationConfigurationResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("currency/allowed") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveAllowedCurrenciesForOrganisation() { + + List currencyOptions = this.readPlatformService + .retrieveAllowedCurrencies(); + + return Response.ok().entity(new CurrencyList(currencyOptions)).build(); + } + + @PUT + @Path("currency/update") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateAllowedCurrenciesForOrganisation(final UpdateOrganisationCurrencyCommand command) { + + try { + this.writePlatformService.updateOrganisationCurrencies(command); + + return Response.ok().entity(command).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("currency/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllPlatformCurrencies() { + + try { + List currencyOptions = this.readPlatformService + .retrieveAllPlatformCurrencies(); + + return Response.ok().entity(new CurrencyList(currencyOptions)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("loan/amortization/allowed") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllowedLoanAmortizationMethodOptionsForOrganisation() { + + try { + List options = this.readPlatformService + .retrieveLoanAmortizationMethodOptions(); + + return Response.ok().entity(new EnumOptionList(options)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("loan/interestcalculation/allowed") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllowedLoanInterestMethodOptionsForOrganisation() { + + try { + List options = this.readPlatformService + .retrieveLoanInterestMethodOptions(); + + return Response.ok().entity(new EnumOptionList(options)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("loan/repaymentfrequency/allowed") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllowedRepaymentFrequencyOptionsForOrganisation() { + + try { + List options = this.readPlatformService + .retrieveRepaymentFrequencyOptions(); + + return Response.ok().entity(new EnumOptionList(options)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("loan/interestfrequency/allowed") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllowedNominalInterestFrequencyOptionsForOrganisation() { + + try { + List options = this.readPlatformService.retrieveInterestFrequencyOptions(); + + return Response.ok().entity(new EnumOptionList(options)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ClientResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ClientResource.java new file mode 100644 index 000000000..e93906edd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ClientResource.java @@ -0,0 +1,232 @@ +package org.mifosng.platform.rest; + +import java.util.Arrays; +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.ClientData; +import org.mifosng.data.ClientDataWithAccountsData; +import org.mifosng.data.ClientList; +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.NoteData; +import org.mifosng.data.NoteDataList; +import org.mifosng.data.command.EnrollClientCommand; +import org.mifosng.data.command.NoteCommand; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/client") +@Component +@Scope("singleton") +public class ClientResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveAllIndividualClients() { + + try { + Collection clients = this.readPlatformService.retrieveAllIndividualClients(); + + return Response.ok().entity(new ClientList(clients)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response newClientDetails() { + + try { + ClientData clientData = this.readPlatformService.retrieveNewClientDetails(); + + return Response.ok().entity(clientData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response enrollClient(final EnrollClientCommand command) { + + try { + Long clientId = this.writePlatformService.enrollClient(command); + + return Response.ok().entity(new EntityIdentifier(clientId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{clientId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveClientData(@PathParam("clientId") final Long clientId) { + + try { + ClientData clientData = this.readPlatformService.retrieveIndividualClient(clientId); + + return Response.ok().entity(clientData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{clientId}/withaccounts") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveClientAccount(@PathParam("clientId") final Long clientId) { + + try { + ClientDataWithAccountsData clientAccount = this.readPlatformService + .retrieveClientAccountDetails(clientId); + + return Response.ok().entity(clientAccount).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("{clientId}/note/new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response addNewClientNote(@PathParam("clientId") final Long clientId, final NoteCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.addClientNote(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{clientId}/note/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveAllClientNotes(@PathParam("clientId") final Long clientId) { + + try { + Collection notes = this.readPlatformService.retrieveAllClientNotes(clientId); + + return Response.ok().entity(new NoteDataList(notes)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{clientId}/note/{noteId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveClientNote(@PathParam("clientId") final Long clientId, @PathParam("noteId") final Long noteId) { + + try { + NoteData note = this.readPlatformService.retrieveClientNote(clientId, noteId); + + return Response.ok().entity(note).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ExtraDataResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ExtraDataResource.java new file mode 100644 index 000000000..856c817e5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ExtraDataResource.java @@ -0,0 +1,128 @@ +package org.mifosng.platform.rest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.ExtraDatasets; +import org.mifosng.data.reports.GenericResultset; +import org.mifosng.platform.InvalidSqlException; +import org.mifosng.platform.ReadPlatformService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/protected/extradata") +@Component +@Scope("singleton") +public class ExtraDataResource { + + private final static Logger logger = LoggerFactory + .getLogger(ExtraDataResource.class); + + @Autowired + private ReadPlatformService readPlatformService; + +// @GET +// @Path("{dataSetPrefix}") +// @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) +// @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) +// public Response extraDataNames(@PathParam("dataSetPrefix") final String dataSetPrefix) { +// +// List result = this.readPlatformService.retrieveExtraDataTableNames(dataSetPrefix); +// +// return Response.ok().entity(result).build(); +// } + + @GET + @Path("datasets/{datasetType}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response datasets(@PathParam("datasetType") final String datasetType, @Context UriInfo uriInfo) { + + //MultivaluedMap queryParams = uriInfo.getQueryParameters(); + //String tableName = queryParams.getFirst("tableName"); + + ExtraDatasets result = this.readPlatformService.retrieveExtraDatasetNames(datasetType); + return Response.ok().entity(result).build(); + } + + @GET + @Path("{datasetType}/{datasetName}/{datasetPKValue}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response extraData(@PathParam("datasetType") final String datasetType,@PathParam("datasetName") final String datasetName, @PathParam("datasetPKValue") final String datasetPKValue, @Context UriInfo uriInfo) { + + try { + GenericResultset result = this.readPlatformService.retrieveExtraData(datasetType, datasetName, datasetPKValue); + + return Response.ok().entity(result).build(); + } catch (InvalidSqlException e) { + List allErrors = new ArrayList(); + + ErrorResponse err = new ErrorResponse("extradata.invalid.sql", "sql", e.getSql()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } + } + + @POST + @Path("{datasetType}/{datasetName}/{datasetPKValue}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response saveExtraData(@PathParam("datasetType") final String datasetType,@PathParam("datasetName") final String datasetName, @PathParam("datasetPKValue") final String datasetPKValue, @Context UriInfo uriInfo) { + + try { + + MultivaluedMap incomingParams = uriInfo.getQueryParameters(); + Map queryParams = new HashMap(); + + Set keys = incomingParams.keySet(); + for (String key : keys) { + String pValue = incomingParams.get(key).get(0); + queryParams.put(key, pValue); + logger.info(key + " - " + pValue); + } + + + this.readPlatformService.tempSaveExtraData(datasetType, datasetName, datasetPKValue, queryParams); + + EntityIdentifier entityIdentifier = new EntityIdentifier(Long.valueOf(datasetPKValue)); + + return Response.ok().entity(entityIdentifier).build(); + } catch (InvalidSqlException e) { + List allErrors = new ArrayList(); + + ErrorResponse err = new ErrorResponse("extradata.invalid.sql", "sql", e.getSql()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/FlexibleReportingResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/FlexibleReportingResource.java new file mode 100644 index 000000000..6b4b089cf --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/FlexibleReportingResource.java @@ -0,0 +1,113 @@ +package org.mifosng.platform.rest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.mifosng.data.ExtraDatasets; +import org.mifosng.data.reports.GenericResultset; +import org.mifosng.platform.ReadPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import com.sun.jersey.api.json.JSONWithPadding; + +/** + * TODO - remove open reporting from application + * @deprecated - remove from application + */ +@Deprecated +@Path("/open/reporting") +@Component +@Scope("singleton") +public class FlexibleReportingResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @GET + @Path("flexireportxxx") + @Consumes({ "application/x-javascript" }) + @Produces({ "application/x-javascript" }) + public Response retrieveReport( + @QueryParam("callback") @DefaultValue("callback") final String callbackName, @Context UriInfo uriInfo) { + + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String rptDB = queryParams.getFirst("MRP_rptDB"); + String name = queryParams.getFirst("MRP_Name"); + String type = queryParams.getFirst("MRP_Type"); + + Map extractedQueryParams = new HashMap(); + + Set keys = queryParams.keySet(); + String pKey; + String pValue; + for (String k : keys) { + + if (k.startsWith("MRP_")) + { + pKey = "${" + k.substring(4) + "}"; + pValue = queryParams.get(k).get(0); + + extractedQueryParams.put(pKey, pValue); + } + } + + GenericResultset result = this.readPlatformService.retrieveGenericResultset(rptDB, name, type, extractedQueryParams); + + JSONWithPadding paddedResult = new JSONWithPadding(result, + callbackName); + + return Response.ok().entity(paddedResult).build(); + } + + + @GET + @Path("extradatanamesxxx") + @Consumes({ "application/x-javascript" }) + @Produces({ "application/x-javascript" }) + public Response extraDataNames( + @QueryParam("callback") @DefaultValue("callback") final String callbackName, @Context UriInfo uriInfo) { + + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String tableName = queryParams.getFirst("tableName"); + + ExtraDatasets result = this.readPlatformService.retrieveExtraDatasetNames(tableName); + + JSONWithPadding paddedResult = new JSONWithPadding(result, + callbackName); + + return Response.ok().entity(paddedResult).build(); + } + + @GET + @Path("extradataxxx") + @Consumes({ "application/x-javascript" }) + @Produces({ "application/x-javascript" }) + public Response extraData( + @QueryParam("callback") @DefaultValue("callback") final String callbackName, @Context UriInfo uriInfo) { + + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String extraDataTableName = queryParams.getFirst("extraDataTableName"); + String extraDataTableId = queryParams.getFirst("extraDataTableId"); + + GenericResultset result = this.readPlatformService.retrieveExtraData(extraDataTableName, extraDataTableName, extraDataTableId); + + JSONWithPadding paddedResult = new JSONWithPadding(result, + callbackName); + + return Response.ok().entity(paddedResult).build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportResource.java new file mode 100644 index 000000000..545d8da46 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportResource.java @@ -0,0 +1,81 @@ +package org.mifosng.platform.rest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.command.ImportClientCommand; +import org.mifosng.data.command.ImportLoanCommand; +import org.mifosng.data.command.ImportLoanRepaymentsCommand; +import org.mifosng.platform.ImportPlatformService; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/protected/import") +@Component +@Scope("singleton") +public class ImportResource { + + @Autowired + private ImportPlatformService importPlatformService; + + @POST + @Path("clients") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response importClients(final ImportClientCommand command) { + try { + this.importPlatformService.importClients(command); + + return Response.ok().build(); + } catch (ClientNotAuthenticatedException e) { + ErrorResponse err = new ErrorResponse("client.not.authenticated", + "authentication", ""); + throw new WebApplicationException(Response + .status(Status.UNAUTHORIZED).entity(err).build()); + } + } + + @POST + @Path("loans") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response importLoans(final ImportLoanCommand command) { + try { + this.importPlatformService.importLoans(command); + + return Response.ok().build(); + } catch (ClientNotAuthenticatedException e) { + ErrorResponse err = new ErrorResponse("client.not.authenticated", + "authentication", ""); + throw new WebApplicationException(Response + .status(Status.UNAUTHORIZED).entity(err).build()); + } + } + + @POST + @Path("loanrepayments") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response importLoanRepayments(final ImportLoanRepaymentsCommand command) { + try { + this.importPlatformService.importLoanRepayments(command); + + return Response.ok().build(); + } catch (ClientNotAuthenticatedException e) { + ErrorResponse err = new ErrorResponse("client.not.authenticated", + "authentication", ""); + throw new WebApplicationException(Response + .status(Status.UNAUTHORIZED).entity(err).build()); + } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportTriggerResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportTriggerResource.java new file mode 100644 index 000000000..881ebfb90 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ImportTriggerResource.java @@ -0,0 +1,60 @@ +package org.mifosng.platform.rest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.mifosng.data.command.ImportClientCommand; +import org.mifosng.data.command.ImportLoanCommand; +import org.mifosng.data.command.ImportLoanRepaymentsCommand; +import org.mifosng.platform.ImportPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/protected/import/trigger") +@Component +@Scope("singleton") +public class ImportTriggerResource { + + @Autowired + private ImportPlatformService importPlatformService; + + @Autowired + private ImportResource importResource; + + // TODO - REMOVE THIS HARDCODED IMPORT OF CREOCORE CSV FILE + @GET + @Path("clients/creocore") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response triggerCreoCoreClientImport() { + + ImportClientCommand command = this.importPlatformService.populateClientImportFromCsv(); + return importResource.importClients(command); + } + + // TODO - REMOVE THIS HARDCODED IMPORT OF CREOCORE CSV FILE + @GET + @Path("loans/creocore") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response triggerCreoCoreLoanImport() { + + ImportLoanCommand command = this.importPlatformService.populateLoanImportFromCsv(); + return importResource.importLoans(command); + } + + @GET + @Path("repayments/creocore") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response triggerCreoCoreLoanRepaymentsImport() { + + ImportLoanRepaymentsCommand command = this.importPlatformService.populateLoanRepaymentsImportFromCsv(); + return importResource.importLoanRepayments(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanOpenResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanOpenResource.java new file mode 100644 index 000000000..ff50a1e2b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanOpenResource.java @@ -0,0 +1,104 @@ +package org.mifosng.platform.rest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.mifosng.data.LoanPayoffReadModel; +import org.mifosng.data.MoneyData; +import org.mifosng.platform.CalculationPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import com.sun.jersey.api.json.JSONWithPadding; + +@Path("/open/loan/{loanId}") +@Component +@Scope("singleton") +public class LoanOpenResource { + + @Autowired + private CalculationPlatformService calculationPlatformService; + + @GET + @Path("calculatepayoff/{payoffDate}") + @Consumes({ "application/x-javascript" }) + @Produces({ "application/x-javascript" }) + public Response calculatePayoff( + @QueryParam("callback") @DefaultValue("callback") final String callbackName, + @PathParam("loanId") final String loanId, + @PathParam("payoffDate") final String payoffDate) { + + try { + + DateTimeFormatter formatter = ISODateTimeFormat.dateParser(); + DateTime payoffDateTime = formatter.parseDateTime(payoffDate); + + LoanPayoffReadModel loanPayoffInformation = this.calculationPlatformService + .calculatePayoffOn(Long.valueOf(loanId), + payoffDateTime.toLocalDate()); + + JSONObject payoffInfo = new JSONObject(); + payoffInfo.put("reference", loanPayoffInformation.getReference()); + payoffInfo.put("acutalDisbursementDate", + loanPayoffInformation.getAcutalDisbursementDate()); + payoffInfo.put("expectedMaturityDate", + loanPayoffInformation.getExpectedMaturityDate()); + payoffInfo.put("projectedMaturityDate", + loanPayoffInformation.getProjectedMaturityDate()); + payoffInfo.put("expectedLoanTermInDays", + loanPayoffInformation.getExpectedLoanTermInDays()); + payoffInfo.put("projectedLoanTermInDays", + loanPayoffInformation.getProjectedLoanTermInDays()); + + JSONObject totalPaidToDate = jsonifyMoneyReadModel(loanPayoffInformation + .getTotalPaidToDate()); + payoffInfo.put("totalPaidToDate", totalPaidToDate); + + JSONObject totalOutstandingBasedOnExpectedMaturityDate = jsonifyMoneyReadModel(loanPayoffInformation + .getTotalOutstandingBasedOnExpectedMaturityDate()); + payoffInfo.put("totalOutstandingBasedOnExpectedMaturityDate", + totalOutstandingBasedOnExpectedMaturityDate); + + JSONObject totalOutstandingBasedOnPayoffDate = jsonifyMoneyReadModel(loanPayoffInformation + .getTotalOutstandingBasedOnPayoffDate()); + payoffInfo.put("totalOutstandingBasedOnPayoffDate", + totalOutstandingBasedOnPayoffDate); + + JSONObject interestRebateOwed = jsonifyMoneyReadModel(loanPayoffInformation + .getInterestRebateOwed()); + payoffInfo.put("interestRebateOwed", interestRebateOwed); + + JSONWithPadding payoff = new JSONWithPadding(payoffInfo, + callbackName); + + return Response.ok(payoff).build(); + } catch (JSONException e) { + return Response.status(Status.BAD_REQUEST).build(); + } + } + + private JSONObject jsonifyMoneyReadModel(final MoneyData money) + throws JSONException { + + JSONObject object = new JSONObject(); + object.put("amount", money.getAmount()); + object.put("currencyCode", money.getCurrencyCode()); + object.put("currencyDigitsAfterDecimal", + money.getCurrencyDigitsAfterDecimal()); + + return object; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanProductResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanProductResource.java new file mode 100644 index 000000000..e142e148c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanProductResource.java @@ -0,0 +1,147 @@ +package org.mifosng.platform.rest; + +import java.util.Arrays; +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.LoanProductData; +import org.mifosng.data.LoanProductList; +import org.mifosng.data.command.CreateLoanProductCommand; +import org.mifosng.data.command.UpdateLoanProductCommand; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/product/loan") +@Component +@Scope("singleton") +public class LoanProductResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveAllLoanProducts() { + + Collection products = this.readPlatformService + .retrieveAllLoanProducts(); + + return Response.ok().entity(new LoanProductList(products)).build(); + } + + @POST + @Path("new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response createLoanProduct(final CreateLoanProductCommand command) { + + try { + EntityIdentifier entityIdentifier = this.writePlatformService.createLoanProduct(command); + + return Response.ok().entity(entityIdentifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("update") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateLoanProduct(final UpdateLoanProductCommand command) { + + try { + EntityIdentifier entityIdentifier = this.writePlatformService.updateLoanProduct(command); + return Response.ok().entity(entityIdentifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{productId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveLoanProductDetails(@PathParam("productId") final Long productId) { + + try { + LoanProductData loanProduct = this.readPlatformService.retrieveLoanProduct(productId); + + return Response.ok().entity(loanProduct).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("empty") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveNewLoanProductDetails() { + + try { + LoanProductData loanProduct = this.readPlatformService.retrieveNewLoanProductDetails(); + + return Response.ok().entity(loanProduct).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanResource.java new file mode 100644 index 000000000..bf9305dcd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/LoanResource.java @@ -0,0 +1,526 @@ +package org.mifosng.platform.rest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.LoanAccountData; +import org.mifosng.data.LoanRepaymentData; +import org.mifosng.data.LoanSchedule; +import org.mifosng.data.NewLoanWorkflowStepOneData; +import org.mifosng.data.command.AdjustLoanTransactionCommand; +import org.mifosng.data.command.CalculateLoanScheduleCommand; +import org.mifosng.data.command.LoanStateTransitionCommand; +import org.mifosng.data.command.LoanTransactionCommand; +import org.mifosng.data.command.SubmitLoanApplicationCommand; +import org.mifosng.data.command.UndoLoanApprovalCommand; +import org.mifosng.data.command.UndoLoanDisbursalCommand; +import org.mifosng.platform.CalculationPlatformService; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.ReadPlatformServiceImpl; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/loan") +@Component +@Scope("singleton") +public class LoanResource { + + private final static Logger logger = LoggerFactory.getLogger(ReadPlatformServiceImpl.class); + + @Autowired + private CalculationPlatformService calculationPlatformService; + + @Autowired + private ReadPlatformService readPlatformService; + + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("new/{clientId}/workflow/one") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveDetailsForNewLoanApplicationStepOne(@PathParam("clientId") final Long clientId) { + + try { + NewLoanWorkflowStepOneData workflowData = this.readPlatformService.retrieveClientAndProductDetails(clientId, null); + + return Response.ok().entity(workflowData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("new/{clientId}/product/{productId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveDetailsForNewLoanApplicationStepOne(@PathParam("clientId") final Long clientId, @PathParam("productId") final Long productId) { + + try { + NewLoanWorkflowStepOneData workflowData = this.readPlatformService.retrieveClientAndProductDetails(clientId, productId); + + return Response.ok().entity(workflowData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("calculate") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response calculateLoanSchedule(final CalculateLoanScheduleCommand command) { + + try { + LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command); + + return Response.ok().entity(loanSchedule).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response submitLoanApplication(final SubmitLoanApplicationCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.submitLoanApplication(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{loanId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveLoanAccountDetails(@PathParam("loanId") final Long loanId) { + + try { + LoanAccountData loanAccount = this.readPlatformService.retrieveLoanAccountDetails(loanId); + + return Response.ok().entity(loanAccount).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("unknown.error", "error", e.getMessage()); + allErrors.add(err); + + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(allErrors)).build()); + } + } + + @DELETE + @Path("{loanId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response deleteLoanApplication(@PathParam("loanId") final Long loanId) { + + try { + EntityIdentifier identifier = this.writePlatformService.deleteLoan(loanId); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("unknown.error", "error", e.getMessage()); + allErrors.add(err); + + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(allErrors)).build()); + } + } + + @POST + @Path("approve") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response approveLoanApplication(final LoanStateTransitionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.approveLoanApplication(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("undoapproval") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response undoLoanApproval(final UndoLoanApprovalCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.undoLoanApproval(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("reject") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response rejectLoanApplication(final LoanStateTransitionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.rejectLoan(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("withdraw") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response withdrawLoanApplication(final LoanStateTransitionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.withdrawLoan(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("disburse") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response disburseLoanApplication(final LoanStateTransitionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.disburseLoan(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("undodisbursal") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response undoLoanDisbursal(final UndoLoanDisbursalCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.undloLoanDisbursal(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{loanId}/repayment") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveNewRepaymentDetails(@PathParam("loanId") final Long loanId) { + + try { + LoanRepaymentData loanRepaymentData = this.readPlatformService.retrieveNewLoanRepaymentDetails(loanId); + + return Response.ok().entity(loanRepaymentData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{loanId}/repayment/{repaymentId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveRepaymentDetails(@PathParam("loanId") final Long loanId, @PathParam("repaymentId") final Long repaymentId) { + + try { + LoanRepaymentData loanRepaymentData = this.readPlatformService.retrieveLoanRepaymentDetails(loanId, repaymentId); + + return Response.ok().entity(loanRepaymentData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("repayment") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response makeLoanRepayment(final LoanTransactionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.makeLoanRepayment(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("unknown.error", "error", + e.getMessage()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } + } + + @POST + @Path("repayment/adjust") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response adjustLoanTransaction(final AdjustLoanTransactionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.adjustLoanTransaction(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("unknown.error", "error", + e.getMessage()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } + } + + @GET + @Path("{loanId}/waive") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveNewWaiverDetails(@PathParam("loanId") final Long loanId) { + + try { + LoanRepaymentData loanRepaymentData = this.readPlatformService.retrieveNewLoanWaiverDetails(loanId); + + return Response.ok().entity(loanRepaymentData).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("waive") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response waiveLoanAmount(final LoanTransactionCommand command) { + + try { + EntityIdentifier identifier = this.writePlatformService.waiveLoanAmount(command); + + return Response.ok().entity(identifier).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("unknown.error", "error", + e.getMessage()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/NoteResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/NoteResource.java new file mode 100644 index 000000000..312422997 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/NoteResource.java @@ -0,0 +1,36 @@ +package org.mifosng.platform.rest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.command.NoteCommand; +import org.mifosng.platform.WritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/protected/note") +@Component +@Scope("singleton") +public class NoteResource { + + @Autowired + private WritePlatformService writePlatformService; + + @POST + @Path("{noteId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateClientAccount(@PathParam("noteId") final Long noteId, final NoteCommand command) { + + EntityIdentifier identifier = this.writePlatformService.updateNote(command); + + return Response.ok().entity(identifier).build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/OfficeResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/OfficeResource.java new file mode 100644 index 000000000..7e558b5b0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/OfficeResource.java @@ -0,0 +1,135 @@ +package org.mifosng.platform.rest; + +import java.util.Arrays; +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.OfficeData; +import org.mifosng.data.OfficeList; +import org.mifosng.data.command.OfficeCommand; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/office") +@Component +@Scope("singleton") +public class OfficeResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveOffices() { + + try { + Collection offices = this.readPlatformService.retrieveAllOffices(); + + return Response.ok().entity(new OfficeList(offices)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("{officeId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retreiveOffice(@PathParam("officeId") final Long officeId) { + + try { + OfficeData office = this.readPlatformService.retrieveOffice(officeId); + + return Response.ok().entity(office).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response createOffice(final OfficeCommand command) { + + try { + Long officeId = this.writePlatformService.createOffice(command); + + return Response.ok().entity(new EntityIdentifier(officeId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("update") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateOffice(final OfficeCommand command) { + + try { + Long officeId = this.writePlatformService.updateOffice(command); + + return Response.ok().entity(new EntityIdentifier(officeId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/ProtectedFlexibleReportingResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ProtectedFlexibleReportingResource.java new file mode 100644 index 000000000..6f3af0ea3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/ProtectedFlexibleReportingResource.java @@ -0,0 +1,155 @@ +package org.mifosng.platform.rest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.reports.GenericResultset; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/reporting") +@Component +@Scope("singleton") +public class ProtectedFlexibleReportingResource { + + private final static Logger logger = LoggerFactory.getLogger(ProtectedFlexibleReportingResource.class); + + @Autowired + private ReadPlatformService readPlatformService; + + private String _corsHeaders; + + private Response makeCORS(ResponseBuilder req, String returnMethod) { + ResponseBuilder rb = req + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + + if (!"".equals(returnMethod)) { + rb.header("Access-Control-Allow-Headers", returnMethod); + } + + return rb.build(); + } + + private Response makeCORS(ResponseBuilder req) { + return makeCORS(req, _corsHeaders); + } + + // This OPTIONS request/response is necessary + // if you consumes other format than text/plain or + // if you use other HTTP verbs than GET and POST + @OPTIONS + @Path("/flexireport") + public Response corsMyResource(@HeaderParam("Access-Control-Request-Headers") String requestH) { + _corsHeaders = requestH; + return makeCORS(Response.ok(), requestH); + } + + @GET + @Path("flexireport") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveReport(@Context UriInfo uriInfo, @Context HttpServletResponse httpServletResponse) { + + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String rptDB = queryParams.getFirst("MRP_rptDB"); + String name = queryParams.getFirst("MRP_Name"); + String type = queryParams.getFirst("MRP_Type"); + + Map extractedQueryParams = new HashMap(); + + Set keys = queryParams.keySet(); + String pKey; + String pValue; + for (String k : keys) { + + if (k.startsWith("MRP_")) + { + pKey = "${" + k.substring(4) + "}"; + pValue = queryParams.get(k).get(0); + + extractedQueryParams.put(pKey, pValue); + } + } + logger.info("BEGINNING REQUEST FOR: " + name); + GenericResultset result = this.readPlatformService.retrieveGenericResultset(rptDB, name, type, extractedQueryParams); + +// JSONWithPadding paddedResult = new JSONWithPadding(result, +// callbackName); + + return makeCORS(Response.ok().entity(result)); + } + + @GET + @Path("/exportcsv/{reportDb}/{reportName}/{reportType}/office/{officeId}/currency/{currencyId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response exportCsvReport( + @PathParam("reportDb") String rptDB, + @PathParam("reportName") String name, + @PathParam("reportType") String type, + @PathParam("officeId") Long officeId, @PathParam("currencyId") Long currencyId) throws IOException { + + try { + Map extractedQueryParams = new HashMap(); + + if (officeId != null && officeId > 0) { + extractedQueryParams.put("${officeId}", officeId.toString()); + } + + if (currencyId != null) { + extractedQueryParams.put("${currencyId}", currencyId.toString()); + } + + GenericResultset result = this.readPlatformService.retrieveGenericResultset(rptDB, name, type, extractedQueryParams); + + return Response.ok().entity(result).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("forceauth") + public Response hackToForceAuthentication() { + return Response.ok().build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/TenantResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/TenantResource.java new file mode 100644 index 000000000..3ee0a2c6a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/TenantResource.java @@ -0,0 +1,38 @@ +package org.mifosng.platform.rest; + +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.mifosng.data.OrganisationList; +import org.mifosng.data.OrganisationReadModel; +import org.mifosng.platform.ReadPlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/protected/org") +@Component +@Scope("singleton") +public class TenantResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @GET + @Path("view") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveExistingOrganisations() { + + Collection organisations = this.readPlatformService + .retrieveAll(); + return Response.ok().entity(new OrganisationList(organisations)) + .build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserAdmistrationResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserAdmistrationResource.java new file mode 100644 index 000000000..0dba773bc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserAdmistrationResource.java @@ -0,0 +1,456 @@ +package org.mifosng.platform.rest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.mifosng.data.AppUserData; +import org.mifosng.data.EntityIdentifier; +import org.mifosng.data.EnumOptionList; +import org.mifosng.data.EnumOptionReadModel; +import org.mifosng.data.ErrorResponse; +import org.mifosng.data.ErrorResponseList; +import org.mifosng.data.PermissionData; +import org.mifosng.data.PermissionList; +import org.mifosng.data.RoleData; +import org.mifosng.data.RoleList; +import org.mifosng.data.UserList; +import org.mifosng.data.command.ChangePasswordCommand; +import org.mifosng.data.command.RoleCommand; +import org.mifosng.data.command.UserCommand; +import org.mifosng.platform.ReadPlatformService; +import org.mifosng.platform.WritePlatformService; +import org.mifosng.platform.exceptions.ApplicationDomainRuleException; +import org.mifosng.platform.exceptions.ClientNotAuthenticatedException; +import org.mifosng.platform.exceptions.NewDataValidationException; +import org.mifosng.platform.infrastructure.PlatformEmailSendException; +import org.mifosng.platform.infrastructure.UsernameAlreadyExistsException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; + +@Path("/protected/admin") +@Component +@Scope("singleton") +public class UserAdmistrationResource { + + @Autowired + private ReadPlatformService readPlatformService; + + @Autowired + private WritePlatformService writePlatformService; + + @GET + @Path("user/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveUsers() { + + try { + Collection users = this.readPlatformService.retrieveAllUsers(); + + return Response.ok().entity(new UserList(users)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("user/new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response newUserDetails() { + + try { + AppUserData newUser = this.readPlatformService.retrieveNewUserDetails(); + + return Response.ok().entity(newUser).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("user/new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response createUser(final UserCommand command) { + + try { + Long userId = this.writePlatformService.createUser(command); + return Response.ok().entity(new EntityIdentifier(userId)).build(); + } catch (UsernameAlreadyExistsException e) { + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("validation.msg.username.already.exists.in.organisation", "username", command.getUsername()); + allErrors.add(err); + + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } catch (PlatformEmailSendException e) { + List allErrors = new ArrayList(); + ErrorResponse err = new ErrorResponse("error.msg.user.email.invalid","email", "Email is invalid."); + allErrors.add(err); + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(new ErrorResponseList(allErrors)).build()); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @DELETE + @Path("user/{userId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response deleteUser(@PathParam("userId") final Long userId) { + + try { + this.writePlatformService.deleteUser(userId); + + return Response.ok().build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("user/{userId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveUser(@PathParam("userId") final Long userId) { + + try { + AppUserData user = this.readPlatformService.retrieveUser(userId); + + return Response.ok().entity(user).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("user/{userId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateUser(UserCommand command) { + + try { + Long userId = this.writePlatformService.updateUser(command); + + return Response.ok().entity(new EntityIdentifier(userId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("user/current") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveCurrentUser() { + + try { + AppUserData user = this.readPlatformService.retrieveCurrentUser(); + + return Response.ok().entity(user).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("user/current") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateCurrentUser(UserCommand command) { + + try { + Long userId = this.writePlatformService.updateCurrentUser(command); + + return Response.ok().entity(new EntityIdentifier(userId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } catch (DataIntegrityViolationException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.username.already.exists.in.organisation", "username", command.getUsername()); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } + } + + @POST + @Path("user/current/password") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateCurrentUserPassword(ChangePasswordCommand command) { + + try { + Long userId = this.writePlatformService.updateCurrentUserPassword(command); + + return Response.ok().entity(new EntityIdentifier(userId)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("role/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveAllRoles() { + + try { + Collection roles = this.readPlatformService.retrieveAllRoles(); + return Response.ok().entity(new RoleList(roles)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("role/new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveNewRoleDetails() { + + try { + Collection allPermissions = this.readPlatformService.retrieveAllPermissions(); + + RoleData role = new RoleData(); + role.setAvailablePermissions(allPermissions); + + return Response.ok().entity(role).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("role/{roleId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response retrieveRole(@PathParam("roleId") final Long roleId) { + + try { + RoleData role = this.readPlatformService.retrieveRole(roleId); + + Collection availablePermissions = this.readPlatformService.retrieveAllPermissions(); + + availablePermissions.removeAll(role.getSelectedPermissions()); + + role.setAvailablePermissions(availablePermissions); + + return Response.ok().entity(role).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + + @POST + @Path("role/{roleId}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response updateRole(@PathParam("roleId") final Long roleId, final RoleCommand command) { + + try { + this.writePlatformService.updateRole(command); + return Response.ok().entity(new EntityIdentifier(roleId)).build(); + + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @POST + @Path("role/new") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response createRole(final RoleCommand command) { + + try { + Long roleId = this.writePlatformService.createRole(command); + return Response.ok().entity(new EntityIdentifier(roleId)).build(); + + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("permissiongroup/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrievePermissionGroups() { + + try { + Collection options = this.readPlatformService + .retrieveAllPermissionGroups(); + + return Response + .ok() + .entity(new EnumOptionList(new ArrayList( + options))).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } + + @GET + @Path("permissions/all") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrievePermissions() { + + try { + Collection permissions = this.readPlatformService.retrieveAllPermissions(); + + return Response.ok().entity(new PermissionList(permissions)).build(); + } catch (ClientNotAuthenticatedException e) { + throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).build()); + } catch (AccessDeniedException e) { + ErrorResponse errorResponse = new ErrorResponse("error.msg.no.permission", "id"); + ErrorResponseList list = new ErrorResponseList(Arrays.asList(errorResponse)); + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(list).build()); + } catch (ApplicationDomainRuleException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getErrors())).build()); + } catch (NewDataValidationException e) { + throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(new ErrorResponseList(e.getValidationErrors())).build()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserResource.java new file mode 100644 index 000000000..102bde537 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/rest/UserResource.java @@ -0,0 +1,85 @@ +package org.mifosng.platform.rest; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.mifosng.data.AuthenticatedUserData; +import org.mifosng.oauth.CustomOAuthProviderTokenServices; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth.provider.token.OAuthProviderTokenImpl; +import org.springframework.stereotype.Component; + +@Path("/protected/user") +@Component +@Scope("singleton") +public class UserResource { + + @Autowired + private CustomOAuthProviderTokenServices oauthProviderTokenServices; + + @GET + @Path("{accessToken}") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response retrieveClientAccount( + @PathParam("accessToken") final String oauthToken) { + + OAuthProviderTokenImpl token = (OAuthProviderTokenImpl) this.oauthProviderTokenServices.getTokenByNonEncodedKey(oauthToken); + if (token == null) { + token = (OAuthProviderTokenImpl) this.oauthProviderTokenServices.getToken(oauthToken); + } + + Authentication auth = null; + if (token != null && token.isAccessToken()) { + auth = token.getUserAuthentication(); + } + + Collection permissions = new ArrayList(); + if (auth != null) { + for (GrantedAuthority authority : auth.getAuthorities()) { + permissions.add(authority.getAuthority()); + } + } + AuthenticatedUserData authenticatedUserData = new AuthenticatedUserData(permissions); + if (auth != null) { + authenticatedUserData.setUsername(((UserDetails)auth.getPrincipal()).getUsername()); + } + return Response.ok().entity(authenticatedUserData).build(); + } + + @GET + @Path("{accessToken}/signout") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) + public Response userSignOut(@PathParam("accessToken") final String oauthToken, @Context HttpServletRequest request) { + + this.oauthProviderTokenServices.removeTokenByNonEncodedKey(oauthToken); + + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(null); + + return Response.ok().build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/security/AccessConfirmationController.java b/mifosng-provider/src/main/java/org/mifosng/platform/security/AccessConfirmationController.java new file mode 100644 index 000000000..9c5776ecb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/security/AccessConfirmationController.java @@ -0,0 +1,85 @@ +package org.mifosng.platform.security; + +import java.util.Map; +import java.util.TreeMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mifosng.oauth.CustomOAuthProviderTokenServices; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth.common.OAuthConsumerParameter; +import org.springframework.security.oauth.provider.ConsumerDetails; +import org.springframework.security.oauth.provider.ConsumerDetailsService; +import org.springframework.security.oauth.provider.OAuthProviderSupport; +import org.springframework.security.oauth.provider.filter.CoreOAuthProviderSupport; +import org.springframework.security.oauth.provider.token.OAuthProviderToken; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class AccessConfirmationController { + + @Autowired + private CustomOAuthProviderTokenServices tokenServices; + + @Autowired + private ConsumerDetailsService consumerDetailsService; + + private OAuthProviderSupport providerSupport = new CoreOAuthProviderSupport(); + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @RequestMapping("/oauth/confirm_access") + public ModelAndView getAccessConfirmation(final HttpServletRequest request, + final HttpServletResponse response) throws Exception { + + Map oauthParams = this.providerSupport.parseParameters(request); + String oauthToken = retrieveOAuthToken(oauthParams); + if (oauthToken == null) { + throw new IllegalArgumentException( + "A request token to authorize must be provided."); + } + + OAuthProviderToken providerToken = this.tokenServices.getToken(oauthToken); + ConsumerDetails consumer = this.consumerDetailsService + .loadConsumerByConsumerKey(providerToken.getConsumerKey()); + + String callback = retrieveCallbackurl(oauthParams); + TreeMap model = new TreeMap(); + model.put(OAuthConsumerParameter.oauth_token.toString(), oauthToken); + if (callback != null) { + model.put(OAuthConsumerParameter.oauth_callback.toString(), callback); + } + model.put("consumer", consumer); + + // check if user is fully authenticated here and if not redirect to login page passing oauth token info etc + SecurityContext context = SecurityContextHolder.getContext(); + if (context.getAuthentication() instanceof AnonymousAuthenticationToken) { + model.put("successUrl", "/oauth/confirm_access"); + + return new ModelAndView("login", model); + } + + if ("mifosng-ui-consumer-key".equalsIgnoreCase(consumer.getConsumerKey())) { + response.sendRedirect("authorize?requestToken=" + oauthToken); + return null; + } else { + redirectStrategy.sendRedirect(request, response, "access_confirmation"); + return new ModelAndView("access_confirmation", model); + } + } + + private String retrieveOAuthToken(Map oauthParams) { + return oauthParams.get(OAuthConsumerParameter.oauth_token.toString()); + } + + private String retrieveCallbackurl(Map oauthParams) { + return oauthParams.get(OAuthConsumerParameter.oauth_callback.toString()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsService.java b/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsService.java new file mode 100644 index 000000000..a26f6a29d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsService.java @@ -0,0 +1,10 @@ +package org.mifosng.platform.security; + +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * Interface to hide implementation detail of spring security. + */ +public interface PlatformUserDetailsService extends UserDetailsService { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsServiceImpl.java new file mode 100644 index 000000000..20b897a20 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/security/PlatformUserDetailsServiceImpl.java @@ -0,0 +1,24 @@ +package org.mifosng.platform.security; + +import org.mifosng.platform.infrastructure.PlatformUser; +import org.mifosng.platform.user.domain.PlatformUserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +public class PlatformUserDetailsServiceImpl implements PlatformUserDetailsService { + + @Autowired + private PlatformUserRepository platformUserRepository; + + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException { + + PlatformUser appUser = this.platformUserRepository.findByUsername(username); + + if (appUser == null) { throw new UsernameNotFoundException(username + ": not found"); } + + return appUser; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java new file mode 100644 index 000000000..c12fea14e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java @@ -0,0 +1,308 @@ +package org.mifosng.platform.user.domain; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.infrastructure.PlatformUser; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.Organisation; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +@Entity +@Table(name = "admin_appuser", uniqueConstraints=@UniqueConstraint(columnNames = {"org_id", "username"})) +public class AppUser extends AbstractAuditableCustom implements PlatformUser { + + @Column(name = "email", nullable=false, length=100) + private String email; + + @Column(name = "username", nullable=false, length=100) + private String username; + + @Column(name = "firstname", nullable=false, length=100) + private String firstname; + + @Column(name = "lastname", nullable=false, length=100) + private String lastname; + + @Column(name = "password", nullable=false) + private String password; + + @Column(name = "nonexpired", nullable=false) + private final boolean accountNonExpired; + + @Column(name = "nonlocked", nullable=false) + private final boolean accountNonLocked; + + @Column(name = "nonexpired_credentials", nullable=false) + private final boolean credentialsNonExpired; + + @Column(name = "enabled", nullable=false) + private final boolean enabled; + + @Column(name = "firsttime_login_remaining", nullable=false) + private boolean firstTimeLoginRemaining; + + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + @ManyToOne + @JoinColumn(name = "office_id") + private Office office; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "admin_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles; + + + public static AppUser createNew(Organisation organisation, Office office, + Set allRoles, String username, String email, + String firstname, String lastname, String password) { + + boolean userEnabled = true; + boolean userAccountNonExpired = true; + boolean userCredentialsNonExpired = true; + boolean userAccountNonLocked = true; + + Collection authorities = new ArrayList(); + User user = new User(username, password, userEnabled, userAccountNonExpired, userCredentialsNonExpired, userAccountNonLocked, authorities); + return new AppUser(organisation, office, user, allRoles, email, firstname, lastname); + } + + protected AppUser() { + this.organisation = null; + this.office = null; + this.email = null; + this.username = null; + this.password = null; + this.accountNonExpired = false; + this.accountNonLocked = false; + this.credentialsNonExpired = false; + this.enabled = false; + this.roles = new HashSet(); + this.firstTimeLoginRemaining = true; + } + + public AppUser(final Organisation organisation, final Office office, final User user, final Set roles, final String email, final String firstname, final String lastname) { + this.organisation = organisation; + this.office = office; + this.email = email.trim(); + this.username = user.getUsername().trim(); + this.firstname = firstname.trim(); + this.lastname = lastname.trim(); + this.password = user.getPassword().trim(); + this.accountNonExpired = user.isAccountNonExpired(); + this.accountNonLocked = user.isAccountNonLocked(); + this.credentialsNonExpired = user.isCredentialsNonExpired(); + this.enabled = user.isEnabled(); + this.roles = roles; + this.firstTimeLoginRemaining = true; + } + + @Override + public boolean isFirstTimeLoginRemaining() { + return this.firstTimeLoginRemaining; + } + + @Override + public void updateUsernamePasswordOnFirstTimeLogin(final String newUsername, + final String newPasswordEncoded) { + if (this.username.equals(newUsername)) { + throw new UsernameMustBeDifferentException(); + } + this.username = newUsername; + updatePasswordOnFirstTimeLogin(newPasswordEncoded); + this.firstTimeLoginRemaining = false; + } + + @Override + public void updatePasswordOnFirstTimeLogin(final String newPasswordEncoded) { + if (this.password.equals(newPasswordEncoded)) { + throw new PasswordMustBeDifferentException(); + } + this.password = newPasswordEncoded; + this.firstTimeLoginRemaining = false; + } + + public void updatePassword(final String encodePassword) { + this.password = encodePassword; + } + + @Override + public Collection getAuthorities() { + return this.populateGrantedAuthorities(); + } + + private List populateGrantedAuthorities() { + List grantedAuthorities = new ArrayList(); + for (Role role :this.roles) { + Collection permissions = role.getPermissions(); + for (Permission permission : permissions) { + grantedAuthorities.add(new SimpleGrantedAuthority(permission.code())); + } + } + return grantedAuthorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isAccountNonExpired() { + return this.accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return this.accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return this.credentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + public String getEmail() { + return this.email; + } + + public Set getRoles() { + return this.roles; + } + + public String getRoleNames() { + StringBuilder roleNames = new StringBuilder(); + + for (Role role : this.roles) { + roleNames.append(role.toData().getName()).append(' '); + } + + return roleNames.toString(); + } + + public void setUserIdAs(final Long id) { + this.setId(id); + } + + public Organisation getOrganisation() { + return this.organisation; + } + + public Office getOffice() { + return this.office; + } + + public boolean isHeadOfficeUser() { + boolean headOfficeUser = false; + if (this.office != null) { + headOfficeUser = this.office.isHeadOffice(); + } + return headOfficeUser; + } + + public boolean hasPermissionTo(final String permissionCode) { + boolean match = false; + for (Role role : this.roles) { + if (role.hasPermissionTo(permissionCode)) { + match = true; + break; + } + } + + return match; + } + + public boolean hasNotPermissionForAnyOf(final String... permissionCodes) { + boolean hasNotPermission = true; + for (String permissionCode : permissionCodes) { + boolean checkPermission = this.hasPermissionTo(permissionCode); + if (checkPermission) { + hasNotPermission = false; + break; + } + } + return hasNotPermission; + } + + public boolean hasNotPermissionTo(final String permissionCode) { + return !this.hasPermissionTo(permissionCode); + } + + public boolean canAccess(Long officeId) { + return this.office.hasAnOfficeInHierarchyWithId(officeId); + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public void update(Set allRoles, Office office, String username, + String firstname, String lastname, String email) { + this.roles.clear(); + this.roles = allRoles; + this.office = office; + this.username = username; + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + } + + public void update(String username, String firstname, String lastname, String email) { + this.username = username; + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + } + + public boolean canNotApproveLoanInPast() { + return hasNotPermissionForAnyOf("CAN_APPROVE_LOAN_IN_THE_PAST_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE"); + } + + public boolean canNotRejectLoanInPast() { + return hasNotPermissionForAnyOf("CAN_REJECT_LOAN_IN_THE_PAST_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE"); + } + + public boolean canNotWithdrawByClientLoanInPast() { + return hasNotPermissionForAnyOf("CAN_WITHDRAW_LOAN_IN_THE_PAST_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE"); + } + + public boolean canNotDisburseLoanInPast() { + return hasNotPermissionForAnyOf("CAN_DISBURSE_LOAN_IN_THE_PAST_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE"); + } + + public boolean canNotMakeRepaymentOnLoanInPast() { + return hasNotPermissionForAnyOf("CAN_MAKE_LOAN_REPAYMENT_IN_THE_PAST_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE"); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUserRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUserRepository.java new file mode 100644 index 000000000..475610b24 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUserRepository.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface AppUserRepository extends JpaRepository, JpaSpecificationExecutor, PlatformUserRepository { + // no behaviour added +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserDomainService.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserDomainService.java new file mode 100644 index 000000000..be9c72eb5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserDomainService.java @@ -0,0 +1,93 @@ +package org.mifosng.platform.user.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.mifosng.platform.infrastructure.EmailDetail; +import org.mifosng.platform.infrastructure.PlatformEmailService; +import org.mifosng.platform.infrastructure.PlatformPasswordEncoder; +import org.mifosng.platform.infrastructure.RandomPasswordGenerator; +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.Organisation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class JpaUserDomainService implements UserDomainService { + + @Autowired + private UserPriviledgeDomainService userPriviledgeDomainService; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private AppUserRepository userRepository; + + @Autowired + private PlatformPasswordEncoder applicationPasswordEncoder; + + @Autowired + private PlatformEmailService emailService; + + public static Specification thatMatch(final Organisation organisation) { + return new Specification() { + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { + return cb.equal(root.get("organisation"), organisation); + } + }; + } + + @Transactional + @Override + public void createDefaultAdminUser(final Organisation organisation, final Office office) { + + this.userPriviledgeDomainService.createAllOrganisationRolesAndPermissions(organisation); + + List organisationalRoles = this.roleRepository.findAll(thatMatch(organisation)); + Set allRoles = new HashSet(organisationalRoles); + + String username = "admin" + organisation.getId(); + String password = ""; + + AppUser defaultAdministrator = AppUser.createNew(organisation, office, allRoles, username, organisation.getContactEmail(), "Organisation", "Administrator", password); + + this.create(defaultAdministrator); + } + + @Transactional + @Override + public void create(final AppUser appUser) { + generateKeyUsedForPasswordSalting(appUser); + + String unencodedPassword = appUser.getPassword(); + if (org.apache.commons.lang.StringUtils.isBlank(unencodedPassword)) { + unencodedPassword = new RandomPasswordGenerator(13).generate(); + } + // update so encoding works. + appUser.updatePassword(unencodedPassword); + + String encodePassword = this.applicationPasswordEncoder.encode(appUser); + appUser.updatePassword(encodePassword); + + this.userRepository.save(appUser); + + EmailDetail emailDetail = new EmailDetail(appUser.getOrganisation().getName(), appUser.getOrganisation().getContactName(), appUser.getEmail(), appUser.getUsername()); + + this.emailService.sendToUserAccount(emailDetail, unencodedPassword); + } + + private void generateKeyUsedForPasswordSalting(final AppUser appUser) { + this.userRepository.save(appUser); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserPriviledgeDomainService.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserPriviledgeDomainService.java new file mode 100644 index 000000000..62bbd4de3 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/JpaUserPriviledgeDomainService.java @@ -0,0 +1,114 @@ +package org.mifosng.platform.user.domain; + +import java.util.Arrays; + +import org.mifosng.platform.organisation.domain.Organisation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class JpaUserPriviledgeDomainService implements UserPriviledgeDomainService { + + @Autowired + private PermissionRepository permissionRepository; + + @Autowired + private RoleRepository roleRepository; + + @Transactional + @Override + public void createAllOrganisationRolesAndPermissions(final Organisation organisation) { + + // 2. create out of the box application permissions and roles + Permission viewUsersAndRolesPermission = new PermissionBuilder().viewUsersAndRoles().with(organisation).build(); + Permission createUserPermission = new PermissionBuilder().userCreation().with(organisation).build(); + Permission createRolePermission = new PermissionBuilder().roleCreation().with(organisation).build(); + Permission updateApplicationPermissionsPermission = new PermissionBuilder().updateApplicationPermissions().with(organisation).build(); + + Permission[] userAdminPermissions = new Permission[] {viewUsersAndRolesPermission, createUserPermission, + createRolePermission, updateApplicationPermissionsPermission}; + + Permission viewOrganisationOfficesStaffAndProductsPermission = new PermissionBuilder().viewOrganisationsOfficesStaffAndProducts().with(organisation).build(); + Permission addOfficePermission = new PermissionBuilder().addOffice().with(organisation).build(); + Permission addStaffPermission = new PermissionBuilder().addStaff().with(organisation).build(); + Permission addLoanProductPermission = new PermissionBuilder().addLoanProduct().with(organisation).build(); + + Permission[] organisationAdminPermissions = new Permission[] {viewOrganisationOfficesStaffAndProductsPermission, addOfficePermission + ,addStaffPermission, addLoanProductPermission}; + + Permission viewLoanPortfolioPermission = new PermissionBuilder().viewLoanPortfolio().with(organisation).build(); + Permission addLoanPermission = new PermissionBuilder().addLoan().with(organisation).build(); + Permission addBackdatedLoanPermission = new PermissionBuilder().addBackdatedLoan().with(organisation).build(); + Permission approveLoanPermission = new PermissionBuilder().approveLoan().with(organisation).build(); + Permission approveLoanWithPastDatePermission = new PermissionBuilder().approveLoanInThePast().with(organisation).build(); + Permission rejectLoanPermission = new PermissionBuilder().rejectLoan().with(organisation).build(); + Permission rejectLoanWithPastDatePermission = new PermissionBuilder().rejectLoanInThePast().with(organisation).build(); + Permission withdrawLoanPermission = new PermissionBuilder().withdrawLoan().with(organisation).build(); + Permission withdrawLoanWithPastDatePermission = new PermissionBuilder().withdrawLoanInThePast().with(organisation).build(); + Permission undoLoanApprovalPermission = new PermissionBuilder().undoLoanApproval().with(organisation).build(); + + Permission disburseLoanPermission = new PermissionBuilder().disburseLoan().with(organisation).build(); + Permission disburseLoanWithPastDatePermission = new PermissionBuilder().disburseLoanInThePast().with(organisation).build(); + Permission undoLoanDisbursalPermission = new PermissionBuilder().undoLoanDisbursal().with(organisation).build(); + + Permission makeLoanRepaymentPermission = new PermissionBuilder().makeLoanRepayment().with(organisation).build(); + Permission makeLoanRepaymentWithPastDatePermission = new PermissionBuilder().makeLoanRepaymentInThePast().with(organisation).build(); + + // Permission writeoffLoanPermission = new PermissionBuilder().writeoffLoan().with(organisation).build(); + // Permission writeoffLoanWithPastDatePermission = new PermissionBuilder().writeoffLoanInThePast().with(organisation).build(); + + // Permission rescheduleLoanPermission = new PermissionBuilder().rescheduleLoan().with(organisation).build(); + // Permission rescheduleLoanWithPastDatePermission = new PermissionBuilder().rescheduleInThePast().with(organisation).build(); + + // MIFOS-2878 - support recovery of payments for written off loans. + + // support interest reduction for early payments and interest increase for late payments + + // how many MFIs allow clients to be in multiple groups (more than one)? + + Permission[] fullLoanPortfolioPermissions = new Permission[] {viewLoanPortfolioPermission, + addLoanPermission, + addBackdatedLoanPermission, + approveLoanPermission, + approveLoanWithPastDatePermission, + rejectLoanPermission, + rejectLoanWithPastDatePermission, + withdrawLoanPermission, + withdrawLoanWithPastDatePermission, + undoLoanApprovalPermission, + disburseLoanPermission, + disburseLoanWithPastDatePermission, + undoLoanDisbursalPermission, + makeLoanRepaymentPermission, + makeLoanRepaymentWithPastDatePermission, + // writeoffLoanPermission, + // writeoffLoanWithPastDatePermission, + // rescheduleLoanPermission + // ,rescheduleLoanWithPastDatePermission + }; + + Permission dataMigrationPermission = new PermissionBuilder() + .dataMigration().with(organisation).build(); + + Permission[] migrationPermissions = new Permission[] { dataMigrationPermission }; + + this.permissionRepository.save(Arrays.asList(userAdminPermissions)); + this.permissionRepository.save(Arrays.asList(organisationAdminPermissions)); + this.permissionRepository.save(Arrays.asList(fullLoanPortfolioPermissions)); + this.permissionRepository.save(Arrays.asList(migrationPermissions)); + + // 2. roles + String userAdminRoleDescription = "A user administrator can create new roles, assign and update roles assigned to users and create and deactivate users."; + + Role userAdminRole = new Role(organisation, "User Administrator", userAdminRoleDescription, Arrays.asList(userAdminPermissions)); + + String organisationAdminRoleDescription = "A organisation administrator can view, create and update organisation office hierarchy, staff and products."; + Role organisatonAdminRole = new Role(organisation, "Organisation Administrator", organisationAdminRoleDescription, Arrays.asList(organisationAdminPermissions)); + + String fullLoanPortfolioRoleDescription = "A loan portfolio user can view, create and update client, group and loan information."; + Role loanPortfolioFullRole = new Role(organisation, "Full Loan Portfolio", fullLoanPortfolioRoleDescription, Arrays.asList(fullLoanPortfolioPermissions)); + + this.roleRepository.save(Arrays.asList(userAdminRole, organisatonAdminRole, loanPortfolioFullRole)); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PasswordMustBeDifferentException.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PasswordMustBeDifferentException.java new file mode 100644 index 000000000..ea685ec85 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PasswordMustBeDifferentException.java @@ -0,0 +1,5 @@ +package org.mifosng.platform.user.domain; + +public class PasswordMustBeDifferentException extends RuntimeException { + // basic runtime exception +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Permission.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Permission.java new file mode 100644 index 000000000..e1f63dbca --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Permission.java @@ -0,0 +1,64 @@ +package org.mifosng.platform.user.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosng.data.PermissionData; +import org.mifosng.platform.organisation.domain.Organisation; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "admin_permission") +public class Permission extends AbstractPersistable { + + @Column(name = "code", nullable = false, length=100) + private final String code; + + @Column(name = "default_name", nullable = false, length=100) + private final String defaultName; + + @Column(name = "default_description", nullable = false, length=500) + private final String defaultDescription; + + @Column(name = "group_enum", nullable = false) + @Enumerated(EnumType.ORDINAL) + private final PermissionGroup permissionGroup; + + @ManyToOne + @JoinColumn(name = "org_id", nullable = false) + private final Organisation organisation; + + public Permission() { + this.code = null; + this.defaultDescription = null; + this.organisation = null; + this.defaultName = null; + this.permissionGroup = null; + } + + public Permission(final Organisation organisation, final String code, final String defaultDescription, final String defaultName, + final PermissionGroup permissionGroup) { + this.organisation = organisation; + this.code = code; + this.defaultDescription = defaultDescription; + this.permissionGroup = permissionGroup; + this.defaultName = defaultName; + } + + public boolean hasCode(String checkCode) { + return this.code.equalsIgnoreCase(checkCode); + } + + public String code() { + return this.code; + } + + public PermissionData toData() { + return new PermissionData(this.getId(), this.organisation.getId(), this.defaultName, this.defaultDescription, this.code, this.permissionGroup.ordinal()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionBuilder.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionBuilder.java new file mode 100644 index 000000000..fb6503469 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionBuilder.java @@ -0,0 +1,245 @@ +package org.mifosng.platform.user.domain; + +import org.mifosng.platform.organisation.domain.Organisation; + +public class PermissionBuilder { + + private Organisation organisation; + private String code; + private String name; + private String description; + private PermissionGroup permissionGroup; + + public Permission build() { + return new Permission(this.organisation, this.code, this.description, this.name, this.permissionGroup); + } + + public PermissionBuilder with(final Organisation withOrg) { + this.organisation = withOrg; + return this; + } + + public PermissionBuilder userCreation() { + this.code = "CAN_CREATE_APPLICATION_USER_ROLE"; + this.name = "User Creation"; + this.description = "Allows an application user to create other users for the application."; + this.permissionGroup = PermissionGroup.USER_ADMINISTRATION; + return this; + } + + public PermissionBuilder roleCreation() { + this.code = "CAN_CREATE_APPLICATION_ROLE_ROLE"; + this.name = "Role Creation"; + this.description = "Allows an application user to create other application roles for assigning to users."; + this.permissionGroup = PermissionGroup.USER_ADMINISTRATION; + return this; + } + + public PermissionBuilder viewUsersAndRoles() { + this.code = "CAN_VIEW_USERS_AND_ROLES_ROLE"; + this.name = "View users and roles"; + this.description = "Allows an application user to view other application users and roles in their organisation."; + this.permissionGroup = PermissionGroup.USER_ADMINISTRATION; + return this; + } + + public PermissionBuilder updateApplicationPermissions() { + this.code = "CAN_UPDATE_APPLICATION_PERMISSION_NAMES_AND_DESCRIPTIONS_ROLE"; + this.name = "Update name and descriptions of application permissions"; + this.description = "Allows an application user to update the names and description of application permissions for their organisation."; + this.permissionGroup = PermissionGroup.USER_ADMINISTRATION; + return this; + } + + public PermissionBuilder viewOrganisationsOfficesStaffAndProducts() { + this.code = "CAN_VIEW_ORGANISATION_WIDE_VIEW_OF_OFFICES_STAFF_AND_PRODUCTS_ROLE"; + this.name = "View organisations offices, staff and products"; + this.description = "Allows an application user to view all offices, staff and products for their organisation."; + this.permissionGroup = PermissionGroup.ORGANISATION_ADMINISTRATION; + return this; + } + + public PermissionBuilder addOffice() { + this.code = "CAN_ADD_OFFICE_ROLE"; + this.name = "Add offices"; + this.description = "Allows an application user add new offices for their organisation."; + this.permissionGroup = PermissionGroup.ORGANISATION_ADMINISTRATION; + return this; + } + + public PermissionBuilder addStaff() { + this.code = "CAN_ADD_STAFF_ROLE"; + this.name = "Add staff"; + this.description = "Allows an application user add new staff members for their organisation."; + this.permissionGroup = PermissionGroup.ORGANISATION_ADMINISTRATION; + return this; + } + + public PermissionBuilder addLoanProduct() { + this.code = "CAN_ADD_LOAN_PRODUCT_ROLE"; + this.name = "Add loan product"; + this.description = "Allows an application user add new loan products for their organisation."; + this.permissionGroup = PermissionGroup.ORGANISATION_ADMINISTRATION; + return this; + } + + public PermissionBuilder viewLoanPortfolio() { + this.code = "CAN_VIEW_LOAN_PORTFOLIO_ROLE"; + this.name = "View client, group and loan information"; + this.description = "Allows an application user to view client, group and loan information for their organisation."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder addLoan() { + this.code = "CAN_ADD_LOAN_ROLE"; + this.name = "Add loan."; + this.description = "Allows an application user to add a loan."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder addBackdatedLoan() { + this.code = "CAN_ADD_BACKDATED_LOAN_ROLE"; + this.name = "Add backdated loan."; + this.description = "Allows an application user to add a loan where the submitted on date is in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder approveLoan() { + this.code = "CAN_APPROVE_LOAN_ROLE"; + this.name = "Approve loan."; + this.description = "Allows an application user to approve a loan."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder approveLoanInThePast() { + this.code = "CAN_APPROVE_LOAN_IN_THE_PAST_ROLE"; + this.name = "Approve loan with date in past."; + this.description = "Allows an application user to approve a loan with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder rejectLoan() { + this.code = "CAN_REJECT_LOAN_ROLE"; + this.name = "Reject loan."; + this.description = "Allows an application user to reject the loan application."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder rejectLoanInThePast() { + this.code = "CAN_REJECT_LOAN_IN_THE_PAST_ROLE"; + this.name = "Reject loan with date in past."; + this.description = "Allows an application user to reject the loan application with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder withdrawLoan() { + this.code = "CAN_WITHDRAW_LOAN_ROLE"; + this.name = "Withdraw loan."; + this.description = "Allows an application user to withdraw the loan application."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder withdrawLoanInThePast() { + this.code = "CAN_WITHDRAW_LOAN_IN_THE_PAST_ROLE"; + this.name = "Withdraw loan with date in past."; + this.description = "Allows an application user to withraw the loan application with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder undoLoanApproval() { + this.code = "CAN_UNDO_LOAN_APPROVAL_ROLE"; + this.name = "Undo loan approval."; + this.description = "Allows an application user to undo a loan approval."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder disburseLoan() { + this.code = "CAN_DISBURSE_LOAN_ROLE"; + this.name = "Disburse loan."; + this.description = "Allows an application user to disburse the loan."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder disburseLoanInThePast() { + this.code = "CAN_DISBURSE_LOAN_IN_THE_PAST_ROLE"; + this.name = "Disburse loan with date in past."; + this.description = "Allows an application user to disburse the loan with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder undoLoanDisbursal() { + this.code = "CAN_UNDO_LOAN_DISBURSAL_ROLE"; + this.name = "Undo loan disbursal."; + this.description = "Allows an application user to undo a loan disbursal (if no payments already made)."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder makeLoanRepayment() { + this.code = "CAN_MAKE_LOAN_REPAYMENT_ROLE"; + this.name = "Make loan repayment."; + this.description = "Allows an application user to make a repayment against a loan."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder makeLoanRepaymentInThePast() { + this.code = "CAN_MAKE_LOAN_REPAYMENT_IN_THE_PAST_ROLE"; + this.name = "Make loan repayment with date in past."; + this.description = "Allows an application user to disburse the loan with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder writeoffLoan() { + this.code = "CAN_WRITEOFF_LOAN_ROLE"; + this.name = "Write off loan."; + this.description = "Allows an application user to write off the loan application."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder writeoffLoanInThePast() { + this.code = "CAN_WRITEOFF_LOAN_IN_THE_PAST_ROLE"; + this.name = "Write off loan with date in past."; + this.description = "Allows an application user to write off the loan application with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder rescheduleLoan() { + this.code = "CAN_RESCHEDULE_LOAN_ROLE"; + this.name = "Reschedule loan."; + this.description = "Allows an application user to reschedule the loan application."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder rescheduleInThePast() { + this.code = "CAN_RESCHEDULE_LOAN_IN_THE_PAST_ROLE"; + this.name = "Reschedule loan with date in past."; + this.description = "Allows an application user to reschedule the loan application with a date in the past."; + this.permissionGroup = PermissionGroup.PORTFOLIO_MANAGEMENT; + return this; + } + + public PermissionBuilder dataMigration() { + this.code = "CAN_MIGRATE_DATA_ROLE"; + this.name = "Upload client, group and loan data onto application in bulk"; + this.description = "Allows an application user to upload client, group and loan information in bulk for their organisation."; + this.permissionGroup = PermissionGroup.DATA_MIGRATION; + return this; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionGroup.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionGroup.java new file mode 100644 index 000000000..a119fc92a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionGroup.java @@ -0,0 +1,5 @@ +package org.mifosng.platform.user.domain; + +public enum PermissionGroup { + INVALID, USER_ADMINISTRATION, ORGANISATION_ADMINISTRATION, PORTFOLIO_MANAGEMENT, REPORTING, DATA_MIGRATION; +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionRepository.java new file mode 100644 index 000000000..e236b5bb8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PermissionRepository.java @@ -0,0 +1,7 @@ +package org.mifosng.platform.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PermissionRepository extends JpaRepository { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PlatformUserRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PlatformUserRepository.java new file mode 100644 index 000000000..cd064e296 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/PlatformUserRepository.java @@ -0,0 +1,9 @@ +package org.mifosng.platform.user.domain; + +import org.mifosng.platform.infrastructure.PlatformUser; + +public interface PlatformUserRepository { + + PlatformUser findByUsername(String username); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Role.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Role.java new file mode 100644 index 000000000..5ba10068c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/Role.java @@ -0,0 +1,90 @@ +package org.mifosng.platform.user.domain; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosng.data.PermissionData; +import org.mifosng.data.RoleData; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.organisation.domain.Organisation; + +@Entity +@Table(name = "admin_role") +public class Role extends AbstractAuditableCustom { + + @Column(name="name", nullable=false, length=100) + private String name; + + @Column(name="description", nullable=false, length=500) + private String description; + + @ManyToOne + @JoinColumn(name="org_id", nullable=false) + private Organisation organisation; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "admin_role_permission", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "permission_id")) + private Set permissions; + + protected Role() { + this.name = null; + this.description = null; + this.permissions = new HashSet(); + this.organisation = null; + } + + public Role(final Organisation organisation, final String name, final String description, final List rolePermissions) { + this.organisation = organisation; + this.name = name.trim(); + this.description = description.trim(); + this.permissions = new HashSet(rolePermissions); + } + + public Collection getPermissions() { + return this.permissions; + } + + public boolean hasPermissionTo(final String permissionCode) { + boolean match = false; + for (Permission permission : this.permissions) { + if (permission.hasCode(permissionCode)) { + match = true; + break; + } + } + return match; + } + + public RoleData toData() { + + RoleData data = new RoleData(this.getId(), this.organisation.getId(), this.name, this.description); + + Collection rolePermissions = new ArrayList(); + for (Permission permission : this.permissions) { + PermissionData permissionData = permission.toData(); + rolePermissions.add(permissionData); + } + data.setSelectedPermissions(rolePermissions); + + return data; + } + + public void update(String name, String description, List selectedPermissions) { + this.name = name; + this.description = description; + this.permissions.clear(); + this.permissions = new HashSet(selectedPermissions); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/RoleRepository.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/RoleRepository.java new file mode 100644 index 000000000..3dea77850 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/RoleRepository.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface RoleRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserDomainService.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserDomainService.java new file mode 100644 index 000000000..d539e3995 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserDomainService.java @@ -0,0 +1,12 @@ +package org.mifosng.platform.user.domain; + +import org.mifosng.platform.organisation.domain.Office; +import org.mifosng.platform.organisation.domain.Organisation; + + +public interface UserDomainService { + + void createDefaultAdminUser(Organisation organisation, Office office); + + void create(AppUser appUser); +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserPriviledgeDomainService.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserPriviledgeDomainService.java new file mode 100644 index 000000000..f5f4fa7ef --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UserPriviledgeDomainService.java @@ -0,0 +1,10 @@ +package org.mifosng.platform.user.domain; + +import org.mifosng.platform.organisation.domain.Organisation; + + +public interface UserPriviledgeDomainService { + + void createAllOrganisationRolesAndPermissions(Organisation organisation); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UsernameMustBeDifferentException.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UsernameMustBeDifferentException.java new file mode 100644 index 000000000..098347282 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/UsernameMustBeDifferentException.java @@ -0,0 +1,5 @@ +package org.mifosng.platform.user.domain; + +public class UsernameMustBeDifferentException extends RuntimeException { + // basic runtime exception +} diff --git a/mifosng-provider/src/main/resources/META-INF/orm.xml b/mifosng-provider/src/main/resources/META-INF/orm.xml new file mode 100644 index 000000000..da5ba62b9 --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/orm.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/mifosng-provider/src/main/resources/META-INF/persistence.xml b/mifosng-provider/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000..f691a8e28 --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,14 @@ + + + + org.hibernate.ejb.HibernatePersistence + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml new file mode 100644 index 000000000..1946d0d84 --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/infrastructure.xml b/mifosng-provider/src/main/resources/META-INF/spring/infrastructure.xml new file mode 100644 index 000000000..ec498b0a3 --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/spring/infrastructure.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml b/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml new file mode 100644 index 000000000..d48bc7a1f --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/securityContextOld.xml b/mifosng-provider/src/main/resources/META-INF/spring/securityContextOld.xml new file mode 100644 index 000000000..43e0a29d9 --- /dev/null +++ b/mifosng-provider/src/main/resources/META-INF/spring/securityContextOld.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/ccylist.csv b/mifosng-provider/src/main/resources/ccylist.csv new file mode 100644 index 000000000..3e88e4fc8 --- /dev/null +++ b/mifosng-provider/src/main/resources/ccylist.csv @@ -0,0 +1,164 @@ +Code,Name,DPs +AED,UAE Dirham,2 +AFN,Afghanistan Afghani,2 +ALL,Albanian Lek,2 +AMD,Armenian Dram,2 +ANG,Netherlands Antillian Guilder,2 +AOA,Angolan Kwanza,2 +ARS,Argentine Peso,2 +AUD,Australian Dollar,2 +AWG,Aruban Guilder,2 +AZM,Azerbaijanian Manat,2 +BAM,Bosnia and Herzegovina Convertible Marks,2 +BBD,Barbados Dollar,2 +BDT,Bangladesh Taka,2 +BGN,Bulgarian Lev,2 +BHD,Bahraini Dinar,3 +BIF,Burundi Franc,0 +BMD,Bermudian Dollar,2 +BND,Brunei Dollar,2 +BOB,Bolivian Boliviano,2 +BRL,Brazilian Real,2 +BSD,Bahamian Dollar,2 +BTN,Bhutan Ngultrum,2 +BWP,Botswana Pula,2 +BYR,Belarussian Ruble,0 +BZD,Belize Dollar,2 +CAD,Canadian Dollar,2 +CDF,Franc Congolais,2 +CHF,Swiss Franc,2 +CLP,Chilean Peso,0 +CNY,Chinese Yuan Renminbi,2 +COP,Colombian Peso,2 +CRC,Costa Rican Colon,2 +CSD,Serbian Dinar,2 +CUP,Cuban Peso,2 +CVE,Cape Verde Escudo,2 +CYP,Cyprus Pound,2 +CZK,Czech Koruna,2 +DJF,Djibouti Franc,0 +DKK,Danish Krone,2 +DOP,Dominican Peso,2 +DZD,Algerian Dinar,2 +EEK,Estonian Kroon,2 +EGP,Egyptian Pound,2 +ERN,Eritrea Nafka,2 +ETB,Ethiopian Birr,2 +EUR,euro,2 +FJD,Fiji Dollar,2 +FKP,Falkland Islands Pound,2 +GBP,Pound Sterling,2 +GEL,Georgian Lari,2 +GHC,Ghana Cedi,2 +GIP,Gibraltar Pound,2 +GMD,Gambian Dalasi,2 +GNF,Guinea Franc,0 +GTQ,Guatemala Quetzal,2 +GYD,Guyana Dollar,2 +HKD,Hong Kong Dollar,2 +HNL,Honduras Lempira,2 +HRK,Croatian Kuna,2 +HTG,Haiti Gourde,2 +HUF,Hungarian Forint,2 +IDR,Indonesian Rupiah,2 +ILS,New Israeli Shekel,2 +INR,Indian Rupee,2 +IQD,Iraqi Dinar,3 +IRR,Iranian Rial,2 +ISK,Iceland Krona,0 +JMD,Jamaican Dollar,2 +JOD,Jordanian Dinar,3 +JPY,Japanese Yen,0 +KES,Kenyan Shilling,2 +KGS,Kyrgyzstan Som,2 +KHR,Cambodia Riel,2 +KMF,Comoro Franc,0 +KPW,North Korean Won,2 +KRW,Korean Won,0 +KWD,Kuwaiti Dinar,3 +KYD,Cayman Islands Dollar,2 +KZT,Kazakhstan Tenge,2 +LAK,Lao Kip,2 +LBP,Lebanese Pound,2 +LKR,Sri Lanka Rupee,2 +LRD,Liberian Dollar,2 +LSL,Lesotho Loti,2 +LTL,Lithuanian Litas,2 +LVL,Latvian Lats,2 +LYD,Libyan Dinar,3 +MAD,Moroccan Dirham,2 +MDL,Moldovan Leu,2 +MGA,Malagasy Ariary,2 +MKD,Macedonian Denar,2 +MMK,Myanmar Kyat,2 +MNT,Mongolian Tugrik,2 +MOP,Macau Pataca,2 +MRO,Mauritania Ouguiya,2 +MTL,Maltese Lira,2 +MUR,Mauritius Rupee,2 +MVR,Maldives Rufiyaa,2 +MWK,Malawi Kwacha,2 +MXN,Mexican Peso,2 +MYR,Malaysian Ringgit,2 +MZM,Mozambique Metical,2 +NAD,Namibia Dollar,2 +NGN,Nigerian Naira,2 +NIO,Nicaragua Cordoba Oro,2 +NOK,Norwegian Krone,2 +NPR,Nepalese Rupee,2 +NZD,New Zealand Dollar,2 +OMR,Rial Omani,3 +PAB,Panama Balboa,2 +PEN,Peruvian Nuevo Sol,2 +PGK,Papua New Guinea Kina,2 +PHP,Philippine Peso,2 +PKR,Pakistan Rupee,2 +PLN,Polish Zloty,2 +PYG,Paraguayan Guarani,0 +QAR,Qatari Rial,2 +RON,Romanian Leu,2 +RUB,Russian Ruble,2 +RWF,Rwanda Franc,0 +SAR,Saudi Riyal,2 +SBD,Solomon Islands Dollar,2 +SCR,Seychelles Rupee,2 +SDD,Sudanese Dinar,2 +SEK,Swedish Krona,2 +SGD,Singapore Dollar,2 +SHP,St Helena Pound,2 +SIT,Slovenian Tolar,2 +SKK,Slovak Koruna,2 +SLL,Sierra Leone Leone,2 +SOS,Somali Shilling,2 +SRD,Surinam Dollar,2 +STD,São Tome and Principe Dobra,2 +SVC,El Salvador Colon,2 +SYP,Syrian Pound,2 +SZL,Swaziland Lilangeni,2 +THB,Thai Baht,2 +TJS,Tajik Somoni,2 +TMM,Turkmenistan Manat,2 +TND,Tunisian Dinar,3 +TOP,Tonga Pa'anga,2 +TRY,Turkish Lira,2 +TTD,Trinidad and Tobago Dollar,2 +TWD,New Taiwan Dollar,2 +TZS,Tanzanian Shilling,2 +UAH,Ukraine Hryvnia,2 +UGX,Uganda Shilling,2 +USD,US Dollar,2 +UYU,Peso Uruguayo,2 +UZS,Uzbekistan Sum,2 +VEB,Venezuelan Bolivar,2 +VND,Vietnamese Dong,2 +VUV,Vanuatu Vatu,0 +WST,Samoa Tala,2 +XAF,CFA Franc BEAC,0 +XCD,East Caribbean Dollar,2 +XDR,SDR (Special Drawing Rights),5 +XOF,CFA Franc BCEAO,0 +XPF,CFP Franc,0 +YER,Yemeni Rial,2 +ZAR,South African Rand,2 +ZMK,Zambian Kwacha,2 +ZWD,Zimbabwe Dollar,2 diff --git a/mifosng-provider/src/main/resources/logback.xml b/mifosng-provider/src/main/resources/logback.xml new file mode 100644 index 000000000..42c1d98e5 --- /dev/null +++ b/mifosng-provider/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + ${catalina.home}/logs/mifosng-provider.log + true + + + %-4relative [%thread] %-5level %logger{35} - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/MPESALoanDisbursalsExportSummary.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/MPESALoanDisbursalsExportSummary.properties new file mode 100644 index 000000000..d4af24d05 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/MPESALoanDisbursalsExportSummary.properties @@ -0,0 +1,17 @@ +LoanDisbursalsExportSummary=MPESA Loan Disbursals Export Summary +TotalAmountOfDisbursals\:=Total Amount Of Disbursals: +TotalNumberOfExportedRecords\:=Number Of Exported Records: +ListOfExportErrors\:=List Of Export Errors: +LoanAccountNo=Loan Account Number +ReceiptNo=Receipt Number +ErrorDescription=Error Description +PhoneNo=Phone Number +SelectedGroup\:=Selected Group: +SelectedProduct\:=Selected Product: +SelectedExportDate\:=Selected Export Date: +AsOf\:=As Of: +LastEditedBy=Last edited by +PrintedBy\:=Printed by: +Page=Page +On\:=On: +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoanSummaryBranch.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoanSummaryBranch.properties new file mode 100644 index 000000000..578a6287b --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoanSummaryBranch.properties @@ -0,0 +1,18 @@ +ActiveLoanSummaryBranch=Active Loan Summary per Branch +Branch=Branch +NoOfCenters=No. of Centers +NoOfGroups=No. of Groups +NoOfClients=No. of Clients +NoOfActiveLoans=No. of Active Loans +NoOfLoansInArrears=No. of Loans in Arrears +TotalLoansDisbursed=Total Loans Disbursed +TotalPrincipalRepaid=Total Principal Repaid +TotalInterestRepaid=Total Interest Repaid +TotalPrincipalOutstanding=Total Principal Outstanding +TotalInterestOutstanding=Total Interest Outstanding +AmountInArrears=Amount in Arrears +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansCenter.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansCenter.properties new file mode 100644 index 000000000..7bdfe4edb --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansCenter.properties @@ -0,0 +1,21 @@ +ActiveLoansCenter=Active Loans by Center +Office\:=Office: +Center\:=Center: +SystemId=System Id +CenterName=Center Name +GroupName=Group Name +ClientName=Client Name +LoanAmount=Loan Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +Total=Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLastInstallment.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLastInstallment.properties new file mode 100644 index 000000000..39e7851c6 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLastInstallment.properties @@ -0,0 +1,20 @@ +ActiveLoansLastInstallment=Active Loans in their Last Installment +Office\:=Office: +SystemId=System Id +CenterName=Center Name +GroupName=Group Name +ClientName=Client Name +LoanAmount=Loan Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +Total=Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanOfficer.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanOfficer.properties new file mode 100644 index 000000000..33d323a30 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanOfficer.properties @@ -0,0 +1,21 @@ +ActiveLoansLoanOfficer=Active Loans by Loan Officer +Office\:=Office: +LoanOfficer\:=Loan Officer: +SystemId=System Id +CenterName=Center Name +GroupName=Group Name +ClientName=Client Name +LoanAmount=Loan Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +Total=Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanProduct.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanProduct.properties new file mode 100644 index 000000000..c1c82a95f --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanProduct.properties @@ -0,0 +1,21 @@ +ActiveLoansLoanProduct=Active Loans by Loan Product +Office\:=Office: +LoanProduct\:=Loan Product: +SystemId=System Id +CenterName=Center Name +GroupName=Group Name +ClientName=Client Name +LoanAmount=Loan Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +Total=Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanPurpose.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanPurpose.properties new file mode 100644 index 000000000..4e13a236b --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/activeLoansLoanPurpose.properties @@ -0,0 +1,21 @@ +ActiveLoansLoanPurpose=Active Loans by Loan Purpose +Office\:=Office: +LoanPurpose\:=Loan Purpose: +SystemId=System Id +CenterName=Center Name +GroupName=Group Name +ClientName=Client Name +LoanAmount=Loan Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +Total=Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/agingSummary.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/agingSummary.properties new file mode 100644 index 000000000..bb0ab1f7c --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/agingSummary.properties @@ -0,0 +1,37 @@ +AgingSummary=Aging Summary +Office\:=Office: +AGINGANALYSIS=AGING ANALYSIS +ArrearsAmount=Arrears Amount +PAR=PAR +PARratio=PAR Ratio +AGINGTABLE=AGING TABLE +ofloans=# of Loans +LoanAmount=Loan Amount +PrincipalInArrears=Principal in Arrears +InterestInArrears=Interest in Arrears +PrincipalOutstanding=Principal Outstanding +InterestOutstanding=Interest Outstanding +BalanceOutstanding=Balance Outstanding +Total=Total +VersionAt=Version 1.3 +Page=Page +On\:=On: +PrintedBy\:=Printed by: +1\ Week\ in\ Arrears=1 Week in Arrears +2\ Weeks\ in\ Arrears=2 Weeks in Arrears +3\ Weeks\ in\ Arrears=3 Weeks in Arrears +4\ Weeks\ in\ Arrears=4 Weeks in Arrears +5\ Weeks\ in\ Arrears=5 Weeks in Arrears +6\ Weeks\ in\ Arrears=6 Weeks in Arrears +7\ Weeks\ in\ Arrears=7 Weeks in Arrears +8\ Weeks\ in\ Arrears=8 Weeks in Arrears +9\ Weeks\ in\ Arrears=9 Weeks in Arrears +10\ Weeks\ in\ Arrears=10 Weeks in Arrears +11\ Weeks\ in\ Arrears=11 Weeks in Arrears +12\ Weeks\ in\ Arrears=12 Weeks in Arrears +12+\ Weeks\ in\ Arrears=> 12 Weeks in Arrears +0\ to\ 30\ Days\ in\ Arrears=1 to 30 Days in Arrears +30\ to\ 60\ Days\ in\ Arrears=31 to 60 Days in Arrears +60\ to\ 90\ Days\ in\ Arrears=61 to 90 Days in Arrears +90\ to\ 180\ Days\ in\ Arrears=91 to 180 Days in Arrears +>\ 180\ Days\ in\ Arrears=> 180 Days in Arrears diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstanding.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstanding.properties new file mode 100644 index 000000000..d6b2763e9 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstanding.properties @@ -0,0 +1,29 @@ +balanceOutstanding=Balance Outstanding +loanOfficer\:=Loan Officer: +branch\:=Office: +product\:=Product: +outstandingAsOnDate\:=As On Date: +status\:=Status: +loanProducts=Loan Products +loanProduct\:=Loan Product: +accountId=Account Id +memberName=Client Name +loanAmount=Loan Amount +balance=Balance Amount +loanStatus=Loan Status +disbursedDate=Disbursal Date +gender=Gender +groupName=Group Name +loanOfficer=Loan Officer +sourceOfFund=Source Of Funds +subTotal=Total for +savingProducts=Saving Products +savingProduct\:=Saving Product: +savingStatus=Saving Status +dateOfOpening=Date Of Opening +noDataAvailable=No Data Available +blank= +VersionAt=Version 1.3 +printedBy\:=Printed By: +printedOn\:=Printed On: +page\:=Page: diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstandingBySourceOfFunds.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstandingBySourceOfFunds.properties new file mode 100644 index 000000000..7eb2899e7 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/balanceOutstandingBySourceOfFunds.properties @@ -0,0 +1,16 @@ +BalanceOutstandingBySourceOfFunds=Balance Outstanding by Source of Funds +AsOnDate\:=As On Date: +Office\:=Office: +SourceOfFunds\:=Source of Funds: +SourceOfFunds=Source of Funds +AmountDisbursed=Amount Disbursed +AmountPaid=Amount Paid +AmountOutstanding=Amount Outstanding +PrincipalInArrears=Principal in Arrears +NumberOfLoans=Number of Loans +NumberOfClients=Number of Clients +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/branchExpectedCashFlow.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/branchExpectedCashFlow.properties new file mode 100644 index 000000000..690a25249 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/branchExpectedCashFlow.properties @@ -0,0 +1,21 @@ +BranchExpectedCashFlow=Branch Expected Cash Flow +From\:=From: +To\:=To: +Office\:=Office: +ReportDate\:=Report Date: +Branch\:=Branch: +Date=Date +ExpectedCashIn=Expected Cash In +Principal=Principal +Interest=Interest +ClientFees=Client Fees +LoanFees=Loan Fees +ExpectedCashOut=Expected Cash Out +LoanDisbursals=Loan Disbursals +NetExpectedCashFlow=Net Expected Cash Flow +Total=Total +PrintedBy\:=Printed by: +On\:=On: +MFIName\:=MFI Name: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerCollectionSheet.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerCollectionSheet.properties new file mode 100644 index 000000000..8836e4e9b --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerCollectionSheet.properties @@ -0,0 +1,40 @@ +CenterCollectionSheet=Center Collection Sheet +MeetingDate\:=Meeting Date: +Office\:=Office: +LoanOfficer\:=Loan Officer: +TopCustomerHierarchyId\:=Top Customer Hierarchy Id: +BranchOffice\:=Branch Office: +MeetingLocation\:=Meeting Location: +CenterName\:=Center Name: +ClientMifosID=Client Mifos ID +Name=Name +Att*=Att* +*Att_P,A,L,AA=* Att = P, A, L, AA +ProductID=Product - ID +LoanInstallment=Loan Installment # +AdvancePayment=Advance Payment +AmountPrepaid=Amount Prepaid +AmountPrepaid\:=Amount Prepaid: +Due=Due +Collected=Collected +Principal=Principal +Interest=Interest +F/P=Fees/Penalties +TOTAL=TOTAL +TOTAL\:=TOTAL: +WithdrawalsDisbursals=Withdrawals / Disbursals +Other=Other +Summary=Summary +TotalAmountDue\:=Total Amount Due: +Collection=Collection +AmountCollectedBeforeMeeting\:=Amount Collected Before Meeting: +AmountCollectedDuringMeeting\:=Amount Collected During Meeting: +Issues=Issues +TotalWithdrawalIssued\:=Total Withdrawal Issued: +TotalDisbursalsIssued\:=Total Disbursals Issued: +NETCOLLECTION\:=NET COLLECTION: +Signatures\:=Signatures: +Signature=Signature +VersionAt=v1.3 +Page=Page +AmountCollected=Amount Collected diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerScheduleLoanOfficer.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerScheduleLoanOfficer.properties new file mode 100644 index 000000000..85737eaa4 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/centerScheduleLoanOfficer.properties @@ -0,0 +1,18 @@ +CenterScheduleLoanOfficer=Center Schedule by Loan Officer +Office\:=Office: +From\:=From: +To\:=To: +LoanOfficer=Loan Officer +MeetingDay=Meeting Day +Center=Center +Group=Group +Other=Other +NumOfCenters=No. of Centers: +NumOfGroups=No. of Groups: +TotalNumOfCenters=Total No. of Centers: +TotalNumOfGroups=Total No. of Groups: +VersionAt=Version 1.3 +PrintedBy\:=Printed By: +On\:=On: +Page=Page + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientExit.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientExit.properties new file mode 100644 index 000000000..87e110776 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientExit.properties @@ -0,0 +1,21 @@ +ClientExit=Client Exit +From\:=From: +To\:=To: +Office\:=Office: +LoanOfficer\:=Loan Officer: +Group\:=Group: +ClientID=Client ID +ClientName=Client Name +GroupName=Group Name +NationalID=National ID +ClientClosedDate=Closed Date +ExitReason=Reason +ExitNotes=Notes +NumLoans=# Loans +AmountDisbursed=Amount Disbursed +LoanOfficer=Loan Officer +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientSummary.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientSummary.properties new file mode 100644 index 000000000..157514184 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/clientSummary.properties @@ -0,0 +1,29 @@ +ClientSummary=Client Summary +Office\:=Office: +Gender\:=Gender: +VersionAt=Version 1.3 +On\:=On: +PrintedBy\:=Printed by: +Page=Page +ClientDetails=Client Details +TotalClients=Total Clients +ActiveClients=Active Clients +OnHoldClients=On Hold Clients +ClosedClients=Closed Clients +ActiveCenters=Active Centers +ActiveGroups=Active Groups +MaritalStatus=Marital Status +EducationLevel=Education Level +PovertyStatus=Poverty Status +Gender=Gender +Age=Age +18to25=18 - 25 +26to30=26 - 30 +31to35=31 - 35 +36to40=36 - 40 +41to45=41 - 45 +46to50=46 - 50 +above50=> 50 +Citizenship=Citizenship +Ethinicity=Ethinicity + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoanSummaryBranch.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoanSummaryBranch.properties new file mode 100644 index 000000000..7d2c33411 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoanSummaryBranch.properties @@ -0,0 +1,13 @@ +ClosedLoanSummaryBranch=Closed Loan Summary per Branch +Branch=Branch +NoOfCenters=No. of Centers with Closed Loans +NoOfGroups=No. of Groups with Closed Loans +NoOfClients=No. of Clients with Closed Loans +NoOfClosedLoans=No. of Closed Loans +AmountOfClosedLoans=Total Amount of Closed Loans +InterestRepaid=Total Interest Repaid +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoans.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoans.properties new file mode 100644 index 000000000..a24cd42ac --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/closedLoans.properties @@ -0,0 +1,44 @@ +ClosedLoans=Closed Loans +Office\:=Office: +LoanOfficer\:=Loan Officer: +Center\:=Center: +LoanId=Loan Id +CenterName=Center Name +GroupName=Group Name +MemberName=Client Name +DisbursalAmount=Disbursal Amount +InterestRate=Interest Rate(%) +DisbursalDate=Disbursal Date +RepaidPrincipal=Repaid Principal +RepaidInterest=Repaid Interest +OutstandingPrincipal=Outstanding Principal +OutstandingInterest=Outstanding Interest +LoanOfficer=Loan Officer +SubTotal=Sub Total +GrandTotal\:=Grand Total: +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 +ClosureDate=Closed / Rescheduled Date +PurposeOfLoan=Purpose Of Loan +LoanCycle=Loan Cycle +FeesCollected=Fees Collected +TotalCollected=Total Collected +Reason =Reason +DaysAfterClosure=Days Since Loan Closed +NewLoanTaken=New Loan Taken +DisbursalPeriod\:=Disbursal Period: +ClosurePeriod\:=Closure Period: +To=To +(ClosedDate)From\:=(ClosedDate)From: +(ClosedDate)To\:=(ClosedDate)To: +(DisbursalDate)From\:=(DisbursalDate)From: +(DisbursalDate)To\:=(DisbursalDate)To: +CenterSubTotal\:=Center Total: +LoanOfficerSubTotal\:=Loan Officer Total: +BranchTotal\:=Branch Total: +PrincipalCollected=Principal Collected +InterestCollected=Interest Collected +NoDataAvailable=No Data Available +OfficeLoanOfficerCenter\:=Office, Loan Officer, Center: diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dormantClientsSummary.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dormantClientsSummary.properties new file mode 100644 index 000000000..8068c85cf --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dormantClientsSummary.properties @@ -0,0 +1,13 @@ +DormantClientsSummary=Dormant Clients Summary +Branch=Branch +NoCenters=No. of Centers +NoGroups=No. of Groups +NoClients=No. of Clients +NoClientsWithoutActiveLoan=No. of Clients without Active Loan +LongestTimeWithoutLoan=Longest Time without Loan +ActiveClientsDormant=% of Active Clients that are Dormant +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch.properties new file mode 100644 index 000000000..eb6b465f3 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch.properties @@ -0,0 +1,20 @@ +DuevsCollectedBranch=Due vs Collected by Branch +From\:=From: +To\:=To: +ReportDate\:= Report Date: +Office\:=Office: +On\:=On: +RepaymentDate=Repayment Date +Due=Due +Collections*=Collections* +Principal=Principal +Interest=Interest +FP=Fees / Penalties +ArrearsPrincipal=Arrears Principal +ArrearsInterest=Arrears Interest +Total=Total +Page=Page +GrandTotal=Grand Total +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Printed by: +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_es.properties new file mode 100644 index 000000000..d279ae5d1 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_es.properties @@ -0,0 +1,20 @@ +DuevsCollectedBranch=Debido contra Completo por Rama +From\:=De: +To\:=A: +ReportDate\:=Informe la Fecha: +Office\:=Oficina: +On\:En: +RepaymentDate=Fecha de devolución +Due=Debido +Collections*=Reunió* +Principal=Director +Interest=Interese +FP=Honorarios / Sanciones +ArrearsPrincipal=Atrasos Principales +ArrearsInterest=Atrasos Interese +Total=Suma +Page=Página +GrandTotal=Suma total +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Impreso: +VersionAt=La versión 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_fr.properties new file mode 100644 index 000000000..ee7902681 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedBranch_fr.properties @@ -0,0 +1,20 @@ +DuevsCollectedBranch=Le dû contre le Complèt par la Branche +From\:=De: +To\:=À: +ReportDate\:=Rapporter la Date: +Office\:=Bureau: +On\:=Sur: +RepaymentDate=Date de remboursement +Due=Dû +Collections*=Recueilli* +Principal=Principal +Interest=Intéresser +FP=Frais / Sanctions +ArrearsPrincipal=Principal d'Arriéré +ArrearsInterest=Intéresser d'Arriéré +Total=Total +Page=Page +GrandTotal=Grand Total +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Imprimé par: +VersionAt=La version 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter.properties new file mode 100644 index 000000000..d3a731e35 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter.properties @@ -0,0 +1,21 @@ +On\:=On: +To\:=To: +From\:=From: +Office\:=Office: +Total=Total +GrandTotal=Grand Total +DuevsCollectedCenter=Due vs Collected by Center +LoanOfficer\:=Loan Officer: +Center\:=Center: +RepaymentDate=Repayment Date +Due=Due +Collections*=Collections* +Principal=Principal +Interest=Interest +FP=Fees / Penalties +ArrearsPrincipal=Arrears Principal +ArrearsInterest=Arrears Interest +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Printed by: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_es.properties new file mode 100644 index 000000000..486dd20ec --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_es.properties @@ -0,0 +1,21 @@ +On\:En: +To\:=A: +From\:=De: +Office\:=Oficina: +Total=Suma +GrandTotal=Suma total +DuevsCollectedCenter=Debido contra completo por el centro +LoanOfficer\:=Preste a Oficial: +Center\:=Centro: +RepaymentDate=Fecha de devolución +Due=Debido +Collections*=Reunió* +Principal=Director +Interest=Interese +FP=Honorarios / Sanciones +ArrearsPrincipal=Atrasos Principales +ArrearsInterest=Atrasos Interese +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Impreso: +Page=Página +VersionAt=La versión 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_fr.properties new file mode 100644 index 000000000..e8de25366 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedCenter_fr.properties @@ -0,0 +1,21 @@ +On\:=Sur: +To\:=À: +From\:=De: +Office\:=Bureau: +Total=Total +GrandTotal=Grand Total +DuevsCollectedCenter=Le dû contre le Complèt par le Centre +LoanOfficer\:=Prêter l'Officier: +Center\:=Centre: +RepaymentDate=Date de remboursement +Due=Dû +Collections*=Recueilli* +Principal=Principal +Interest=Intéresser +FP=Frais / Sanctions +ArrearsPrincipal=Principal d'Arriéré +ArrearsInterest=Intéresser d'Arriéré +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Imprimé par: +Page=Page +VersionAt=La version 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer.properties new file mode 100644 index 000000000..557e0f72c --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer.properties @@ -0,0 +1,20 @@ +On\:=On: +To\:=To: +From\:=From: +Office\:=Office: +Total=Total +GrandTotal=Grand Total +DuevsCollectedLoanOfficer=Due vs Collected by Loan Officer +LoanOfficer\:=Loan Officer: +RepaymentDate=Repayment Date +Due=Due +Collections*=Collections* +Principal=Principal +Interest=Interest +FP=Fees / Penalties +ArrearsPrincipal=Arrears Principal +ArrearsInterest=Arrears Interest +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Printed by: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_es.properties new file mode 100644 index 000000000..ccd973c16 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_es.properties @@ -0,0 +1,20 @@ +On\:En: +To\:=A: +From\:=De: +Office\:=Oficina: +Total=Suma +GrandTotal=Suma total +DuevsCollectedLoanOfficer=Debido contra Completo por Presta a Oficial +LoanOfficer\:=Preste a Oficial: +RepaymentDate=Fecha de devolución +Due=Debido +Collections*=Reunió* +Principal=Director +Interest=Interese +FP=Honorarios / Sanciones +ArrearsPrincipal=Atrasos Principales +ArrearsInterest=Atrasos Interese +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Impreso: +Page=Página +VersionAt=La versión 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_fr.properties new file mode 100644 index 000000000..ac2a2b9b6 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/dueVsCollectedLoanOfficer_fr.properties @@ -0,0 +1,20 @@ +On\:=Sur: +To\:=À: +From\:=De: +Office\:=Bureau: +Total=Total +GrandTotal=Grand Total +DuevsCollectedLoanOfficer=Le dû contre le Complèt par Prête Officier +LoanOfficer\:=Prêter l'Officier: +RepaymentDate=Date de remboursement +Due=Dû +Collections*=Recueilli* +Principal=Principal +Interest=Intéresser +FP=Frais / Sanctions +ArrearsPrincipal=Principal d'Arriéré +ArrearsInterest=Intéresser d'Arriéré +*TheseAmountsDoNotIncludeAdjustedNorReversedPayments=* These amounts do not include adjusted nor reversed payments +PrintedBy\:=Imprimé par: +Page=Page +VersionAt=La version 1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/fundsMovement.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/fundsMovement.properties new file mode 100644 index 000000000..702d753a6 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/fundsMovement.properties @@ -0,0 +1,23 @@ +FundsMovement=Funds Movement +AsOf\:=As Of: +Office\:=Office: +SourceOfFunds\:=Source of Funds: +MifosAccountId=Account ID +ClientName=Client name +LoanDisbursalDate=Loan Disbursal Date +LoanAmountDisbursed=Loan Amount Disbursed +NoOfInstallments=No. of Installments +InstallmentFrequency=Installment Frequency +PrincipalOutstanding=Principal Outstanding +InterestOutstanding=Interest Outstanding +LoanFeesOutstanding=Loan Fees Outstanding +ArrearsAmount=Arrears Amount +ArrearsDays=Arrears Days +SubTotal=Sub Total +week\(s\)=week(s) +month\(s\)=month(s) +day\(s\)=day(s) +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupCollectionSheetMPESA.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupCollectionSheetMPESA.properties new file mode 100644 index 000000000..886b8efad --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupCollectionSheetMPESA.properties @@ -0,0 +1,38 @@ +GroupCollectionSheetMPESA=Group Collection Sheet (MPESA) +MeetingDate\:=Meeting Date: +Office\:=Office: +LoanOfficer\:=Loan Officer: +TopCustomerHierarchyId\:=Top Customer Hierarchy Id: +BranchOffice\:=Branch Office: +MeetingLocation\:=Meeting Location: +GroupName\:=Group Name: +GovtID=Govt ID +Name=Name +PhoneNo=Phone No. +Att*=Att* +*Att_P,A,L,AA=* Att = P, A, L, AA +Product=Product +Balance=Balance +Due=Due +Collected=Collected +Principal=Principal +Interest=Interest +F/P=F/P +TOTAL=TOTAL +TOTAL\:=TOTAL: +ReceiptId=Receipt Id +WithdrawalsDisbursals=Withdrawals / Disbursals +AllowableLoanAmount=Allowable Loan Amount +RequestedLoanAmount=Requested Loan Amount +Summary=Summary +TotalAmountDue\:=Total Amount Due: +Collection=Collection +AmountCollectedBeforeMeeting\:=Amount Collected Before Meeting: +AmountCollectedDuringMeeting\:=Amount Collected During Meeting: +Issues=Issues +TotalWithdrawalIssued\:=Total Withdrawal Issued: +TotalDisbursalsIssued\:=Total Disbursals Issued: +NETCOLLECTION\:=NET COLLECTION: +Signature=Signature +VersionAt=v1.3 +Page=Page diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupsInformation.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupsInformation.properties new file mode 100644 index 000000000..a8b68e961 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/groupsInformation.properties @@ -0,0 +1,23 @@ +GroupsInformation=Groups Information +Office\:=Office: +Gender\:=Gender: +VersionAt=Version 1.3 +On\:=On: +PrintedBy\:=Printed by: +Page=Page +BranchName=Branch Name +NoOfCenters=No. of Centers +NoOfActiveGroups=No. of Active Groups +NoOfOnHoldGroups=No. of On Hold Groups +NoOfClosedGroups=No. of Closed Groups +ExternalId=External ID +CenterName=Center Name +GroupName=Group Name +SystemId=System ID +FormationDate=Formation Date +GroupSize=Group Size +Status=Status +ClosedReason=Closed Reason +ClosedDate=Closed Date +LoanOfficer=Loan Officer + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanAging.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanAging.properties new file mode 100644 index 000000000..a193ac815 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanAging.properties @@ -0,0 +1,22 @@ +AgingTable=Aging Table +loans=# Of Loans +LoanAmount=Loan Amount +PrincipalInArrears=Principal In Arrears +InterestInArrears=Interest In Arrears +PrincipalOutstanding=Principal Outstanding +InterestOutstanding=Interest Outstanding +BalanceOutstanding=Balance Outstanding +LastReceived=Last Received +CustomerName=Customer Name +CustomerId=Customer Id +ageingReport=Ageing Report +Period\:=Period: +SlNo=Sl No +SubTotal\:=Period Total: +Total\:=Total: +AsOnDate\:=As On Date: +PrintedBy\:=Printed by: +On\:=On: +Page=Page +Office\:=Office: +BranchName\:=BranchName: \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct.properties new file mode 100644 index 000000000..5a851c622 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct.properties @@ -0,0 +1,20 @@ +ProductName=Product Name +DuringPeriod=During Period +OfLoansDisbursed=# of Loans Disbursed +AmountDisbursed=Amount Disbursed +Cumulative=Cumulative +EndOfPeriod=End of Period +AmountOutstanding=Amount Outstanding +ArrearsAmount=Arrears Amount +PAR=PAR +OfActiveLoans=# of Active Loans +OfPortfolioAmount=% of Portfolio Amount +Office\:=Office: +Total=Total +LoanClassificationProduct=Loan Classification by Product +VersionAt=Version 1.3 +From\:=From: +To\:=To: +PrintedBy\:=Printed by: +On\:=On: +Page=Page diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_es.properties new file mode 100644 index 000000000..4e577de06 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_es.properties @@ -0,0 +1,20 @@ +ProductName=Nombre de producto +DuringPeriod=Durante el Período +OfLoansDisbursed=# de Préstamos Desembolsados +AmountDisbursed=Cantidad Desembolsado +Cumulative=Acumulativos +EndOfPeriod=El Fin del Período +AmountOutstanding=Cantidad Sobresaliente +ArrearsAmount=Atrasos Cantidad +PAR=IGUALDAD +OfActiveLoans=# de Activo Préstamos +OfPortfolioAmount=% de Cartera Cantidad +Office\:=Oficina: +Total=Suma +LoanClassificationProduct=Preste Clasificación por Producto +VersionAt=La versión 1,3 +From\:=De: +To\:=A: +Page=Página +PrintedBy\:=Impreso: +On\:=En: diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_fr.properties new file mode 100644 index 000000000..451f43e2d --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanClassificationProduct_fr.properties @@ -0,0 +1,20 @@ +ProductName=Nom de produit +DuringPeriod=Pendant la Période +OfLoansDisbursed=# d'Emprunts Déboursé +AmountDisbursed=Quantité Déboursé +Cumulative=Cumulatifs +EndOfPeriod=La Fin de Période +AmountOutstanding=Quantité Remarquable +ArrearsAmount=Arriéré Quantité +PAR=PAIR +OfActiveLoans=# d'Actif Emprunts +OfPortfolioAmount=% de Portefeuille Quantité +Office\:=Bureau: +Total=Total +LoanClassificationProduct=Prêter sous-Produit de Classification +VersionAt=La version 1,3 +From\:=De: +To\:=A: +PrintedBy\:=Imprimé par: +On\:=Sur: +Page=Page diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed.properties new file mode 100644 index 000000000..3f1f37090 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed.properties @@ -0,0 +1,71 @@ +LoanOfficerDetailed=Loan Officer Detailed +LoanOfficer\:=LoanOfficer: +Office\:=Office: +Gender\:=Gender: +GovernmentID\:=Government ID: +From\:=From: +To\:=To: +On\:=On: +PrintedBy\:=Printed by: +Page=Page +GroupFormation=Group Formation +ofClientsadded(ToDate)=# of Clients added(To Date) +ofClientsadded(ThisPeriod)=# of Clients added(This Period) +ofgroupsadded(ToDate)=# of Groups added(To Date) +ofgroupsadded(ThisPeriod)=# of Groups added(This Period) +ofdropoutclients(ToDate)=# of Dropout clients(To Date) +ofdropoutclients(ThisPeriod)=# of Dropout clients(This Period) +KeyDates=Key Dates +JoinedMFI=Joined MFI +BecameLoanOfficer=Became Loan Officer +BranchMemberships=Branch Memberships +LeftMFI=Left MFI +ClientSummary=Client Summary +ofCenters=# of Centers +ofgroups=# of Groups +ofClients=# of Clients +ofClientswithloans=# of Clients with loans +ofClientswithsavings=# of Clients with savings +Dormantclients=Dormant Clients (no loans) +AccountSummary=Account Summary +ofactiveloans=# of Active Loans +PrincipalAmountOutstanding=Principal Amount Outstanding +InterestAmountOutstanding=Interest Amount Outstanding +PortfolioatRisk%=Portfolio at Risk % +ofloanswrittenoff=# of Loans written off +Amountofloanswrittenoff=Amount of loans written off +TotalSavings=Total Savings +VoluntarySavings=Voluntary Savings +MandatorySavings=Mandatory Savings +AginginArrearsbyWeek=Aging in Arrears by Week +ofLoans=# of Loans +PrincipalOutstanding=Principal Outstanding +Total=Total +AginginArrearsbyDays=Aging in Arrears by Days +Name=Name +Clients=Clients +PrincipalOutstanding=Principal Outstanding +CenterSavings=Center Savings +ArrearsAmount=Arrears Amount +PARratio=PAR ratio +VersionAt=Version 1.3 +SummaryofCenters(Groups)Managed=Summary of Centers (Groups) Managed +Current=Current +1\ Week\ in\ Arrears=1 Week in Arrears +2\ Weeks\ in\ Arrears=2 Weeks in Arrears +3\ Weeks\ in\ Arrears=3 Weeks in Arrears +4\ Weeks\ in\ Arrears=4 Weeks in Arrears +5\ Weeks\ in\ Arrears=5 Weeks in Arrears +6\ Weeks\ in\ Arrears=6 Weeks in Arrears +7\ Weeks\ in\ Arrears=7 Weeks in Arrears +8\ Weeks\ in\ Arrears=8 Weeks in Arrears +9\ Weeks\ in\ Arrears=9 Weeks in Arrears +10\ Weeks\ in\ Arrears=10 Weeks in Arrears +11\ Weeks\ in\ Arrears=11 Weeks in Arrears +12\ Weeks\ in\ Arrears=12 Weeks in Arrears +12+\ Weeks\ in\ Arrears=12+ Weeks in Arrears +0\ to\ 30\ Days\ in\ Arrears=0 to 30 Days in Arrears +30\ to\ 60\ Days\ in\ Arrears=30 to 60 Days in Arrears +60\ to\ 90\ Days\ in\ Arrears=60 to 90 Days in Arrears +90\ to\ 180\ Days\ in\ Arrears=90 to 180 Days in Arrears +>\ 180\ Days\ in\ Arrears=> 180 Days in Arrears diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_es.properties new file mode 100644 index 000000000..8ad3589a6 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_es.properties @@ -0,0 +1,71 @@ +LoanOfficerDetailed=El Oficial del préstamo Detalló +LoanOfficer\:=Preste a Oficial: +Office=Oficina: +Gender\:=Género: +GovernmentID\:=Identificación del Gobierno: +From\:=De: +To\:=A: +On\:=En: +PrintedBy\:=Impreso: +Page=Página +GroupFormation=Agrupe Formación +ofClientsadded(ToDate)=# de Clientes agregó (Fechar) +ofClientsadded(ThisPeriod)=# de Clientes agregó (Este Período) +ofgroupsadded(ToDate)=# de Grupos agregados (Fechar) +ofgroupsadded(ThisPeriod)=# de Grupos agregados (Este Período) +ofdropoutclients(ToDate)=# de Clientes de abandono (Fechar) +ofdropoutclients(ThisPeriod)=# de Clientes de abandono (Este Período) +KeyDates=Fechas clave +JoinedMFI=MFI unido +BecameLoanOfficer=Llegó a ser Oficial de Préstamo +BranchMemberships=Ramifique Asociaciones +LeftMFI=MFI izquierdo +ClientSummary=Resumen de cliente +ofCenters=# de Centros +ofgroups=# de Grupos +ofClients=# de Clientes +ofClientswithloans=# de Clientes con préstamos +ofClientswithsavings=# de Clientes con ahorros +Dormantclients=Clientes inactivos (no préstamos) +AccountSummary=Dé cuenta Resumen +ofactiveloans=# de Préstamos activos +PrincipalAmountOutstanding=Principal Saldo pendiente +InterestAmountOutstanding=Interese Saldo pendiente +PortfolioatRisk%=La cartera en Riesgo % +ofloanswrittenoff=# de préstamos anuló +Amountofloanswrittenoff=La cantidad de préstamos anuló +TotalSavings=Ahorros totales +VoluntarySavings=Ahorros voluntarios +MandatorySavings=Ahorros obligatorios +AginginArrearsbyWeek=El envejecimiento atrasado por Semana +ofLoans=# de Préstamos +PrincipalOutstanding=Director Sobresaliente +Total=Suma +AginginArrearsbyDays=El envejecimiento atrasado por Días +Name=Nombre +Clients=Clientes +PrincipalOutstanding=Director Sobresaliente +CenterSavings=Centro Ahorros +ArrearsAmount=Atrasos Cantidad +PARratio=IGUALDAD relación +VersionAt=La versión 1,3 +SummaryofCenters(Groups)Managed=Resumen de los Centros (Grupos) administrado +Current=Ahora +1\ Week\ in\ Arrears=1 semana en los atrasos +2\ Weeks\ in\ Arrears=2 semanas en los atrasos +3\ Weeks\ in\ Arrears=3 semanas en los atrasos +4\ Weeks\ in\ Arrears=4 semanas en los atrasos +5\ Weeks\ in\ Arrears=5 semanas en los atrasos +6\ Weeks\ in\ Arrears=6 semanas en los atrasos +7\ Weeks\ in\ Arrears=7 semanas en los atrasos +8\ Weeks\ in\ Arrears=8 semanas en los atrasos +9\ Weeks\ in\ Arrears=9 semanas en los atrasos +10\ Weeks\ in\ Arrears=10 semanas en los atrasos +11\ Weeks\ in\ Arrears=11 semanas en los atrasos +12\ Weeks\ in\ Arrears=12 semanas en los atrasos +12+\ Weeks\ in\ Arrears=12+ semanas en los atrasos +0\ to\ 30\ Days\ in\ Arrears=0 a 30 días de atraso +30\ to\ 60\ Days\ in\ Arrears=30 a 60 días de atraso +60\ to\ 90\ Days\ in\ Arrears=60 a 90 días de atraso +90\ to\ 180\ Days\ in\ Arrears=90 a 180 días de atraso +>\ 180\ Days\ in\ Arrears=> 180 días de atraso diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_fr.properties new file mode 100644 index 000000000..fe491129c --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerDetailed_fr.properties @@ -0,0 +1,71 @@ +LoanOfficerDetailed=L'Officier d'emprunt A Détaillé +LoanOfficer\:=prêter l'officier: +Office\:=Bureau: +Gender\:=Genre: +GovernmentID\:=Identification du gouvernement: +From\:=De: +To\:=À: +On\:=Sur: +PrintedBy\:=Imprimé par: +Page=Page +GroupFormation=Formation de groupe +ofClientsadded(ToDate)=# des Clients supplémentaires (jusqu'ici) +ofClientsadded(ThisPeriod)=# des Clients s'est ajouté (cette période) +ofgroupsadded(ToDate)=# des Groupes s'est ajouté (jusqu'ici) +ofgroupsadded(ThisPeriod)=# des Groupes s'est ajouté (cette période) +ofdropoutclients(ToDate)=# des Clients de renvoi (jusqu'ici) +ofdropoutclients(ThisPeriod)=# des Clients de renvoi (cette période) +KeyDates=Dates clés +JoinedMFI=Inscrit IFM +BecameLoanOfficer=Devenu agent de crédit +BranchMemberships=Adhésions Direction +LeftMFI=Gauche IFM +ClientSummary=Résumé de client +ofCenters=# de Centres +ofgroups=# de Groupes +ofClients=# de Clients +ofClientswithloans=# de Clients avec des prêts +ofClientswithsavings=# de Clients avec des économies +Dormantclients=Les clients inactifs (pas de prêts) +AccountSummary=Expliquer le Résumé +ofactiveloans=# d'emprunts actifs +PrincipalAmountOutstanding=Remarquable principal de Quantité +InterestAmountOutstanding=Intéresser Remarquable de Quantité +PortfolioatRisk%=Le portefeuille au Risque % +ofloanswrittenoff=d'emprunts a annulé +Amountofloanswrittenoff=La quantité d'emprunts a annulé +TotalSavings=Economies totales +VoluntarySavings=Economies volontaires +MandatorySavings=Economies obligatoires +AginginArrearsbyWeek=Le vieillissement dans Arriéré par la Semaine +ofLoans=# d'Emprunts +PrincipalOutstanding=Le directeur Remarquable +Total=Total +AginginArrearsbyDays=Le vieillissement dans Arriéré par les Jours +Name=Nom +Clients=Clients +PrincipalOutstanding=Directeur Remarquable +CenterSavings=Centre Economies +ArrearsAmount=Arriéré Quantité +PARratio=PAIR rapport +VersionAt=La version 1,3 +SummaryofCenters(Groups)Managed=Le résumé de Centres (les Groupes) A Géré +Current=Actuelle +1\ Week\ in\ Arrears=1 semaine en arriéré +2\ Weeks\ in\ Arrears=2 semaines d'arriérés +3\ Weeks\ in\ Arrears=3 semaines d'arriérés +4\ Weeks\ in\ Arrears=4 semaines d'arriérés +5\ Weeks\ in\ Arrears=5 semaines d'arriérés +6\ Weeks\ in\ Arrears=6 semaines d'arriérés +7\ Weeks\ in\ Arrears=7 semaines d'arriérés +8\ Weeks\ in\ Arrears=8 semaines d'arriérés +9\ Weeks\ in\ Arrears=9 semaines d'arriérés +10\ Weeks\ in\ Arrears=10 semaines d'arriérés +11\ Weeks\ in\ Arrears=11 semaines d'arriérés +12\ Weeks\ in\ Arrears=12 semaines d'arriérés +12+\ Weeks\ in\ Arrears=12+ semaines d'arriérés +0\ to\ 30\ Days\ in\ Arrears=0 à 30 jours d'arriérés +30\ to\ 60\ Days\ in\ Arrears=30 à 60 jours d'arriérés +60\ to\ 90\ Days\ in\ Arrears=60 à 90 jours d'arriérés +90\ to\ 180\ Days\ in\ Arrears=90 à 180 jours d'arriérés +>\ 180\ Days\ in\ Arrears=> 180 jours d'arriérés diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative.properties new file mode 100644 index 000000000..6ecfd516a --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative.properties @@ -0,0 +1,26 @@ +LoanOfficerPerformanceSummaryCumulative=Loan Officer Performance Summary (Cumulative) +Branch\:=Office: +On\:=On: +DuringPeriod=During Period +Asof=As of +Name=Name +Datejoined=Date Joined +ofcenters=# of Centers +ofgroups=# of Groups +ofclients=# of Clients +women=% Women +ofloans=# of Loans +Portfolio=Loan Amount +OutstandingPrincipal=Principal Outstanding +OutstandingInterest=Interest Outstanding +PrincipalOverdue=Principal Overdue +CurrentPAR=Current PAR +TotalSavings=Total Savings +TotalAsofdate=Total As of Date +GrandTotalAsofdate=Grand Total As of Date +VersionAt=Version 1.3 +From\:=From: +To\:=To: +Page=Page +PrintedBy\:=Printed by: + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_es.properties new file mode 100644 index 000000000..b2ecf6593 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_es.properties @@ -0,0 +1,26 @@ +LoanOfficerPerformanceSummaryCumulative=Preste a Oficial de Resumen de Desempeño (Acumulativos) +Branch:\=Oficina: +On\:=En: +DuringPeriod=Durante Período +Asof=Al +Name=Nombre +Datejoined=La fecha unió +ofcenters=# de centros +ofgroups=# de grupos +ofclients=# de clientes +women=% mujeres +ofloans=# de préstamos +Portfolio=Cartera +OutstandingPrincipal=Sobresaliente director +OutstandingInterest=Sobresaliente interés +PrincipalOverdue=Director atrasado +CurrentPAR=Actual IGUALDAD +TotalSavings=Totales Ahorros +TotalAsofdate=La Suma total Al fecha +GrandTotalAsofdate=La Suma total Al fecha +VersionAt=La versión 1,3 +From\:=De: +To\:=A: +Page=Página +PrintedBy\:=Impreso: + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_fr.properties new file mode 100644 index 000000000..fda784e25 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryCumulative_fr.properties @@ -0,0 +1,26 @@ +LoanOfficerPerformanceSummaryCumulative=Prêter de Résumé d'exécution d'Officier (Cumulatifs) +Branch\:=bureau: +On\:=Sur: +DuringPeriod=Pendant la Période +Asof=Depuis +Name=Nom +Datejoined=La date a joint +ofcenters=# de centres +ofgroups=# de groupes +ofclients=# de clients +women=% femmes +ofloans=# d'emprunts +Portfolio=Portefeuille +OutstandingPrincipal=Remarquable directeur +OutstandingInterest=Remarquable intérêt +PrincipalOverdue=Directeur eu retard +CurrentPAR=Actuel PAIR +TotalSavings=Totale economies +TotalAsofdate=Le Total depuis Date +GrandTotalAsofdate=Le Total depuis Date +VersionAt=La version 1,3 +From\:=De: +To\:=À: +Page=Page +PrintedBy\:=Imprimé par: + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod.properties new file mode 100644 index 000000000..abb07cca2 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod.properties @@ -0,0 +1,22 @@ +LoanOfficerPerformanceSummaryDuringPeriod=Loan Officer Performance Summary (During Period) +Branch\:=Office: +On\:=On: +Name=Name +Datejoined=Date Joined +VersionAt=Version 1.3 +PrintedBy\:=Printed by: +Page=Page +From\:=From: +To\:=To: +TotalGroupsFormed=Total Groups Formed +TotalLoansDisbursed=Total Loans Disbursed +TotalAmtOfLoansDisbursed=Total amt of Loans Disbursed +TotalAmtOfLoansRepaid=Total amt of Loans Repaid +TotalNewSavingsAccounts=Total # new Savings Accounts +TotalClientsRecruited=Total Clients Recruited +TotalAmtSavingsDeposits=Total amt Savings Deposits +TotalAmtSavingsWithdrawals=Total amt Savings Withdrawals +ofDropouts=# of Dropouts +TotalDuringPeriod=Total During Period +GrandTotalDuringPeriod=Grand Total During Period + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_es.properties new file mode 100644 index 000000000..1a6b2b24b --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_es.properties @@ -0,0 +1,22 @@ +LoanOfficerPerformanceSummaryDuringPeriod=Preste a Oficial de Resumen de Desempeño (Durante Período) +Branch:\=Oficina: +On\:=En: +Name=Nombre +Datejoined=La fecha unió +TotalGroupsFormed=Totales grupos formaron +TotalLoansDisbursed=Totales préstamos desembolsado +TotalAmtOfLoansDisbursed=El cantidad total de préstamos desembolsó +TotalAmtOfLoansRepaid=El cantidad total de préstamos devolvieron +TotalNewSavingsAccounts=Totalice nuevo ahorros cuentas +TotalClientsRecruited=Totales clientes alistaron +TotalAmtSavingsDeposits=Cantidad total ahorros depósitos +TotalAmtSavingsWithdrawals=Ahorros de cantidad total retiradas +ofDropouts=# de abandonos +TotalDuringPeriod=La Suma total Durante el Período +GrandTotalDuringPeriod=La Suma total Durante el Período +VersionAt=La versión 1,3 +From\:=De: +To\:=A: +Page=Página +PrintedBy\:=Impreso: + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_fr.properties new file mode 100644 index 000000000..f71878b5b --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loanOfficerPerformanceSummaryDuringPeriod_fr.properties @@ -0,0 +1,22 @@ +LoanOfficerPerformanceSummaryDuringPeriod=Prêter de Résumé d'exécution d'Officier (Pendant la Période) +Branch\:=Bureau: +On\:=Sur: +Name=Nom +Datejoined=La date a joint +TotalGroupsFormed=Totaux groupes formé +TotalLoansDisbursed=Emprunts totaux déboursé +TotalAmtOfLoansDisbursed=La quantité totale de emprunts a déboursé +TotalAmtOfLoansRepaid=La quantité totale de emprunts a remboursé +TotalNewSavingsAccounts=Totaliser nouveau économies comptes +TotalClientsRecruited=Totaux clients recruté +TotalAmtSavingsDeposits=Quantité totale économies dépôts +TotalAmtSavingsWithdrawals=Les économies totales de quantité retraits +ofDropouts=# de Marginaux +TotalDuringPeriod=Le Total Pendant la Période +GrandTotalDuringPeriod=Le Total Pendant la Période +VersionAt=La version 1,3 +From\:=De: +To\:=À: +Page=Page +PrintedBy\:=Imprimé par: + diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansPendingApproval.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansPendingApproval.properties new file mode 100644 index 000000000..ba8c93292 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansPendingApproval.properties @@ -0,0 +1,20 @@ +LoansPendingApproval=Loans Pending Approval +AsOf\:=As Of: +Office\:=Office: +Office=Office +LoanOfficer\:=Loan Officer: +Product\:=Product: +AccountId=Account ID +ClientName=Client name +GroupName=Group name +CenterName=Center name +ProductName=Product name +LoanOfficer=Loan Officer +LoanAmount=Loan Amount +TimeInExistingStatus(days)=Days Pending +LastEditedBy=Last edited by +PrintedBy\:=Printed by: +Page=Page +On\:=On: +VersionAt=Version 1.3 +Total=Total diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansToBeDisbursed.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansToBeDisbursed.properties new file mode 100644 index 000000000..be3337fd0 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/loansToBeDisbursed.properties @@ -0,0 +1,20 @@ +LoansToBeDisbursed=Loans To Be Disbursed +AsOf\:=As Of: +Office\:=Office: +Office=Office +LoanOfficer\:=Loan Officer: +Product\:=Product: +AccountId=Account ID +ClientName=Client name +GroupName=Group name +CenterName=Center name +ProductName=Product name +LoanOfficer=Loan Officer +LoanAmount=Loan Amount +TimeInExistingStatus(days)=Days awaiting disbursal +LastEditedBy=Last edited by +PrintedBy\:=Printed by: +Page=Page +On\:=On: +VersionAt=Version 1.3 +Total=Total diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress.properties new file mode 100644 index 000000000..d431bf85a --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress.properties @@ -0,0 +1,72 @@ +Office\:=Office: +Period1\:=Period 1: +Period2\:=Period 2: +On\:=On: +OUTREACH=OUTREACH +%Change=% Change +PORTFOLIO=PORTFOLIO +PAR=PAR +Numberofactiveloans=Number of Active Loans +TotalActiveLoans=Total # Active Loans +DisbursalAmount=Disbursal Amount +TotalDisbursalAmount=Total Disbursal Amount +ActiveLoansOutstanding=Active Loans Outstanding +TotalOutstandingAmount=Total Outstanding Amount +Numberofsavingsaccounts=Number of Savings Accounts +TotalSavingsAccounts=Total Savings Accounts +TotalDeposits=Total Deposits +TotalSavingsDeposits=Total Savings Deposits +INCOME=INCOME +Total=Total +AGINGANALYSIS=AGING ANALYSIS +ArrearsAmount=Arrears Amount +PARratio=PAR Ratio +AGINGTABLE=AGING TABLE +ofloans=# of Loans +Outstanding=Outstanding +PERFORMANCERATIOS=PERFORMANCE RATIOS +Activeborrowersperloanofficer=Active Borrowers per Loan Officer +Netloanportfolioperloanofficer=Net Loan Portfolio per Loan Officer +Averageportfolioperactiveborrower=Average Portfolio per Active Borrower +Activeclientsperbranch=Active Clients per Branch +Averageclientspercenter=Average Clients per Center +Averageclientsperloanofficer=Average Clients per Loan Officer +Averageclientsperbranch=Average Clients per Branch +Averageloanoutstanding=Average Loan Outstanding +Averageprincipaloutstanding=Average Principal Outstanding +Averageinterestoutstanding=Average Interest Outstanding +Averageloansize=Average Loan Size +MFIProgress=MFI Progress +VersionAt=Version 1.3 +Page=Page +PrintedBy\:=Printed by: +Jan=Jan +Feb=Feb +Mar=Mar +Apr=Apr +May=May +Jun=Jun +Jul=Jul +Aug=Aug +Sep=Sep +Oct=Oct +Nov=Nov +Dec=Dec +1\ Week\ in\ Arrears=1 Week in Arrears +2\ Weeks\ in\ Arrears=2 Weeks in Arrears +3\ Weeks\ in\ Arrears=3 Weeks in Arrears +4\ Weeks\ in\ Arrears=4 Weeks in Arrears +5\ Weeks\ in\ Arrears=5 Weeks in Arrears +6\ Weeks\ in\ Arrears=6 Weeks in Arrears +7\ Weeks\ in\ Arrears=7 Weeks in Arrears +8\ Weeks\ in\ Arrears=8 Weeks in Arrears +9\ Weeks\ in\ Arrears=9 Weeks in Arrears +10\ Weeks\ in\ Arrears=10 Weeks in Arrears +11\ Weeks\ in\ Arrears=11 Weeks in Arrears +12\ Weeks\ in\ Arrears=12 Weeks in Arrears +12+\ Weeks\ in\ Arrears=12+ Weeks in Arrears +0\ to\ 30\ Days\ in\ Arrears=0 to 30 Days in Arrears +30\ to\ 60\ Days\ in\ Arrears=30 to 60 Days in Arrears +60\ to\ 90\ Days\ in\ Arrears=60 to 90 Days in Arrears +90\ to\ 180\ Days\ in\ Arrears=90 to 180 Days in Arrears +>\ 180\ Days\ in\ Arrears=> 180 Days in Arrears diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_es.properties new file mode 100644 index 000000000..ad55232fd --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_es.properties @@ -0,0 +1,72 @@ +Office\:=Oficina: +Period1\:=El período 1: +Period2\:=El período 2: +On\:=En: +OUTREACH=ALCANCE +%Change=% Cambio +PORTFOLIO=CARTERA +Numberofactiveloans=El número de préstamos activos +TotalActiveLoans=Total # los Préstamos Activos +DisbursalAmount=Desembolso Importe +TotalDisbursalAmount=Cantidad total de Desembolso +ActiveLoansOutstanding=Activo Saldo de créditos +PAR=IGUALDAD +TotalOutstandingAmount=Cantidad Sobresaliente total +Numberofsavingsaccounts=El número de cuentas de ahorros +TotalSavingsAccounts=Cuentas de ahorros totales +TotalDeposits=Depósitos totales +TotalSavingsDeposits=Los Ahorros totales Depositan +NCOME=INGRESOS +Total=Suma +AGINGANALYSIS=El ENVEJECIMIENTO el ANALISIS +ArrearsAmount=Atrasos Cantidad +PARratio=IGUALDAD relación +AGINGTABLE=El ENVEJECIMIENTO MESA +ofloans=# de préstamos +Outstanding=Sobresaliente +PERFORMANCERATIOS=PROPORCIONES de DESEMPEÑO +Activeborrowersperloanofficer=Los prestatarios activos por presta a oficial +Netloanportfolioperloanofficer=La cartera neta del préstamo por presta a oficial +Averageportfolioperactiveborrower=La cartera media por prestatario activo +Activeclientsperbranch=Los clientes activos por rama +Averageclientspercenter=Los clientes medios por el centro +Averageclientsperloanofficer=Los clientes medios por presta a oficial +Averageclientsperbranch=Los clientes medios por rama +Averageloanoutstanding=El promedio presta sobresaliente +Averageprincipaloutstanding=Director medio sobresaliente +Averageinterestoutstanding=El promedio interesa sobresaliente +Averageloansize=Tamaño medio de préstamo +MFIProgress=Sobre el progreso de MFI +VersionAt=Versión1.3 +Page=Página +PrintedBy\:=Impreso: +Jan=Ene +Feb=Feb +Mar=Mar +Apr=Abr +Mai=May +Jun=Jun +Jul=Jul +Aug=Ago +Sep=Sep +Oct=Oct +Nov=Nov +Dec=Dic +1\ Week\ in\ Arrears=1 semana en los atrasos +2\ Weeks\ in\ Arrears=2 semanas en los atrasos +3\ Weeks\ in\ Arrears=3 semanas en los atrasos +4\ Weeks\ in\ Arrears=4 semanas en los atrasos +5\ Weeks\ in\ Arrears=5 semanas en los atrasos +6\ Weeks\ in\ Arrears=6 semanas en los atrasos +7\ Weeks\ in\ Arrears=7 semanas en los atrasos +8\ Weeks\ in\ Arrears=8 semanas en los atrasos +9\ Weeks\ in\ Arrears=9 semanas en los atrasos +10\ Weeks\ in\ Arrears=10 semanas en los atrasos +11\ Weeks\ in\ Arrears=11 semanas en los atrasos +12\ Weeks\ in\ Arrears=12 semanas en los atrasos +12+\ Weeks\ in\ Arrears=12+ semanas en los atrasos +0\ to\ 30\ Days\ in\ Arrears=0 a 30 días de atraso +30\ to\ 60\ Days\ in\ Arrears=30 a 60 días de atraso +60\ to\ 90\ Days\ in\ Arrears=60 a 90 días de atraso +90\ to\ 180\ Days\ in\ Arrears=90 a 180 días de atraso +>\ 180\ Days\ in\ Arrears=> 180 días de atraso diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_fr.properties new file mode 100644 index 000000000..b323d2766 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mfiProgress_fr.properties @@ -0,0 +1,72 @@ +Office\:=Bureau: +Period1\:=La période 1: +Period2\:=La période 2: +On\:=Sur: +OUTREACH=ASSISTANCE +%Change=% Changement +PORTFOLIO=PORTEFEUILLE +Numberofactiveloans=Le nombre d'emprunts actifs +TotalActiveLoans=Total # les Emprunts Actifs +DisbursalAmount=Montant Décaissement +TotalDisbursalAmount=La Quantité totale de Disbursal +ActiveLoansOutstanding=Prêts en cours en suspens +PAR=PAIR +TotalOutstandingAmount=La Quantité Remarquable totale +Numberofsavingsaccounts=Le nombre d'économies explique +TotalSavingsAccounts=Les Economies totales Expliquent +TotalDeposits=Dépôts totaux +TotalSavingsDeposits=Les Economies totales Déposent +INCOME=REVENU +Total=Total +AGINGANALYSIS=ANALYSE DE VIEILLISSEMENT +ArrearsAmount=Arriéré Quantité +PARratio=PAIR rapport +AGINGTABLE=TABLE DE VIEILLISSEMENT +ofloans=# d'emprunts +Outstanding=Remarquable +PERFORMANCERATIOS=PROPORTIONS D'EXECUTION +Activeborrowersperloanofficer=Les emprunteurs actifs par prête l'officier +Netloanportfolioperloanofficer=Le portefeuille net d'emprunt par prête l'officier +Averageportfolioperactiveborrower=Le portefeuille moyen par l'emprunteur actif +Activeclientsperbranch=Les clients actifs par la branche +Averageclientspercenter=Les clients moyens par le centre +Averageclientsperloanofficer=Les clients moyens par prête l'officier +Averageclientsperbranch=Les clients moyens par la branche +Averageloanoutstanding=La moyenne prête remarquable +Averageprincipaloutstanding=Le directeur moyen remarquable +Averageinterestoutstanding=La moyenne s'intéresse remarquable +Averageloansize=La taille moyenne d'emprunt +MFIProgress=MFI d'avancement +VersionAt=La version 1,3 +Page=Page +PrintedBy\:=Imprimé par: +Jan=Jan +Feb=Fév +Mar=Mar +Apr=Avr +Mai=Mai +Jun=Jun +Jul=Jul +Aug=Aoû +Sep=Sep +Oct=Oct +Nov=Nov +Dec=Déc +1\ Week\ in\ Arrears=1 semaine en arriéré +2\ Weeks\ in\ Arrears=2 semaines d'arriérés +3\ Weeks\ in\ Arrears=3 semaines d'arriérés +4\ Weeks\ in\ Arrears=4 semaines d'arriérés +5\ Weeks\ in\ Arrears=5 semaines d'arriérés +6\ Weeks\ in\ Arrears=6 semaines d'arriérés +7\ Weeks\ in\ Arrears=7 semaines d'arriérés +8\ Weeks\ in\ Arrears=8 semaines d'arriérés +9\ Weeks\ in\ Arrears=9 semaines d'arriérés +10\ Weeks\ in\ Arrears=10 semaines d'arriérés +11\ Weeks\ in\ Arrears=11 semaines d'arriérés +12\ Weeks\ in\ Arrears=12 semaines d'arriérés +12+\ Weeks\ in\ Arrears=12+ semaines d'arriérés +0\ to\ 30\ Days\ in\ Arrears=0 à 30 jours d'arriérés +30\ to\ 60\ Days\ in\ Arrears=30 à 60 jours d'arriérés +60\ to\ 90\ Days\ in\ Arrears=60 à 90 jours d'arriérés +90\ to\ 180\ Days\ in\ Arrears=90 à 180 jours d'arriérés +>\ 180\ Days\ in\ Arrears=> 180 jours d'arriérés diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions.properties new file mode 100644 index 000000000..aa6e82b04 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions.properties @@ -0,0 +1,25 @@ +MifosTransactions-Detail=Mifos Transactions - Detail +MifosTransactions-Summary=Mifos Transactions - Summary +Branch\:=Branch: +TransactionID=Transaction ID +ReceiptNo=Receipt No. +ClientPhoneNo=Client Phone No. +GroupName=Group Name +NationalID=National ID +AccountID=Account ID +ClientName=Client Name +TransactionType=Transaction Type +Receipts=Receipts +Disbursements=Disbursements +MifosUser=Mifos User +ActionDate=Action Date +CreatedDate=Created Date +Adjusted=Adjusted +NoDataAvailable=No Data Available +TotalTransactions=Total Transactions = +TotalReceipts=Total Receipts = +TotalDisbursements=Total Disbursements = +ReportDate=Report Date +PrintedBy\:=Printed by: +MFIName\:=MFI Name: +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_es.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_es.properties new file mode 100644 index 000000000..82cbbcf60 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_es.properties @@ -0,0 +1,25 @@ +MifosTransactions-Detail=Mifos Transactions - Detail +MifosTransactions-Summary=Mifos Transactions - Summary +Branch\:=Branch: +TransactionID=Transaction ID +ReceiptNo=Receipt No. +ClientPhoneNo=Client Phone No. +GroupName=Group Name +NationalID=National ID +AccountID=Account ID +ClientName=Client Name +TransactionType=Transaction Type +Receipts=Receipts +Disbursements=Disbursements +MifosUser=Mifos User +ActionDate=Action Date +CreatedDate=Created Date +Adjusted=Adjusted +NoDataAvailable=No Data Available +TotalTransactions=Total Transactions = +TotalReceipts=Total Receipts = +TotalDisbursements=Total Disbursements = +ReportDate=Report Date +PrintedBy\:=Printed by: +MFIName\:=MFI Name: +VersionAt=Versión1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_fr.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_fr.properties new file mode 100644 index 000000000..f4e923d58 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/mifosTransactions_fr.properties @@ -0,0 +1,25 @@ +MifosTransactions-Detail=Mifos Transactions - Detail +MifosTransactions-Summary=Mifos Transactions - Summary +Branch\:=Branch: +TransactionID=Transaction ID +ReceiptNo=Receipt No. +ClientPhoneNo=Client Phone No. +GroupName=Group Name +NationalID=National ID +AccountID=Account ID +ClientName=Client Name +TransactionType=Transaction Type +Receipts=Receipts +Disbursements=Disbursements +MifosUser=Mifos User +ActionDate=Action Date +CreatedDate=Created Date +Adjusted=Adjusted +NoDataAvailable=No Data Available +TotalTransactions=Total Transactions = +TotalReceipts=Total Receipts = +TotalDisbursements=Total Disbursements = +ReportDate=Report Date +PrintedBy\:=Printed by: +MFIName\:=MFI Name: +VersionAt=Versión1,3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/outreachSummaryBranch.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/outreachSummaryBranch.properties new file mode 100644 index 000000000..7874bbdd9 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/outreachSummaryBranch.properties @@ -0,0 +1,14 @@ +OutreachSummaryBranch=Outreach Summary by Branch +Branch=Branch +Centers=Centers +Groups=Groups +ActiveClients=Active Clients +ActiveBorrowers=Active Borrowers +ActiveDepositors=Active Depositors +Dropouts=Dropouts +NoOfLoanOfficers=No. of Loan Officers +Total=Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 diff --git a/mifosng-provider/src/main/resources/org/mifos/bi/reports/overdueMatureLoans.properties b/mifosng-provider/src/main/resources/org/mifos/bi/reports/overdueMatureLoans.properties new file mode 100644 index 000000000..c5b994372 --- /dev/null +++ b/mifosng-provider/src/main/resources/org/mifos/bi/reports/overdueMatureLoans.properties @@ -0,0 +1,34 @@ +OverdueMatureLoans=Overdue Mature Loans +Office\:=Office: +LoanOfficer\:=Loan Officer: +SystemId=System Id +LoanId=Loan Id +overdueMatureLoans\:=Show Only Mature Loans: +GroupName=Group Name +MemberName=Client Name +DisbursedAmount=Disbursed Amount +InterestRate=Interest Rate +DisbursalDate=Disbursal Date +PrincipalRepaid=Principal Repaid +InterestRepaid=Interest Repaid +PrincipalOutstanding=Principal Outstanding +InterestOutstanding=Interest Outstanding +LoanOfficer=Loan Officer +Total=Total +SubTotal=Sub Total +GrandTotal=Grand Total +PrintedBy\:=Printed by: +On\:=On: +Page=Page +VersionAt=Version 1.3 +LastPaidDate=Last Paid Date +PrincipalOverdue=Principal Overdue +InterestOverdue=Interest Overdue +%PrincipalOverdue=Principal Overdue(%) +PurposeOfLoan=Purpose Of Loan +Center\:=Center: +CenterSubTotal=Center Total: +LoanOfficerSubTotal=Loan Officer Total: +BranchTotal=Branch Total: +NoDataAvailable=No Data Available +OfficeLoanOfficerCenter\:=Office, Loan Officer, Center: diff --git a/mifosng-provider/src/main/webapp/META-INF/context.xml b/mifosng-provider/src/main/webapp/META-INF/context.xml new file mode 100644 index 000000000..7e72e2b3a --- /dev/null +++ b/mifosng-provider/src/main/webapp/META-INF/context.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/spring/webmvc-config.xml b/mifosng-provider/src/main/webapp/WEB-INF/spring/webmvc-config.xml new file mode 100644 index 000000000..76a17f378 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/spring/webmvc-config.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/allinone.css b/mifosng-provider/src/main/webapp/WEB-INF/static/allinone.css new file mode 100644 index 000000000..5e039f653 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/static/allinone.css @@ -0,0 +1,54 @@ +/*reset*/ +html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} +strong{font-weight: bolder;} +/*General*/ +body { font-family:Arial, Helvetica, Sans-Serif; font-size:12px; margin:0px 20px;} +div#container {margin: 0 auto; margin-top: 0px; width: 1000px;} +/*top navigation*/ +#topnav{ margin:0px; padding:0px; list-style:none; color:#fff; line-height:45px; display:inline-block; float:left; z-index:1000;} +#topnav a { color:#fff; text-decoration:none; } +#topnav > li {background:#172322 none repeat scroll 0 0; cursor:pointer; float:left; position:relative;padding:0px 10px;} +#topnav > li a:hover {color:#B0D730;} +#topnav .logo {background:transparent none repeat scroll 0% 0%; padding:0px; background-color:Transparent;} +/* sub-menus*/ +#topnav ul { padding:0px; margin:0px; display:block; display:inline; z-index:1000;} +#topnav li ul { position:absolute; left:-10px; top:0px; margin-top:45px; width:150px; line-height:16px; background-color:#172322; color:#0395CC; /* for IE */ display:none; } +#topnav li:hover ul { display:block;} +#topnav li ul li{ display:block; margin:5px 20px; padding: 5px 0px; border-top: dotted 1px #606060; list-style-type:none; } +#topnav li ul li:first-child { border-top: none; } +#topnav li ul li a { display:block; color:#0395CC; } +#topnav li ul li a:hover { color:#7FCDFE; } +/* main submenu */ +#topnav #main { left:0px; top:-20px; padding-top:20px; background-color:#7cb7e3; color:#fff; z-index:999;} +/* search */ +.searchContainer div { background-color:#fff; display:inline; padding:5px;} +.searchContainer input[type="text"] {border:none;} +.searchContainer img { vertical-align:middle;} +/* corners*/ +#topnav .corner_inset_left { position:absolute; top:0px; left:-12px;} +#topnav .corner_inset_right { position:absolute; top:0px; left:150px;} +#topnav .last { background:transparent none repeat scroll 0% 0%; margin:0px; padding:0px; border:none; position:relative; border:none; height:0px;} +#topnav .corner_left { position:absolute; left:0px; top:0px;} +#topnav .corner_right { position:absolute; left:132px; top:0px;} +#topnav .middle { position:absolute; left:18px; height: 20px; width: 115px; top:0px;} +/*end of top navigation css*/ +/*simple vertical form*/ +label {display: block; font-size: 14px; padding-top: 3px;} +#formcontainer input {font-size:12px; padding:4px 2px; border:solid 1px #aacfe4; width: 402px;} +#formcontainer select {font-size:12px; padding:3px 2px; border:solid 1px #aacfe4; width: 402px;} +fieldset {border-top: 1px solid;} +legend {margin-left: 10px; padding: 0.2em 0.5em; font-size: 16px; font-weight: bold;} +#ignorefornow button {clear:both; margin-left:225px; width:125px; height:31px; + background:#666666 no-repeat; text-align:center; + line-height:31px; color:#FFFFFF; font-size:11px; font-weight:bold;} +#formcontainer {margin:0 auto; width: auto; border:solid 2px #b7ddf2; background:#ebf4fb; padding: 14px;} +a.multiselectwidget {display: block; border: 1px solid #aaa; text-decoration: none; + background-color: #fafafa; color: #123456; clear:both;} +div.multiselectwidget {float:left; text-align: center; margin-right: 10px;} +select.multiselectwidget {width: 450px; height: 80px;} +/*datatables*/ +table.pretty thead tr th {text-align: center;} +table.pretty tfoot tr th {text-align: center;} +table.pretty tbody tr td {text-align: center; border-top: solid 1px;} +table.pretty tbody tr.odd td {background: #EBF4FB;} +table.pretty tbody tr.even td {background: #BCEEEE;} \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/favicon.ico b/mifosng-provider/src/main/webapp/WEB-INF/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ec2bd72e644c26065ef74b59de05546e9a456e6d GIT binary patch literal 318 zcmbtOu?@f=3^P&>$ke$bXXXtiUg0gK)DEq>RO(O%JH(C").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+""),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cq(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cp(){cn=b}function co(){setTimeout(cp,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bv(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bl(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bd,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bk(a){f.nodeName(a,"input")?bj(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bj)}function bj(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bi(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bh(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bg(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i=0===c})}function U(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function M(a,b){return(a&&a!=="*"?a+".":"")+b.replace(y,"`").replace(z,"&")}function L(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function J(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function D(){return!0}function C(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function K(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(K,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.3",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;B.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-1000px",top:"-1000px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="

",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i=f.expando,j=typeof c=="string",k=a.nodeType,l=k?f.cache:a,m=k?a[f.expando]:a[f.expando]&&f.expando;if((!m||e&&m&&l[m]&&!l[m][i])&&j&&d===b)return;m||(k?a[f.expando]=m=++f.uuid:m=f.expando),l[m]||(l[m]={},k||(l[m].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?l[m][i]=f.extend(l[m][i],c):l[m]=f.extend(l[m],c);g=l[m],e&&(g[i]||(g[i]={}),g=g[i]),d!==b&&(g[f.camelCase(c)]=d);if(c==="events"&&!g[c])return g[i]&&g[i].events;j?(h=g[c],h==null&&(h=g[f.camelCase(c)])):h=g;return h}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e=f.expando,g=a.nodeType,h=g?f.cache:a,i=g?a[f.expando]:f.expando;if(!h[i])return;if(b){d=c?h[i][e]:h[i];if(d){d[b]||(b=f.camelCase(b)),delete d[b];if(!l(d))return}}if(c){delete h[i][e];if(!l(h[i]))return}var j=h[i][e];f.support.deleteExpando||!h.setInterval?delete h[i]:h[i]=null,j?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=j):g&&(f.support.deleteExpando?delete a[f.expando]:a.removeAttribute?a.removeAttribute(f.expando):a[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=v:u&&(i=u)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.attr(a,b,""),a.removeAttribute(b),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(u&&f.nodeName(a,"button"))return u.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(u&&f.nodeName(a,"button"))return u.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==null?g:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabIndex=f.propHooks.tabIndex,v={get:function(a,c){var d;return f.prop(a,c)===!0||(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(u=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var w=/\.(.*)$/,x=/^(?:textarea|input|select)$/i,y=/\./g,z=/ /g,A=/[^\w\s.|`]/g,B=function(a){return a.replace(A,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=C;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=C);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),B).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},I=function(c){var d=c.target,e,g;if(!!x.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=H(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:I,beforedeactivate:I,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&I.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&I.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",H(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in G)f.event.add(this,c+".specialChange",G[c]);return x.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return x.test(this.nodeName)}},G=f.event.special.change.filters,G.focus=G.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=S.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(U(c[0])||U(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=R.call(arguments);N.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!T[a]?f.unique(e):e,(this.length>1||P.test(d))&&O.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!be[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)g[h]&&bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=be[l]||be._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bm,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bm.test(g)?g.replace(bm,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bv(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bw=function(a,c){var d,e,g;c=c.replace(bo,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bx=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bp.test(d)&&bq.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bv=bw||bx,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bz=/%20/g,bA=/\[\]$/,bB=/\r?\n/g,bC=/#.*$/,bD=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bE=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bF=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bG=/^(?:GET|HEAD)$/,bH=/^\/\//,bI=/\?/,bJ=/)<[^<]*)*<\/script>/gi,bK=/^(?:select|textarea)/i,bL=/\s+/,bM=/([?&])_=[^&]*/,bN=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bO=f.fn.load,bP={},bQ={},bR,bS,bT=["*/"]+["*"];try{bR=e.href}catch(bU){bR=c.createElement("a"),bR.href="",bR=bR.href}bS=bN.exec(bR.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bO)return bO.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bJ,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bK.test(this.nodeName)||bE.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bB,"\r\n")}}):{name:b.name,value:c.replace(bB,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?bX(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),bX(a,b);return a},ajaxSettings:{url:bR,isLocal:bF.test(bS[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bT},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bV(bP),ajaxTransport:bV(bQ),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?bZ(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=b$(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bD.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bC,"").replace(bH,bS[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bL),d.crossDomain==null&&(r=bN.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bS[1]&&r[2]==bS[2]&&(r[3]||(r[1]==="http:"?80:443))==(bS[3]||(bS[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bW(bP,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bG.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bI.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bM,"$1_="+x);d.url=y+(y===d.url?(bI.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bT+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bW(bQ,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){s<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bz,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cq("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=ct.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!ct.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cu(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cu(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNaN(j)?i:j}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/jquery-ui-1.8.16.custom.min.js b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/jquery-ui-1.8.16.custom.min.js new file mode 100644 index 000000000..14c9064f7 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/jquery-ui-1.8.16.custom.min.js @@ -0,0 +1,791 @@ +/*! + * jQuery UI 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI + */ +(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.16", +keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d= +this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this, +"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart": +"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight, +outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a, +"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&& +a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= +false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); +;/* + * jQuery UI Position 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Position + */ +(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.setTimeout){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j={top:b.of.pageY, +left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/2;if(b.at[1]==="bottom")j.top+= +k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+(parseInt(c.curCSS(this,"marginRight",true))||0),w=m+q+(parseInt(c.curCSS(this,"marginBottom",true))||0),i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]==="center")i.top-= +m/2;i.left=Math.round(i.left);i.top=Math.round(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();b.left= +d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];b.left+= +a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=c(b), +g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); +;/* + * jQuery UI Draggable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Draggables + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== +"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= +this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;if(b.iframeFix)d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('
').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options; +this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}); +this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true}, +_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b= +false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration, +10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle|| +!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&& +a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent= +this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"), +10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"), +10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top, +(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!= +"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"), +10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+ +this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&& +!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.leftg[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.topg[3])?h:!(h-this.offset.click.topg[2])?e:!(e-this.offset.click.left=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= +i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), +top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= +this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", +nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== +String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),l=0;l=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,l);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); +this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){if(!a.disabled){e(this).removeClass("ui-resizable-autohide");b._handles.show()}},function(){if(!a.disabled)if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy(); +var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a= +false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"}); +this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff= +{width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis]; +if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false}, +_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;f=f?0:c.sizeDiff.width;f={width:c.helper.width()-f,height:c.helper.height()-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f, +{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",b);this._helper&&this.helper.remove();return false},_updateVirtualBoundaries:function(b){var a=this.options,c,d,f;a={minWidth:k(a.minWidth)?a.minWidth:0,maxWidth:k(a.maxWidth)?a.maxWidth:Infinity,minHeight:k(a.minHeight)?a.minHeight:0,maxHeight:k(a.maxHeight)?a.maxHeight: +Infinity};if(this._aspectRatio||b){b=a.minHeight*this.aspectRatio;d=a.minWidth/this.aspectRatio;c=a.maxHeight*this.aspectRatio;f=a.maxWidth/this.aspectRatio;if(b>a.minWidth)a.minWidth=b;if(d>a.minHeight)a.minHeight=d;if(cb.width,h=k(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,l=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&l)b.left=i-a.minWidth;if(d&&l)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left= +null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+ +a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+ +c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]); +b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,{version:"1.8.16"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(), +10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top- +f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var l=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:l.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(l.css("position"))){c._revertToRelativePosition=true;l.css({position:"absolute",top:"auto",left:"auto"})}l.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType? +e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a= +e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing, +step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement= +e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top","Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset; +var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left: +a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top- +d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition, +f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25, +display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b= +e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height= +d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},k=function(b){return!isNaN(parseInt(b,10))}})(jQuery); +;/* + * jQuery UI Selectable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Selectables + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), +selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, +c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", +c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= +this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){var a=this.options;this.containerCache={};this.element.addClass("ui-sortable"); +this.refresh();this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a=== +"disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&& +!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top, +left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]}; +this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!= +document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a); +return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0], +e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset(); +c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp({target:null});this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"): +this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}if(this.placeholder){this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null, +dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):d(this.domPosition.parent).prepend(this.currentItem)}return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")}, +toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith(); +if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), +this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h=0;b--){var c=this.items[b];if(!(c.instance!=this.currentContainer&&this.currentContainer&&c.item[0]!=this.currentItem[0])){var e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b= +this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f= +d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")|| +0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out", +a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h- +f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g- +this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this, +this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop", +a,this._uiHash());for(e=0;e li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); +a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); +if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var h=d.closest(".ui-accordion-header");a.active=h.length?h:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion", +function(f){return a._keydown(f)}).next().attr("role","tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(f){a._clickHandler.call(a,f,this);f.preventDefault()})},_createIcons:function(){var a= +this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers);this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"); +this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); +b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,h=this.headers.index(a.target),f=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:f=this.headers[(h+1)%d];break;case b.LEFT:case b.UP:f=this.headers[(h-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); +a.preventDefault()}if(f){c(a.target).attr("tabIndex",-1);c(f).attr("tabIndex",0);f.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ +c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; +if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){var h=this.active;j=a.next();g=this.active.next();e={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):j,oldContent:g};var f=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(j,g,e,b,f);h.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); +if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);a.next().addClass("ui-accordion-content-active")}}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");var g=this.active.next(), +e={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:g},j=this.active=c([]);this._toggle(j,g,e)}},_toggle:function(a,b,d,h,f){var g=this,e=g.options;g.toShow=a;g.toHide=b;g.data=d;var j=function(){if(g)return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data);g.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&h?{toShow:c([]),toHide:b,complete:j,down:f,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:f,autoHeight:e.autoHeight|| +e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;h=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!h[k]&&!c.easing[k])k="slide";h[k]||(h[k]=function(l){this.slide(l,{easing:k,duration:i||700})});h[k](d)}else{if(e.collapsible&&h)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false", +"aria-selected":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");if(this.toHide.length)this.toHide.parent()[0].className=this.toHide.parent()[0].className;this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.16", +animations:{slide:function(a,b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),h=0,f={},g={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){g[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/); +f[i]={value:j[1],unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(g,{step:function(j,i){if(i.prop=="height")h=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=h*f[i.prop].value+f[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide", +paddingTop:"hide",paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); +;/* + * jQuery UI Autocomplete 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + */ +(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.propAttr("readOnly"))){g= +false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!= +a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)}; +this.menu=d("
    ").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&& +a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"); +d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&& +b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source= +this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(d("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, +"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery); +(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", +-1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id"); +this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b, +this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| +this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| +this.first()?":last":":first"))},hasScroll:function(){return this.element.height()").addClass("ui-button-text").html(this.options.label).appendTo(a.empty()).text(),e=this.options.icons,f=e.primary&&e.secondary,d=[];if(e.primary||e.secondary){if(this.options.text)d.push("ui-button-text-icon"+(f?"s":e.primary?"-primary":"-secondary"));e.primary&&a.prepend("");e.secondary&&a.append("");if(!this.options.text){d.push(f?"ui-button-icons-only": +"ui-button-icon-only");this.hasTitle||a.attr("title",c)}}else d.push("ui-button-text-only");a.addClass(d.join(" "))}}});b.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(a,c){a==="disabled"&&this.buttons.button("option",a,c);b.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var a=this.element.css("direction")=== +"ltr";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(a?"ui-corner-left":"ui-corner-right").end().filter(":last").addClass(a?"ui-corner-right":"ui-corner-left").end().end()},destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"); +b.Widget.prototype.destroy.call(this)}})})(jQuery); +;/* + * jQuery UI Dialog 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.button.js + * jquery.ui.draggable.js + * jquery.ui.mouse.js + * jquery.ui.position.js + * jquery.ui.resizable.js + */ +(function(c,l){var m={buttons:true,height:true,maxHeight:true,maxWidth:true,minHeight:true,minWidth:true,width:true},n={maxHeight:true,maxWidth:true,minHeight:true,minWidth:true},o=c.attrFn||{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true,click:true};c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false, +position:{my:"center",at:"center",collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",e=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
    ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+ +b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&!i.isDefaultPrevented()&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog","aria-labelledby":e}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var f=(a.uiDialogTitlebar=c("
    ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g), +h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);return false}).appendTo(f);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id", +e).html(d).prependTo(f);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;f.find("*").add(f).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"); +a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d,e;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!== +b.uiDialog[0]){e=c(this).css("z-index");isNaN(e)||(d=Math.max(d,e))}});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,e=d.options;if(e.modal&&!a||!e.stack&&!e.modal)return d._trigger("focus",b);if(e.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=e.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()};c.ui.dialog.maxZ+=1; +d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;a._size();a._position(b.position);d.show(b.show);a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(e){if(e.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),f=g.filter(":first");g=g.filter(":last");if(e.target===g[0]&&!e.shiftKey){f.focus(1);return false}else if(e.target=== +f[0]&&e.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,e=c("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
    ").addClass("ui-dialog-buttonset").appendTo(e);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a, +function(){return!(d=true)});if(d){c.each(a,function(f,h){h=c.isFunction(h)?{click:h,text:f}:h;var i=c('').click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.each(h,function(j,k){if(j!=="click")j in o?i[j](k):i.attr(j,k)});c.fn.button&&i.button()});e.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(f){return{position:f.position,offset:f.offset}}var b=this,d=b.options,e=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close", +handle:".ui-dialog-titlebar",containment:"document",start:function(f,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",f,a(h))},drag:function(f,h){b._trigger("drag",f,a(h))},stop:function(f,h){d.position=[h.position.left-e.scrollLeft(),h.position.top-e.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);b._trigger("dragStop",f,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(f){return{originalPosition:f.originalPosition, +originalSize:f.originalSize,position:f.position,size:f.size}}a=a===l?this.options.resizable:a;var d=this,e=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:a,start:function(f,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",f,b(h))},resize:function(f,h){d._trigger("resize", +f,b(h))},stop:function(f,h){c(this).removeClass("ui-dialog-resizing");e.height=c(this).height();e.width=c(this).width();d._trigger("resizeStop",f,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(a){var b=[],d=[0,0],e;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "): +[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,f){if(+b[g]===b[g]){d[g]=b[g];b[g]=f}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(e=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(c.extend({of:window},a));e||this.uiDialog.hide()},_setOptions:function(a){var b=this,d={},e=false;c.each(a,function(g,f){b._setOption(g,f); +if(g in m)e=true;if(g in n)d[g]=f});e&&this._size();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",d)},_setOption:function(a,b){var d=this,e=d.uiDialog;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":e.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?e.addClass("ui-dialog-disabled"): +e.removeClass("ui-dialog-disabled");break;case "draggable":var g=e.is(":data(draggable)");g&&!b&&e.draggable("destroy");!g&&b&&d._makeDraggable();break;case "position":d._position(b);break;case "resizable":(g=e.is(":data(resizable)"))&&!b&&e.resizable("destroy");g&&typeof b==="string"&&e.resizable("option","handles",b);!g&&b!==false&&d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break}c.Widget.prototype._setOption.apply(d,arguments)},_size:function(){var a= +this.options,b,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();d=Math.max(0,a.minHeight-b);if(a.height==="auto")if(c.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();a=this.element.css("height","auto").height();e||this.uiDialog.hide();this.element.height(Math.max(a,d))}else this.element.height(Math.max(a.height- +b,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.16",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "), +create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){var b=c.inArray(a,this.instances);b!=-1&&this.oldInstances.push(this.instances.splice(b,1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var d=0;c.each(this.instances,function(){d=Math.max(d,this.css("z-index"))});this.maxZ=d},height:function(){var a,b;if(c.browser.msie&& +c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(b.range==="min"||b.range==="max"?" ui-slider-range-"+b.range:""))}for(var j=c.length;j"); +this.handles=c.add(d(e.join("")).appendTo(a.element));this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(g){g.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(g){d(this).data("index.ui-slider-handle", +g)});this.handles.keydown(function(g){var k=true,l=d(this).data("index.ui-slider-handle"),i,h,m;if(!a.options.disabled){switch(g.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:k=false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");i=a._start(g,l);if(i===false)return}break}m=a.options.step;i=a.options.values&&a.options.values.length? +(h=a.values(l)):(h=a.value());switch(g.keyCode){case d.ui.keyCode.HOME:h=a._valueMin();break;case d.ui.keyCode.END:h=a._valueMax();break;case d.ui.keyCode.PAGE_UP:h=a._trimAlignValue(i+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:h=a._trimAlignValue(i-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(i===a._valueMax())return;h=a._trimAlignValue(i+m);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(i===a._valueMin())return;h=a._trimAlignValue(i- +m);break}a._slide(g,l,h);return k}}).keyup(function(g){var k=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(g,k);a._change(g,k);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");this._mouseDestroy(); +return this},_mouseCapture:function(a){var b=this.options,c,f,e,j,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});f=this._valueMax()-this._valueMin()+1;j=this;this.handles.each(function(k){var l=Math.abs(c-j.values(k));if(f>l){f=l;e=d(this);g=k}});if(b.range===true&&this.values(1)===b.min){g+=1;e=d(this.handles[g])}if(this._start(a,g)===false)return false; +this._mouseSliding=true;j._handleIndex=g;e.addClass("ui-state-active").focus();b=e.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-e.width()/2,top:a.pageY-b.top-e.height()/2-(parseInt(e.css("borderTopWidth"),10)||0)-(parseInt(e.css("borderBottomWidth"),10)||0)+(parseInt(e.css("marginTop"),10)||0)};this.handles.hasClass("ui-state-hover")||this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= +this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= +this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); +c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var f;if(this.options.values&&this.options.values.length){f=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>f||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}else if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;f=arguments[0];for(e=0;e=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= +this.options.range,b=this.options,c=this,f=!this._animateOff?b.animate:false,e,j={},g,k,l,i;if(this.options.values&&this.options.values.length)this.handles.each(function(h){e=(c.values(h)-c._valueMin())/(c._valueMax()-c._valueMin())*100;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";d(this).stop(1,1)[f?"animate":"css"](j,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(h===0)c.range.stop(1,1)[f?"animate":"css"]({left:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({width:e- +g+"%"},{queue:false,duration:b.animate})}else{if(h===0)c.range.stop(1,1)[f?"animate":"css"]({bottom:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({height:e-g+"%"},{queue:false,duration:b.animate})}g=e});else{k=this.value();l=this._valueMin();i=this._valueMax();e=i!==l?(k-l)/(i-l)*100:0;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";this.handle.stop(1,1)[f?"animate":"css"](j,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[f?"animate":"css"]({width:e+"%"}, +b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[f?"animate":"css"]({width:100-e+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[f?"animate":"css"]({height:e+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[f?"animate":"css"]({height:100-e+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.16"})})(jQuery); +;/* + * jQuery UI Tabs 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
    ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(true)},_setOption:function(b,e){if(b=="selected")this.options.collapsible&& +e==this.options.selected||this.select(e);else{this.options[b]=e;this._tabify()}},_tabId:function(b){return b.title&&b.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(b){return b.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[b].concat(d.makeArray(arguments)))},_ui:function(b,e){return{tab:b,panel:e,index:this.anchors.index(b)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b= +d(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(b){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var a=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| +(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))a.panels=a.panels.add(a.element.find(a._sanitizeSelector(i)));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=a._tabId(f);f.href="#"+i;f=a.element.find("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(a.panels[g-1]||a.list);f.data("destroy.tabs",true)}a.panels=a.panels.add(f)}else c.disabled.push(g)});if(b){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); +this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(a._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= +this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return a.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); +if(c.selected>=0&&this.anchors.length){a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash)).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");a.element.queue("tabs",function(){a._trigger("show",null,a._ui(a.anchors[c.selected],a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash))[0]))});this.load(c.selected)}d(window).bind("unload",function(){a.lis.add(a.anchors).unbind(".tabs");a.lis=a.anchors=a.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")); +this.element[c.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);b=0;for(var j;j=this.lis[b];b++)d(j)[d.inArray(b,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+ +g)};this.lis.bind("mouseover.tabs",function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal", +function(){e(f,o);a._trigger("show",null,a._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");a._trigger("show",null,a._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);a.element.dequeue("tabs")})}:function(g,f){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");a.element.dequeue("tabs")}; +this.anchors.bind(c.event+".tabs",function(){var g=this,f=d(g).closest("li"),i=a.panels.filter(":not(.ui-tabs-hide)"),l=a.element.find(a._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||a.panels.filter(":animated").length||a._trigger("select",null,a._ui(this,l[0]))===false){this.blur();return false}c.selected=a.anchors.index(this);a.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected= +-1;c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this));this.blur();return false}c.cookie&&a._cookie(c.selected,c.cookie);if(l.length){i.length&&a.element.queue("tabs",function(){s(g,i)});a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier."; +d.browser.msie&&this.blur()});this.anchors.bind("click.tabs",function(){return false})},_getIndex:function(b){if(typeof b=="string")b=this.anchors.index(this.anchors.filter("[href$="+b+"]"));return b},destroy:function(){var b=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e= +d.data(this,"href.tabs");if(e)this.href=e;var a=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){a.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});b.cookie&&this._cookie(null,b.cookie);return this},add:function(b, +e,a){if(a===p)a=this.anchors.length;var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,b).replace(/#\{label\}/g,e));b=!b.indexOf("#")?b.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=c.element.find("#"+b);j.length||(j=d(h.panelTemplate).attr("id",b).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(a>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[a]); +j.insertBefore(this.panels[a])}h.disabled=d.map(h.disabled,function(k){return k>=a?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[a],this.panels[a]));return this},remove:function(b){b=this._getIndex(b);var e=this.options,a=this.lis.eq(b).remove(),c=this.panels.eq(b).remove(); +if(a.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(b+(b+1=b?--h:h});this._tabify();this._trigger("remove",null,this._ui(a.find("a")[0],c[0]));return this},enable:function(b){b=this._getIndex(b);var e=this.options;if(d.inArray(b,e.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(a){return a!=b});this._trigger("enable",null, +this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(b){b=this._getIndex(b);var e=this.options;if(b!=e.selected){this.lis.eq(b).addClass("ui-state-disabled");e.disabled.push(b);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[b],this.panels[b]))}return this},select:function(b){b=this._getIndex(b);if(b==-1)if(this.options.collapsible&&this.options.selected!=-1)b=this.options.selected;else return this;this.anchors.eq(b).trigger(this.options.event+".tabs");return this}, +load:function(b){b=this._getIndex(b);var e=this,a=this.options,c=this.anchors.eq(b)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(a.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(a.spinner)}this.xhr=d.ajax(d.extend({},a.ajaxOptions,{url:h,success:function(k,n){e.element.find(e._sanitizeSelector(c.hash)).html(k);e._cleanup();a.cache&&d.data(c, +"cache.tabs",true);e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.error(k,n,b,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this}, +url:function(b,e){this.anchors.eq(b).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.16"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(b,e){var a=this,c=this.options,h=a._rotate||(a._rotate=function(j){clearTimeout(a.rotation);a.rotation=setTimeout(function(){var k=c.selected;a.select(++k'))}function N(a){return a.bind("mouseout", +function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");b.length&&b.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");if(!(d.datepicker._isDisabledDatepicker(J.inline?a.parent()[0]:J.input[0])||!b.length)){b.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); +b.addClass("ui-state-hover");b.hasClass("ui-datepicker-prev")&&b.addClass("ui-datepicker-prev-hover");b.hasClass("ui-datepicker-next")&&b.addClass("ui-datepicker-next-hover")}})}function H(a,b){d.extend(a,b);for(var c in b)if(b[c]==null||b[c]==C)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.16"}});var B=(new Date).getTime(),J;d.extend(M.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv}, +setDefaults:function(a){H(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]=f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_-])/g, +"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:N(d('
    '))}},_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker", +function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b);b.settings.disabled&&this._disableDatepicker(a)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&&b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c== +"focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f==""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker(): +d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a, +b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b),true);this._updateDatepicker(b);this._updateAlternate(b);b.settings.disabled&&this._disableDatepicker(a);b.dpDiv.css("display","block")}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+= +1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}H(a.settings,e||{});b=b&&b.constructor==Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/ +2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]);d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b= +d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}},_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e= +a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().removeClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b=d(a),c=d.data(a, +"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().addClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f== +a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false;for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target||a;if(a.nodeName.toLowerCase()!="input")a=d("input", +a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);if(d.datepicker._curInst&&d.datepicker._curInst!=b){d.datepicker._datepickerShowing&&d.datepicker._triggerOnClose(d.datepicker._curInst);d.datepicker._curInst.dpDiv.stop(true,true)}var c=d.datepicker._get(b,"beforeShow");c=c?c.apply(a,[a,b]):{};if(c!==false){H(b.settings,c);b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value= +"";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a);d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.empty();b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b); +c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&&d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){var i=b.dpDiv.find("iframe.ui-datepicker-cover");if(i.length){var g=d.datepicker._getBorders(b.dpDiv);i.css({left:-g[0],top:-g[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex(d(a).zIndex()+1);d.datepicker._datepickerShowing= +true;d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f,h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}}},_updateDatepicker:function(a){this.maxRows=4;var b=d.datepicker._getBorders(a.dpDiv);J=a;a.dpDiv.empty().append(this._generateHTML(a));var c=a.dpDiv.find("iframe.ui-datepicker-cover");c.length&&c.css({left:-b[0],top:-b[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}); +a.dpDiv.find("."+this._dayOverClass+" a").mouseover();b=this._getNumberOfMonths(a);c=b[1];a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");c>1&&a.dpDiv.addClass("ui-datepicker-multi-"+c).css("width",17*c+"em");a.dpDiv[(b[0]!=1||b[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl");a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&& +!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var e=a.yearshtml;setTimeout(function(){e===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml);e=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(), +h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(),j=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>j&&j>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b= +this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1||d.expr.filters.hidden(a));)a=a[b?"previousSibling":"nextSibling"];a=d(a).offset();return[a.left,a.top]},_triggerOnClose:function(a){var b=this._get(a,"onClose");if(b)b.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a])},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b); +this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();d.datepicker._triggerOnClose(b);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")}, +_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&&!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"): +0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth;b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e["selected"+(c=="M"? +"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a=d(a); +this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a,"altField"); +if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"? +b.toString():b+"";if(b=="")return null;var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;e=typeof e!="string"?e:(new Date).getFullYear()%100+parseInt(e,10);for(var f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,j=c=-1,l=-1,u=-1,k=false,o=function(p){(p=A+1-1){j=1;l=u;do{e=this._getDaysInMonth(c,j-1);if(l<=e)break;j++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,j-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=j||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd", +COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames: +null)||this._defaults.monthNames;var i=function(o){(o=k+1 +12?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e?"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&& +a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),j=this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay? +new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),k=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=k&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a,"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-j,1)),this._getFormatConfig(a)); +n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+n+"";var s=this._get(a,"nextText");s=!h?s:this.formatDate(s,this._daylightSavingAdjust(new Date(m, +g+j,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+s+"":f?"":''+s+"";j=this._get(a,"currentText");s=this._get(a,"gotoCurrent")&& +a.currentDay?u:b;j=!h?j:this.formatDate(j,s,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
    '+(c?h:"")+(this._isInRange(a,s)?'":"")+(c?"":h)+"
    ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;j=this._get(a,"showWeek");s=this._get(a,"dayNames");this._get(a,"dayNamesShort");var q=this._get(a,"dayNamesMin"),A=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),D=this._get(a,"showOtherMonths"),K=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var E=this._getDefaultDate(a),w="",x=0;x1)switch(G){case 0:y+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:y+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:y+=" ui-datepicker-group-middle";t="";break}y+='">'}y+='
    '+(/all|left/.test(t)&& +x==0?c?f:n:"")+(/all|right/.test(t)&&x==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,k,o,x>0||G>0,A,v)+'
    ';var z=j?'":"";for(t=0;t<7;t++){var r=(t+h)%7;z+="=5?' class="ui-datepicker-week-end"':"")+'>'+q[r]+""}y+=z+"";z=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, +z);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;z=Math.ceil((t+z)/7);this.maxRows=z=l?this.maxRows>z?this.maxRows:z:z;r=this._daylightSavingAdjust(new Date(m,g,1-t));for(var Q=0;Q";var R=!j?"":'";for(t=0;t<7;t++){var I=p?p.apply(a.input?a.input[0]:null,[r]):[true,""],F=r.getMonth()!=g,L=F&&!K||!I[0]||k&&ro;R+='";r.setDate(r.getDate()+1);r=this._daylightSavingAdjust(r)}y+=R+""}g++;if(g>11){g=0;m++}y+="
    '+this._get(a,"weekHeader")+"
    '+this._get(a,"calculateWeek")(r)+""+(F&&!D?" ":L?''+ +r.getDate()+"":''+r.getDate()+"")+"
    "+(l?""+(i[0]>0&&G==i[1]-1?'
    ':""):"");O+=y}w+=O}w+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': +"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='
    ',o="";if(h||!j)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(k+=o+(h||!(j&&l)?" ":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+=''+c+"";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b, +e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?" ":"")+o;k+="
    ";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+ +(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input? +a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c, +e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a, +"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this; +if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a== +"string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.16";window["DP_jQuery_"+B]=d})(jQuery); +;/* + * jQuery UI Progressbar 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function(b,d){b.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()});this.valueDiv=b("
    ").appendTo(this.element);this.oldValue=this._value();this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); +this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===d)return this._value();this._setOption("value",a);return this},_setOption:function(a,c){if(a==="value"){this.options.value=c;this._refreshValue();this._value()===this.options.max&&this._trigger("complete")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100* +this._value()/this.options.max},_refreshValue:function(){var a=this.value(),c=this._percentage();if(this.oldValue!==a){this.oldValue=a;this._trigger("change")}this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(c.toFixed(0)+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.16"})})(jQuery); +;/* + * jQuery UI Effects 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/ + */ +jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], +16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, +a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= +a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor", +"borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0, +0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211, +211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, +d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; +f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, +[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), +d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement; +if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)}); +return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this, +arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/ +2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b, +d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c, +a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b, +d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ +e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); +;/* + * jQuery UI Effects Fade 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fade + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Fold 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * jquery.effects.core.js + */ +(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1], +10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); +;/* + * jQuery UI Effects Highlight 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Highlight + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& +this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Pulsate 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Pulsate + * + * Depends: + * jquery.effects.core.js + */ +(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); +b.dequeue()})})}})(jQuery); +; \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..5b5dab2ab7b1c50dea9cfe73dc5a269a92d2d4b4 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*FscKIb$B>N1x91EQ4=4yQ7#`R^ z$vje}bP0l+XkK DSH>_4 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_55_fbec88_40x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_flat_55_fbec88_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..47acaadd737478ddb090f47f618810712163317b GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*Fsaj7L$B>N1x91EQ8x$BA993)+ za~~)OO5|O5sDCi_{N8&XlRv*c;OQ6|AR59NN?mFzWBXJVGojypu|S6~c)I$ztaD0e F0syyrGF|`x literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb564f8d0a117f17aa6b844490309dadbd94821 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouq?|on978O6-=0_GYj6;7zWBfT zzjhI`OjAO{6(N>+Em!s|xjZW|^1EO|(5d{JeUmv{p6fa-GJh;t>KCH4`R~7(L8qj} Y_egNRQF(If70@^aPgg&ebxsLQ0Qgob)Bpeg literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_85_dfeffc_1x400.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_85_dfeffc_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..014951529c315d6042e72febc310a4d2db5b4a82 GIT binary patch literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouq?|lm978O6-<~(*YA_IRxoBVf zfAX@vsV!R#l$@#*eLnw)_Sv|_?i7P!ORnX)SxaXh+BPpZ!Fw~yjr&#G|Jw^YMHDhV X&EsZx`7bsSXc~j3tDnm{r-UW|&(SK+ literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_95_fef1ec_1x400.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..4443fdc1a156babad4336f004eaf5ca5dfa0f9ab GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnour0hIh978O6-<~(*YA|4MzBvER z|7}eQtdCVXoUc2b{PaWeaIKu7gJx>{vDV26o)#~38k_!`W=^oo1w6ixmPC4R1b Tyd6G3lNdZ*{an^LB{Ts5`idse literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png new file mode 100644 index 0000000000000000000000000000000000000000..81ecc362d50ef5abbc0420aacd5345822f1f6098 GIT binary patch literal 3457 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_2e83ff_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..09d1cdc856c292c4ab6dd818c7543ac0828bd616 GIT binary patch literal 4369 zcmd^?`8O2)_s3@pGmLE*`#M>&Z`mr_kcu#tBo!IbqU=l7VaSrbQrTh%5m}S08Obh0 zGL{*mi8RK}U~J#s@6Y%1S9~7lb?$xLU+y{go_o*h`AW1wUF3v{Kmh;%r@5J_9RL9Q zdj+hqg8o{9`K7(TZrR4t{=9O`!T-(~c=yEWZ{eswJJe->5bP8)t4;f(Y*i_HU*sLM z2=7-8guZ}@*(HhVC)Mqgr$3T8?#a(hu& z?Kzuw!O%PM>AicSW`_U(cbvJYv3{HfpIP~Q>@$^c588E$vv)V2c|Mr% zuFO$+I~Hg@u}wPm17n%}j1Y+Pbu!bt?iPkjGAo7>9eRN0FZz3X2_QZj+V!}+*8oBQ z_=iI^_TCA;Ea2tPmRNOeX3+VM>KL;o1(h`c@`6Ah`vdH<&+$yTg)jGWW72T}6J`kUAv?2CgyV zrs0y@Fpvpj@kWVE0TzL@Cy#qHn~kgensb{hIm6J&I8hkoNHOz6o1QQ3QM4NZyu?;= zLd>`wPT*uGr+6vAxYv3k8{gMDR>tO}UavDKzzyi6hvbuP=XQ4Y|A)r4#B$U(q7{1Z z0iLeSjo3;T*diS*me%4|!s23l@>R}rn@#Zc{<%CFt;?gd5S<)b=8Yz32U zBBLprntW3RE3f|uNX5Aw|I(IlJjW-Byd?QFFRk%hLU}O*YyYQel}WcXilLMJp9cB4 z)E?D+*Y4zai&XY!>niMfTW-2pp-^KFT93%Leig@uoQGPYRCva-`w#orm`is`p8b4s zxD462;f*^XO$=3by=VzN9i@xxr<1w=pcxl!$!fjWt|fYmq1@@badT?v`d zIi$|e$Ji}FXsiVYf)?pN1R0LBw;+)B5aUJj2fP+=m;=_Eho84g%Jq#@MLPSQEX*@T z6sZb)m?)zby>{j1)(;rRML|gKSs+9jorf-XhQJ2Jyt5Cqc*`S3iX@A5C3jvgAns|4 z*|)YQ%Kmsj+YZ53;nMqh|AFvehUV-9R;1ZZ;w5r9l}8hjSw@#k;>)$P*r%)=Extyu zB!$Kd-F?*50aJ2;TNTR-fc8B{KAq3!vW{g$LlGPfGW+%#CXU zJDcMsvyT2`x~v>>w8@yssoA`KuIZ98CLU{Ia%*nW3G4t}@ApsbC@o^WCqL>OXx>Y^ zSuVWEQ;3=A=@RxCnt0>G@#(VWBQ`0$qTwA#e>SX{_N~JWGsBxFHCw|5|?CzDi>92F-^=b*8sMXnhUJdb!>yGD2nhN@{582 zRPcxuDzs&;8De)>_J19z{0xppXQop#T_5ejGCKv@l>$O#DA-@X{y_1B-AsiU)H}DR z3xDZ8G`amV_WmA&8!W=@jgm|%bnwH%qkg(@J$hLaSV zC-rXIFMM%y<|Gb)o?j zpe-`dJ*N5tC-iH)d0CgLdBsw*C!ST9hY1EkI|Y(&=p&dH&q;a&7HXa5#_wtMsenQL zcpyhwx)Ppw@XmVz?P)DI#^ee1oC!i`>>Jq1ESk-OuQ(Pbv=s{A0AjM@rw#FaU;RUh z*At0{U*NtGVY_-JcuG$?zuuf%ZBTWxKU2yf?iN#-MRWs>A*2;p0G1Tp3d29u5RbnY zDOON-G|PidOOGeybnbzu7UVv71l!b=w7eU5l*{EdKuoKu`#LZ}|fnUr-+lSST9(MTT`0tqOG z#+Q_=lXe-=;rE4u8s~;%i~~ z8v&&+VPeXG=2zw9B5sR$e?R(n%nf?p-(BCZ8}x!_-9T+LT;2=Zu?Wv)j3#>35$6dR z4*7xmI)#06qjh#sXvX(%`#D1mD8fn1G~I;l%Dk{pw)}>_{+3^Fv_q)>2#de5qGCId zPz?ix-3954nM&u@vaw{o%-#HU%_bLJMO#@enR^&B{3ihWdoU6%pBJ`o>im+b-c6r-;c{vd0Z_)`75$jApy2?!9G4_FGa)iZ~9`6VELiYM+n!-mUfvfm{jt zC?!1=%pxJhF>vyQ47Q}R;O48pxgMs)rz$SbM&jkp<6X$r4DHWg>ZnGB-$r2o1*nL# zW0^*itcRY_^Uv^XgQP>W#>KQgM~l{;S(GkVW@&vld^AhWzG^m|9#0#USbM>^en{k2 za8~DTL`(Q~=ofsL&Fc`!L6r~qTnnGo8r98<(aG*<0%aNEr!!BIyY>VV82kxhR%d>V(lN&#BId#urK_i~Pe6?>C~J!pU_lRon#&S_cXoQv;poG8FK4atc

    N)npz1~X%p6x{M(Gw!!H=!}lmO0Xr*8ewyH(Q+>oy`fxQkxJ zzzB$)%*xM4s_2(O>)T-QXhwP|&DZam#{O+47q|WKfz_ZL-MypRN~o{fE*I#6@eM?I zs%f-6{Lz6j7rB#U$%O$~TIT!j?|Ip1CpSmb=JA9qCY3-mQf|fVCxswPjok|VofUEP zW5^pTd5B;wRkyW%1a;nYHB$ef6Pv8^);`m0jv6p72iNJl+sVBqZugsq6cq_pyNREi z>GN!h6ZQ6`aOMr_2KI@j=XR@$aJj(2jcpY?>f=2kMV@di5W7Swj?ug10zRe}F1nR* ztMm6+T^)LJe^SzGgSxahQajq0h7#|8oMV0>D~*N}jl?9_X`ka42R4@rryDc3o(c$R?1*!1O9zleSOczw zYPS3~xbJ$~C(3+D7Zkrfjs_lneY^zv^kHmxt)aqZ!aeGABHZ`gvA&K`72z}ihI$Ht z9V&)wQy0g@R9irwbf!{uE&_J2l9jXz^Vj#=qA77*3Pd9OjrE_tKDHADd!AjFQv(ji zct-BMUt9()1Ox!dsI_h1(^F_U)_QJrx|%+y`zWWlD4=Nd?JQ=URh0*{fb1!o4tS(H z^r_T(8t1SAHf1oduG+X^*EC_kL(!QnXL6Hp);449yO&1xE>MXGqT)t10lzvALllX;;Q)RiJX$dm zlR8ep5-GdHmRm9?N#QCjNUA);vC03Gw6yds6^?c4;(MH>;O5xmQ2nGK3Dmk8i*v5t z-{jJsQq30%z}0`g7SN-yN`l-`@6rkJ|V|>18`MV zwUeH}DxWw&h+A+Dn|4|YNr&EfKS`Hz_NkeW3*sI5Rq-J&FzG=!{-K`n65#7O%^&f> z`PkqxyC_K)>781~7H${^Nj{`>XEa&OPqqQhySR5%w2{5+sEakXXHazJp6~LP2QKDx zpkvZrkDOa+A4BbqqX6ls&O)5-Q7`qkZ_?6~c-wQ9tseNtET;nhEOL^`*naKwcMX;R zbto&a;oTR0s;vjfj3wigUg)Sj)!OHQfZoJwAsWYI1A4ntz>X=W4s|y?tUk1r=>#Ct zf+?hq^>rQ3$KNboG$UhCdEmp{qAR13DK$f0ES7kAG~7q+g!jfVq`1b5+c62N^0%~o zKw91o@Wv;0EW*7fINAX3O~L-V{`;xB0q()#^HKZOlLrXVL*Dtw-$SUp8*_J{r( zW`6r`cz0yZQ#f0#*y+m64{bs7GP|2V$phf42rswJB?s@9qf;Bfc^pm-ZS#^5dkG{u zzv;l&B$NYcegSqAnjnPN1?17VUQbPummcWry((85IFB(pFQNGN{hhN$Fv?~l_fr?| z9=%dK(+;kZ(8=mwptjwC-ikBD$Z{l2++~*8wq5ynF<+PNlZI7ba5V#fg~L}kE;UH5 zJ;{P(`G{tNl&z5rUiH~e{I>GT8~9&*(J;Myx9z5P!db!F8RTII^I7c)HU=ss*bYB` zgwiIMZ_q>KEC$4lFm+Afvu6^$X1jm1rB*4H)-EIO5Rvz_p24?OkJ zovD4{-1KA6*oL?a;3qR7GZRB!cE5oAdA#M@{w+fGgsJ-lSmQ^-?8E&Q%tbmjd=@gZ z(}Mg*jsDf6Z)|7s%@9pc-tuw5W&zqUXjv2bVkC%-X?O3F72W4EsIl#1e>Mdz=X4k*_>VxCu_2?jjg16N*5fwC-36OW&;Sz}@jMn}hgJdEd pO;bST+>R{W-aENZYk%(=^(_R5N$LmL{Qc?!%+I4tt4z=_{|902Wu5>4 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_469bdd_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_469bdd_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2cf079add1ca236adeb509698adabbffb08acb GIT binary patch literal 4369 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_cd0a0a_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab019b73ec11a485fa09378f3a0e155194f6a5d GIT binary patch literal 4369 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_f9bd01_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/redmond/images/ui-icons_f9bd01_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c53cb11904843e176e4ce1f4e7247cd90b3590 GIT binary patch literal 5355 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png new file mode 100644 index 0000000000000000000000000000000000000000..64ece5707d91a6edf9fad4bfcce0c4dbcafcf58d GIT binary patch literal 251 zcmVbvPcjKS|RKP(6sDcCAB(_QB%0978a<$Ah$!b|E zwn;|HO0i8cQj@~)s!ajF0S002ovPDHLkV1oEp BYH0uf literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_flat_10_000000_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..abdc01082bf3534eafecc5819d28c9574d44ea89 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*FsY*{5$B>N1x91EQ4=4yQY-ImG zFPf9b{J;c_6SHRK%WcbN_hZpM=(Ry;4Rxv2@@2Y=$K57eF$X$=!PC{xWt~$(69B)$ BI)4BF literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..9b383f4d2eab09c0f2a739d6b232c32934bc620b GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnour1U*q978O6-yYw{%b*}|_(02F z@qbE9)0CJMo;*v*PWv`Vh2h6EmG8IS-Cm{3U~` zFlmZ}YMcJY=eo?o%*@I?2`NblNeMudl#t?{+tN>ySr~=F{k$>;_x^_y?afmf9pRKH0)6?eSP?3s5hEr>mdKI;Vst E0O;M1& literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png new file mode 100644 index 0000000000000000000000000000000000000000..39d5824d6af5456f1e89fc7847ea3599ea5fd815 GIT binary patch literal 3762 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png new file mode 100644 index 0000000000000000000000000000000000000000..f1273672d253263b7564e9e21d69d7d9d0b337d9 GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^j6j^i!3HGVb)pi0l%l7LV~E7mxPQ=F85a&M@g_{ d|GeK{$Y5lo%PMu^>wln`44$rjF6*2UngE4^EGqy2 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_222222_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_222222_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..b273ff111d219c9b9a8b96d57683d0075fb7871a GIT binary patch literal 4369 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_228ef1_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_228ef1_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..a641a371afa0fbb08ba599dc7ddf14b9bfc3c84f GIT binary patch literal 4369 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 oBo%DI*Kv;w;*%(i9W@e{{5C=l}o! literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ef8c08_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ef8c08_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..85e63e9f604ce042d59eb06a8428eeb7cb7896c9 GIT binary patch literal 4369 zcmd^?`8O2)_s3^phOrG}UnfiUEn8(9QW1?MNkxXVDEpFin2{xWrLx5kBC;k~GmC-Ajq!3AfU8Dx90^_ zp3}MKjJzYC+`T(&egFXQ#9Ek{*oVAaa!zrZtmlRFnwQPRJXH<%pkK2*eP`pT=lwD7 zifq+4BY_rUTa+U|2#&?i7>PVvD?7R4ZfOLPT{e9G~G!Ls3s8JtQE`jMM9wl2V9&Q+K2DHW0M+uQmEr%nYJ^7cK?uIpU-)=wn71ZZ-=@ar0;3^AY z5+TI{2b(e%t{2PZ^HKF*vu@+Xr&BAc@2BC4 z_vCgww#i=)ea5Vo$glEEVBBg_VPBj!)OO>)f@}#dg6ULOeC>LBHz<;*5Y;YfE0lNx zg{N+4@lO~ozxpF69qV@VOGnc248Iuag4C1T)P^(hWkpP!{h!JekX}m^Q#b2B4f1oT zIjsGz)4}-$rQ*-tSuc%qG>%<4xM#E& zN)7lRK~^2VdiloY4>;#}A!yHOAXEmEi^+eA#05pawGXs>!z)gSoDuI#>bRCq-qjJe zZ)r=A`*EMX6+)~er1kdv1L^)0-PsAEM7JF$O6G8>496$24lkOSR^RTfUuIz%iSfn5b-t!##cs7sQI);gdAvqmn_v|%I9k;fCPl0Z)R1+hNQONJN zH%3jT9sOq*a`LF*MiY=zlSSQZ;{_FL9M07A=In+O!~wR}=bzGEQpk2!Vc0p)qKAH? zOk{(%06W#)DdICQ_S%Q@<0Y+!?9%#$gWJ%)EO->^YZP{<`oB4~9xh zL9-0*c4@B#O2ylYs_g`Ky$zb~v!M`NRaMNFYF*Gsu|7)=JyyMHjFC=HhGUE@{aI|B zJ~ITXU052%7jFb5Ys#fhS_?4kqc7H0EU49B8(Chg0&JzU=Gka#xOz1)H0d4m7ZnRA z=M^tdY|U6T!fmte{W?_r8H~qdq|q{5AMU_2It1I4143n~xL?4&K#BOB48l9_Rdm!(c^C?JU;tF0 zEh@o1y6Qa_>}#AwX{VY+`C^kNkxhgb1P5cB0%xupAXyg9NO=SnXrJUE?rQg{Lcsn+ zAZKctGLfbK_B#^&Nev|0^fB&?DN=ak8|0!np524LD25=s84BP8Vl(3=jflNp{X>e@ z637Ri5xx;&JNl+XYImA|{;XR~P*svYDEWYJ6I5!6uO~2twFC1ZQevB7#3z~(apxn& z^J@>Mc`>PJair{yT`iuan-V+i%|Ho-pA<1?V-k^R2Q<5;Co%XxmL` z018t4T0TTwO^w)Gx{9OSJ^9_|kgwX`7%0Rw!PO~@?xvnfUehvN;2Rc;^l>3kfbtk3 z8{j7p;S&{uTlTe9&HTc38q@%_KQFk<&n{vmrN7y&Cz{etcE->rq!6HL)2F!aa=0%! zM%Bwo!7TQ5t;@a_#Q}sjk{UebWQZ8{cp&HN^$*JfH#8spkhk{R@CVBiPuP@yEhu{} zsQfuhTqV%rioATpEphMfhyRYbVfVW`YwLFXUWm-===J(byMf!5;W^CV1g~2194Xx) zFK|z{pm%n-)-DRe{Qhk(d!QaoI*y%Wn6h7<6A{i*Sob&B^y|Spg!&J$`kN>zwUJ3x zaB$ciu*0FJKg}T ztgnh)ASF8njz5>h6?f#{c=*Yr4W_34$GmVIo8OLWjcZK4a0`+Yv-!*}9 zBwKm;DAsA(nDI-`iH@;`=gP+m{lgFLHK3m$W@?)&dGhDA_Z2xOzI0$p(ZJtH$vCxE zj>+kYNBJzs-TlSx!tSH}%I9fQv)mc!C7X0bKlZv4f&}C3+O-4k7AmVO|KYZ9ydP%(N1^uisV8y;~p`x4qFXD?!_OyN9=w(Od6W; zGrT?G;l2v@Ob5k^8w<9w%Jbjb^|H}PYKo}I~bobd!XrTbzp2Zp~H8lgJ)I3?l&(bDiWf8gE&6b z>)9GB=Iu-6%I((+>=jGP>CzD8c0oWITFZGgM!Q7|JrUYq4#^Y(vuDu-a>OWDa4Y4} z5a_*lW#IL_aVf8L+Ty}c&2VojLEIA-;eQK6Wo?xAuK>i;1VWx3c=!s2;j_*iRHOsb*>6-CgcYP+Ho=L@XLd*j~2ln-;WHg)|cCixksH$K={5rGSD@yB%LI|(NCc8 z1Er8H+QO)~S~K{g?nH|2dB8SKs)BxQ?%G}}o*LV!NG2m*TmR|pWj~g`>)ClJCE#F$ zcj)fBg(dKOKmc$Cy}IRlasngIR>z~kP&WW~9cC951{AKmnZ~ZMsqup6QQf7J0T1;C zK9*Qd5*(HxW=tl|RfjO>nkoW#AU3t>JkuzWxy4-l?xmTv15_r1X@p@dz^{&j&;{Mq z$^0$0q&y?kbdZh)kZ+NfXfqLTG}Q^j>qHlUH4VEK`3y^-z6Y<6O88Hf4v^;}!{t-a zDWg;znYu%6zA1~A5~w?fxO~i8-Ib(^02{c4pXjhDI^2 zXB1LP4dvWuc%PXQ{r!d#6>${rm+M8EJM8yf#!H$Kp8AxwUXm5`7Tu-J$mHeCG>vw|&Ay415}_1w&*9K8+2d3v1N+@a$|820o4u60Tj@u&kI!~q2V9X; z>tMvQDI|O$#m+m2O**ZHq`_{#8)ry6`&5s~2k{O4Du16Fn0P;&_(0!e5%Bel){nU0 zJX~<8U6hoI%yx}qGY_1Tq7YKDJ)ETOCs&W)TiCrK*1%DE*vXdD-7hwE*LUgjeHRM` z&@pkhTi>m#Kc+QIK+2Ybn9-sFVKNHyIgfob4H_77yYh))Rq$7Pw|+aD6&yZ|ki9 z8Zb6s{oBt1G+PgfIcxd}{m@~1nzhe;LH)5;!gS8@ddyabpdBc?7JVl?tS+<#bPSMT z2@0uYdsWN(;Ww)n-PlA-0r+62@bYkEa`k{0s})fJgYZ#5=DmIdEvok7aZJRi{w-|} zkea&6X}ZA3b7&vbDb7)v8CuI(+zzSf3z&P2eOrPNP?D~ zf zn0@)0h;~5F&BG5vOFU!=woW&ZSl~nrs{?1w>nWfW_dnpTd z4qvLDYJ*ft>Sp%M(^_xCZpNBnc66JX}A|ZL9IENM`U>`ph7d<+RQiI}@E8Y)70s zMC*_&))}GlmR}@{v9*nm)29-=rn`Q$rc^4G)GVQHlTr6BpGxtHuU(8AF7Ffh54?5w zj+EYT9>x)PWL-iQ@RNmT?R+|c@=FOmj)5Za6_ z@DkVy4l^L>Z3#SI@s_eVwd3D)<^Ivq8a~J{|4mhOL^<7M4D8){ut;GIqqn`oqCk|x pNh;Wa$C0(mdpqYz&F>xK-uVD=DT5%Jzh8ZT#aXmjr70%*{{RacS`YvL literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ffd27a_256x240.png b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/images/ui-icons_ffd27a_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..e117effa3dca24e7978cfc5f8b967f661e81044f GIT binary patch literal 4369 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 literal 0 HcmV?d00001 diff --git a/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/jquery-ui-1.8.16.custom.css b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/jquery-ui-1.8.16.custom.css new file mode 100644 index 000000000..5547c7b9d --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/static/jquery-ui-1.8.16/ui-lightness/jquery-ui-1.8.16.custom.css @@ -0,0 +1,568 @@ +/* + * jQuery UI CSS Framework 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } +.ui-helper-clearfix { display: inline-block; } +/* required comment for clearfix to work in Opera \*/ +* html .ui-helper-clearfix { height:1%; } +.ui-helper-clearfix { display:block; } +/* end clearfix */ +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } + + +/* + * jQuery UI CSS Framework 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1.1em; } +.ui-widget .ui-widget { font-size: 1em; } +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1em; } +.ui-widget-content { border: 1px solid #dddddd; background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; color: #333333; } +.ui-widget-content a { color: #333333; } +.ui-widget-header { border: 1px solid #e78f08; background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; } +.ui-widget-header a { color: #ffffff; } + +/* Interaction states +----------------------------------*/ +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #1c94c4; } +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #fbcb09; background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #c77405; } +.ui-state-hover a, .ui-state-hover a:hover { color: #c77405; text-decoration: none; } +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #fbd850; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #eb8f00; } +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; text-decoration: none; } +.ui-widget :active { outline: none; } + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x; color: #363636; } +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat; color: #ffffff; } +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } +.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } +.ui-widget-header .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } +.ui-state-default .ui-icon { background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-active .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); } +.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_228ef1_256x240.png); } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffd27a_256x240.png); } + +/* positioning */ +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-off { background-position: -96px -144px; } +.ui-icon-radio-on { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } + +/* Overlays */ +.ui-widget-overlay { background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); } +.ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -khtml-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }/* + * jQuery UI Resizable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Resizable#theming + */ +.ui-resizable { position: relative;} +.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } +.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } +.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } +.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } +.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } +.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } +.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } +.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } +.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* + * jQuery UI Selectable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Selectable#theming + */ +.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } +/* + * jQuery UI Accordion 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Accordion#theming + */ +/* IE/Win - Fix animation bug - #4615 */ +.ui-accordion { width: 100%; } +.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } +.ui-accordion .ui-accordion-li-fix { display: inline; } +.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } +.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } +.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } +.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } +.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } +.ui-accordion .ui-accordion-content-active { display: block; } +/* + * jQuery UI Autocomplete 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete#theming + */ +.ui-autocomplete { position: absolute; cursor: default; } + +/* workarounds */ +* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ + +/* + * jQuery UI Menu 1.8.16 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu#theming + */ +.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.ui-menu .ui-menu { + margin-top: -3px; +} +.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.ui-menu .ui-menu-item a.ui-state-hover, +.ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +/* + * jQuery UI Button 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Button#theming + */ +.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ +.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ +button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ +.ui-button-icons-only { width: 3.4em; } +button.ui-button-icons-only { width: 3.7em; } + +/*button text element */ +.ui-button .ui-button-text { display: block; line-height: 1.4; } +.ui-button-text-only .ui-button-text { padding: .4em 1em; } +.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } +.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } +.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } +.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } +/* no icon support for input elements, provide padding by default */ +input.ui-button { padding: .4em 1em; } + +/*button icon element(s) */ +.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } +.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } +.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } +.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } +.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } + +/*button sets*/ +.ui-buttonset { margin-right: 7px; } +.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } + +/* workarounds */ +button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ +/* + * jQuery UI Dialog 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog#theming + */ +.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } +.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } +.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } +.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } +.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } +.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } +.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } +.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } +.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } +.ui-draggable .ui-dialog-titlebar { cursor: move; } +/* + * jQuery UI Slider 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Slider#theming + */ +.ui-slider { position: relative; text-align: left; } +.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } +.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } + +.ui-slider-horizontal { height: .8em; } +.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } +.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } +.ui-slider-horizontal .ui-slider-range-min { left: 0; } +.ui-slider-horizontal .ui-slider-range-max { right: 0; } + +.ui-slider-vertical { width: .8em; height: 100px; } +.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } +.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } +.ui-slider-vertical .ui-slider-range-min { bottom: 0; } +.ui-slider-vertical .ui-slider-range-max { top: 0; }/* + * jQuery UI Tabs 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs#theming + */ +.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ +.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } +.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } +.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } +.ui-tabs .ui-tabs-hide { display: none !important; } +/* + * jQuery UI Datepicker 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Datepicker#theming + */ +.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } +.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } +.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } +.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } +.ui-datepicker .ui-datepicker-prev { left:2px; } +.ui-datepicker .ui-datepicker-next { right:2px; } +.ui-datepicker .ui-datepicker-prev-hover { left:1px; } +.ui-datepicker .ui-datepicker-next-hover { right:1px; } +.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } +.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } +.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } +.ui-datepicker select.ui-datepicker-month-year {width: 100%;} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { width: 49%;} +.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } +.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } +.ui-datepicker td { border: 0; padding: 1px; } +.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } +.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } +.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { width:auto; } +.ui-datepicker-multi .ui-datepicker-group { float:left; } +.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } +.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } +.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } +.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } +.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } +.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } + +/* RTL support */ +.ui-datepicker-rtl { direction: rtl; } +.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } +.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } +.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } +.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } +.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } +.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } +.ui-datepicker-rtl .ui-datepicker-group { float:right; } +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } + +/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ +.ui-datepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ +}/* + * jQuery UI Progressbar 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar#theming + */ +.ui-progressbar { height:2em; text-align: left; } +.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/access_confirmation.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/access_confirmation.jsp new file mode 100644 index 000000000..09cc5e8ba --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/access_confirmation.jsp @@ -0,0 +1,44 @@ +<%@ page import="org.springframework.security.core.AuthenticationException" %> +<%@ page import="org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter" %> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + + MifosNg Platform: Access Confirmation + + + + +

    MifosNg

    + +
    + + +
    +

    Woops!

    + +

    Access could not be granted. (<%= ((AuthenticationException) session.getAttribute(AbstractAuthenticationProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage() %>)

    +
    +
    + + +

    Please Confirm

    + +

    You hereby authorize "" to access the following resource:

    + +
      +
    • +
    + +
    " method="POST"> + " type="hidden"/> + +
    +
    + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/common-head.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/common-head.jsp new file mode 100644 index 000000000..761908339 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/common-head.jsp @@ -0,0 +1,15 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + +${applicationName} ${pageTitle} + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/firsttimelogin.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/firsttimelogin.jsp new file mode 100644 index 000000000..1170d65ef --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/firsttimelogin.jsp @@ -0,0 +1,60 @@ +<%@ page session="true" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + + + + +
    +
    +
     
    +
    +

    All fields are required.

    + + +
    + Change username and password + + +
    +
    + + You have the following errors: +
    + +
    +
    +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/index.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/index.jsp new file mode 100644 index 000000000..6cfb032b8 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/index.jsp @@ -0,0 +1,19 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + MifosNg Platform + + + +

    Successful Authentication

    + +
    +

    There are no UI capabilities on MifosNg Platform. You must use a suitable consumer (client) application to use our platform services.

    +
    + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/login.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/login.jsp new file mode 100644 index 000000000..c00e85f9f --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/login.jsp @@ -0,0 +1,70 @@ +<%@ page session="true" %> +<%@ page import="org.springframework.security.core.AuthenticationException" %> +<%@ page import="org.springframework.security.web.WebAttributes" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + + + + + + + + + +
    +
    +
     
    +

    Mifos - Individual Lending: Login

    +
    + + +
    +
    + + You have the following errors: +
    +

    Your login attempt was not successful. (<%= ((AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)).getMessage() %>)

    +
    +
    +
    +
    + + + + + +
    +
    + Login + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/sessionTimeout.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/sessionTimeout.jsp new file mode 100644 index 000000000..a95b295f5 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/sessionTimeout.jsp @@ -0,0 +1,45 @@ +<%@ page import="org.springframework.security.core.AuthenticationException" %> +<%@ page import="org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + + + + + + + + + Sparklr + + + + +

    Sparklr

    + +
    + +
    +

    Woops!

    + +

    Your login attempt was not successful. (<%= ((AuthenticationException) session.getAttribute(AbstractAuthenticationProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage() %>)

    +
    +
    + + + +

    Login

    + +

    We've got a grand total of 2 users: marissa and paul. Go ahead and log in. Marissa's password is "koala" and Paul's password is "emu".

    +
    +

    +

    + +

    +
    +
    +
    + + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signup.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signup.jsp new file mode 100644 index 000000000..da2555ada --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signup.jsp @@ -0,0 +1,67 @@ +<%@ page session="true" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + + + + +
    +
    +
     
    +
    +

    All fields are required.

    + + +
    + Sign up + + +
    +
    + + You have the following errors: +
    + +
    +
    +
    +
    + + + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signupsuccess.jsp b/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signupsuccess.jsp new file mode 100644 index 000000000..7c968afbe --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/views/tenant/signupsuccess.jsp @@ -0,0 +1,67 @@ +<%@ page session="true" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + + + + + + +
    +
    +
     
    +
    +
    + + You successfully registered your organisation ${signupFormBean.organisationName} on a mifosng community platform. +
    +
    +

    Contact Name: ${signupFormBean.contactName}

    +

    Contact Email: ${signupFormBean.contactEmail}

    +
    +

    What happens next?

    +
    +

    An email with username and password details is sent to the inbox of ${signupFormBean.contactEmail}. When logging in for the first time, you will be asked to change the username and password associated + with the account. +

    +
    +

    Where do I login?

    +
    +

    + You have registered your organisation with the platform. The only capabilities exposed through the platforms user interface are that around signup, login and change password. + All other capabilities are exposed through a HTTP API (documentation to follow) which is secured using OAuth 1.0 protocol. +

    +
    +

    + This means that client (or consumer in OAuth terminology) applications can be developed which make use of backend (or provider in OAuth terminology) capabilities. +

    +
    +

    Below is a list of client applications that are registered on this platform at present which you can use:

    +
    + + + + + + + + + + + + + + + + + +
    Application NameApplication DescriptionDeveloped ByLocation
    MifosNG: Individual LendingProvides capabilities specific to the individual lending credit methodology. Specifically capture of new customer information, loan appraisal, cashflow analysis, loan disbursement, loan monitoring etcMifos Communityhttp://localhost:8080/mifosng-individual-lending-app/
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/mifosng-provider/src/main/webapp/WEB-INF/web.xml b/mifosng-provider/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..17821a1d7 --- /dev/null +++ b/mifosng-provider/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,93 @@ + + + + MifosNg - Platform + A provider of microfinance services which applications can consume once authenticated and authorized. + + + webAppRootKey + mifosngserver.root + + + + contextConfigLocation + classpath*:META-INF/spring/appContext.xml + + + + + + filterChainProxy + org.springframework.web.filter.DelegatingFilterProxy + + + + filterChainProxy + /* + + + + org.springframework.web.context.ContextLoaderListener + + + + org.springframework.security.web.session.HttpSessionEventPublisher + + + + jersey-serlvet + com.sun.jersey.spi.spring.container.servlet.SpringServlet + + + com.sun.jersey.api.json.POJOMappingFeature + true + + + 1 + + + + jersey-serlvet + /api/* + + + + dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/spring/webmvc-config.xml + + 1 + + + + dispatcher + / + + + + + *.jsp + true + + + + + 20 + + \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessorTest.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessorTest.java new file mode 100644 index 000000000..0377248fe --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/DerivedLoanDataProcessorTest.java @@ -0,0 +1,376 @@ +package org.mifosng.platform.loan.domain; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.joda.time.LocalDate; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mifosng.data.CurrencyData; +import org.mifosng.data.DerivedLoanData; +import org.mifosng.data.LoanRepaymentData; +import org.mifosng.data.LoanRepaymentPeriodData; +import org.mifosng.data.LoanRepaymentScheduleData; +import org.mifosng.data.MoneyData; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DerivedLoanDataProcessorTest { + + private DerivedLoanDataProcessor loanDataProcessor = new DerivedLoanDataProcessor(); + private MonetaryCurrency cfaCurrency = new MonetaryCurrencyBuilder().withCode("XOF").withDigitsAfterDecimal(0).build(); + private Money arrearsTolerance = new MoneyBuilder().with(cfaCurrency).with("0.0").build(); + + @Test + public void shouldDeriveLoanRepaymentScheduleWithSingleRepaymentPeriodAndRepaymentInFull() { + + + + List repaymentScheduleWithSinglePeriod = new ArrayList(); + + LoanRepaymentScheduleInstallment firstRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now()) + .build(); + repaymentScheduleWithSinglePeriod.add(firstRepaymentPeriod); + + List singleRepaymentInFull = new ArrayList(); + + Money fullAmount = new MoneyBuilder().with(cfaCurrency).with("1010").build(); + + LoanTransaction fullRepayment = new LoanTransactionBuilder().with(fullAmount).build(); + singleRepaymentInFull.add(fullRepayment); + + // exercise test + DerivedLoanData derivedData = loanDataProcessor.process(repaymentScheduleWithSinglePeriod, singleRepaymentInFull, asCurrencyData(cfaCurrency), arrearsTolerance); + + // verification + assertThat(derivedData, is(notNullValue())); + + LoanRepaymentScheduleData repaymentSchedule = derivedData.getRepaymentSchedule(); + assertThat(repaymentSchedule, is(notNullValue())); + + List periods = repaymentSchedule.getPeriods(); + assertThat(periods, is(notNullValue())); + assertThat(periods.size(), is(1)); + + LoanRepaymentPeriodData periodData = periods.get(0); + + MoneyData expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + MoneyData expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + + MoneyData expectedTotal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1010").build()); + + assertThat(periodData.getPrincipal(), is(expectedPrincipal)); + assertThat(periodData.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(periodData.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(periodData.getInterest(), is(expectedInterest)); + assertThat(periodData.getInterestPaid(), is(expectedInterestPaid)); + assertThat(periodData.getInterestOutstanding(), is(expectedInterestOutstanding)); + + // assert repayments are ok + List loanRepayments = derivedData.getLoanRepayments(); + assertThat(loanRepayments, is(notNullValue())); + assertThat(loanRepayments.size(), is(1)); + + LoanRepaymentData firstRepayment = loanRepayments.get(0); + assertThat(firstRepayment.getPrincipal(), is(expectedPrincipalPaid)); + assertThat(firstRepayment.getInterest(), is(expectedInterestPaid)); + assertThat(firstRepayment.getTotal(), is(expectedTotal)); + } + + private CurrencyData asCurrencyData(MonetaryCurrency currency) { + return new CurrencyData(currency.getCode(), "", currency.getDigitsAfterDecimal(), "CFA", ""); + } + + @Test + public void shouldDeriveLoanRepaymentScheduleWithSingleRepaymentPeriodAndPartialRepayment() { + + List repaymentScheduleWithSinglePeriod = new ArrayList(); + + LoanRepaymentScheduleInstallment firstRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now()) + .build(); + repaymentScheduleWithSinglePeriod.add(firstRepaymentPeriod); + + List singlePartialRepayment = new ArrayList(); + + Money partialAmount = new MoneyBuilder().with(cfaCurrency).with("510").build(); + + LoanTransaction partialRepayment = new LoanTransactionBuilder().with(partialAmount).build(); + singlePartialRepayment.add(partialRepayment); + + // exercise test + DerivedLoanData derivedData = loanDataProcessor.process(repaymentScheduleWithSinglePeriod, singlePartialRepayment, asCurrencyData(cfaCurrency), arrearsTolerance); + + // verification + assertThat(derivedData, is(notNullValue())); + + LoanRepaymentScheduleData repaymentSchedule = derivedData.getRepaymentSchedule(); + assertThat(repaymentSchedule, is(notNullValue())); + + List periods = repaymentSchedule.getPeriods(); + assertThat(periods, is(notNullValue())); + assertThat(periods.size(), is(1)); + + LoanRepaymentPeriodData periodData = periods.get(0); + + MoneyData expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("500").build()); + MoneyData expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("500").build()); + MoneyData expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + + MoneyData expectedTotal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("510").build()); + + assertThat(periodData.getPrincipal(), is(expectedPrincipal)); + assertThat(periodData.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(periodData.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(periodData.getInterest(), is(expectedInterest)); + assertThat(periodData.getInterestPaid(), is(expectedInterestPaid)); + assertThat(periodData.getInterestOutstanding(), is(expectedInterestOutstanding)); + + // assert repayments are ok + List loanRepayments = derivedData.getLoanRepayments(); + assertThat(loanRepayments, is(notNullValue())); + assertThat(loanRepayments.size(), is(1)); + + LoanRepaymentData firstRepayment = loanRepayments.get(0); + assertThat(firstRepayment.getPrincipal(), is(expectedPrincipalPaid)); + assertThat(firstRepayment.getInterest(), is(expectedInterestPaid)); + assertThat(firstRepayment.getTotal(), is(expectedTotal)); + } + + + @Test + public void shouldDeriveLoanRepaymentScheduleWithSingleRepaymentPeriodAndPartialRepaymentLessThanInterestComponent() { + + List repaymentScheduleWithSinglePeriod = new ArrayList(); + + LoanRepaymentScheduleInstallment firstRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now()) + .build(); + repaymentScheduleWithSinglePeriod.add(firstRepaymentPeriod); + + List singlePartialRepayment = new ArrayList(); + + Money partialAmount = new MoneyBuilder().with(cfaCurrency).with("5").build(); + + LoanTransaction partialRepayment = new LoanTransactionBuilder().with(partialAmount).build(); + singlePartialRepayment.add(partialRepayment); + + // exercise test + DerivedLoanData derivedData = loanDataProcessor.process(repaymentScheduleWithSinglePeriod, singlePartialRepayment, asCurrencyData(cfaCurrency), arrearsTolerance); + + // verification + assertThat(derivedData, is(notNullValue())); + + LoanRepaymentScheduleData repaymentSchedule = derivedData.getRepaymentSchedule(); + assertThat(repaymentSchedule, is(notNullValue())); + + List periods = repaymentSchedule.getPeriods(); + assertThat(periods, is(notNullValue())); + assertThat(periods.size(), is(1)); + + LoanRepaymentPeriodData periodData = periods.get(0); + + MoneyData expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + MoneyData expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("5").build()); + MoneyData expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("5").build()); + + MoneyData expectedTotal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("5").build()); + + assertThat(periodData.getPrincipal(), is(expectedPrincipal)); + assertThat(periodData.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(periodData.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(periodData.getInterest(), is(expectedInterest)); + assertThat(periodData.getInterestPaid(), is(expectedInterestPaid)); + assertThat(periodData.getInterestOutstanding(), is(expectedInterestOutstanding)); + + // assert repayments are ok + List loanRepayments = derivedData.getLoanRepayments(); + assertThat(loanRepayments, is(notNullValue())); + assertThat(loanRepayments.size(), is(1)); + + LoanRepaymentData firstRepayment = loanRepayments.get(0); + assertThat(firstRepayment.getPrincipal(), is(expectedPrincipalPaid)); + assertThat(firstRepayment.getInterest(), is(expectedInterestPaid)); + assertThat(firstRepayment.getTotal(), is(expectedTotal)); + } + + @Test + public void shouldDeriveLoanRepaymentScheduleWithSingleRepaymentPeriodAndOverPayment() { + + List repaymentScheduleWithSinglePeriod = new ArrayList(); + + LoanRepaymentScheduleInstallment firstRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now()) + .build(); + repaymentScheduleWithSinglePeriod.add(firstRepaymentPeriod); + + List singleOverRepayment = new ArrayList(); + + Money partialAmount = new MoneyBuilder().with(cfaCurrency).with("1200").build(); + + LoanTransaction overRepayment = new LoanTransactionBuilder().with(partialAmount).build(); + singleOverRepayment.add(overRepayment); + + // exercise test + DerivedLoanData derivedData = loanDataProcessor.process(repaymentScheduleWithSinglePeriod, singleOverRepayment, asCurrencyData(cfaCurrency), arrearsTolerance); + + // verification + assertThat(derivedData, is(notNullValue())); + + LoanRepaymentScheduleData repaymentSchedule = derivedData.getRepaymentSchedule(); + assertThat(repaymentSchedule, is(notNullValue())); + + List periods = repaymentSchedule.getPeriods(); + assertThat(periods, is(notNullValue())); + assertThat(periods.size(), is(1)); + + LoanRepaymentPeriodData periodData = periods.get(0); + + MoneyData expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + MoneyData expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + + MoneyData expectedTotal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1010").build()); + MoneyData expectedOverpayment = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("190").build()); + + assertThat(periodData.getPrincipal(), is(expectedPrincipal)); + assertThat(periodData.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(periodData.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(periodData.getInterest(), is(expectedInterest)); + assertThat(periodData.getInterestPaid(), is(expectedInterestPaid)); + assertThat(periodData.getInterestOutstanding(), is(expectedInterestOutstanding)); + + // assert repayments are ok + List loanRepayments = derivedData.getLoanRepayments(); + assertThat(loanRepayments, is(notNullValue())); + assertThat(loanRepayments.size(), is(1)); + + LoanRepaymentData firstRepayment = loanRepayments.get(0); + assertThat(firstRepayment.getPrincipal(), is(expectedPrincipalPaid)); + assertThat(firstRepayment.getInterest(), is(expectedInterestPaid)); + assertThat(firstRepayment.getTotal(), is(expectedTotal)); + assertThat(firstRepayment.getOverpaid(), is(expectedOverpayment)); + } + + @Test + public void shouldDeriveLoanRepaymentScheduleWithTwoRepaymentPeriodAndOneSinglePayment() { + + List repaymentScheduleWithSinglePeriod = new ArrayList(); + + LoanRepaymentScheduleInstallment firstRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now()) + .build(); + + LoanRepaymentScheduleInstallment secondRepaymentPeriod = new LoanRepaymentScheduleInstallmentBuilder(cfaCurrency) + .withPrincipal("1000") + .withInterest("10") + .withDueDate(LocalDate.now().plusWeeks(1)) + .build(); + + repaymentScheduleWithSinglePeriod.add(firstRepaymentPeriod); + repaymentScheduleWithSinglePeriod.add(secondRepaymentPeriod); + + List singleRepayment = new ArrayList(); + + Money partialAmount = new MoneyBuilder().with(cfaCurrency).with("1200").build(); + + LoanTransaction overRepaymentForFirstPeriod = new LoanTransactionBuilder().with(partialAmount).build(); + singleRepayment.add(overRepaymentForFirstPeriod); + + // exercise test + DerivedLoanData derivedData = loanDataProcessor.process(repaymentScheduleWithSinglePeriod, singleRepayment, asCurrencyData(cfaCurrency), arrearsTolerance); + + // verification + assertThat(derivedData, is(notNullValue())); + + LoanRepaymentScheduleData repaymentSchedule = derivedData.getRepaymentSchedule(); + assertThat(repaymentSchedule, is(notNullValue())); + + List periods = repaymentSchedule.getPeriods(); + assertThat(periods, is(notNullValue())); + assertThat(periods.size(), is(2)); + + LoanRepaymentPeriodData firstPeriod = periods.get(0); + + MoneyData expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + MoneyData expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + MoneyData expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + MoneyData expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + + assertThat(firstPeriod.getPrincipal(), is(expectedPrincipal)); + assertThat(firstPeriod.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(firstPeriod.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(firstPeriod.getInterest(), is(expectedInterest)); + assertThat(firstPeriod.getInterestPaid(), is(expectedInterestPaid)); + assertThat(firstPeriod.getInterestOutstanding(), is(expectedInterestOutstanding)); + + LoanRepaymentPeriodData secondPeriod = periods.get(1); + + expectedPrincipal = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1000").build()); + expectedPrincipalPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("180").build()); + expectedPrincipalOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("820").build()); + expectedInterest = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + expectedInterestPaid = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("10").build()); + expectedInterestOutstanding = moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("0").build()); + + assertThat(secondPeriod.getPrincipal(), is(expectedPrincipal)); + assertThat(secondPeriod.getPrincipalPaid(), is(expectedPrincipalPaid)); + assertThat(secondPeriod.getPrincipalOutstanding(), is(expectedPrincipalOutstanding)); + + assertThat(secondPeriod.getInterest(), is(expectedInterest)); + assertThat(secondPeriod.getInterestPaid(), is(expectedInterestPaid)); + assertThat(secondPeriod.getInterestOutstanding(), is(expectedInterestOutstanding)); + + // assert repayments are ok + List loanRepayments = derivedData.getLoanRepayments(); + assertThat(loanRepayments, is(notNullValue())); + assertThat(loanRepayments.size(), is(1)); + + LoanRepaymentData firstRepayment = loanRepayments.get(0); + assertThat(firstRepayment.getPrincipal(), is(moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1180").build()))); + assertThat(firstRepayment.getInterest(), is(moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("20").build()))); + assertThat(firstRepayment.getTotal(), is(moneyDataFrom(new MoneyBuilder().with(cfaCurrency).with("1200").build()))); + } + + private MoneyData moneyDataFrom(Money money) { + CurrencyData currency = new CurrencyData(money.getCurrencyCode(), "", money.getCurrencyDigitsAfterDecimal(), "CFA", ""); + return MoneyData.of(currency, money.getAmount()); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java new file mode 100644 index 000000000..c2051c3bf --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallmentBuilder.java @@ -0,0 +1,41 @@ +package org.mifosng.platform.loan.domain; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; + +public class LoanRepaymentScheduleInstallmentBuilder { + + private Loan loan = null; + private Integer installmentNumber = Integer.valueOf(1); + private LocalDate from = LocalDate.now(); + private LocalDate dueDate = LocalDate.now(); + private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); + private Money principal = new MoneyBuilder().build(); + private Money interest = new MoneyBuilder().build(); + + public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) { + this.currencyDetail = currencyDetail; + this.principal = new MoneyBuilder().with(currencyDetail).build(); + this.interest = new MoneyBuilder().with(currencyDetail).build(); + } + + public LoanRepaymentScheduleInstallment build() { + return new LoanRepaymentScheduleInstallment(loan, installmentNumber, from, dueDate, principal.getAmount(), interest.getAmount()); + } + + public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) { + this.principal = new MoneyBuilder().with(currencyDetail).with(withPrincipal).build(); + return this; + } + + public LoanRepaymentScheduleInstallmentBuilder withInterest(String withInterest) { + this.interest = new MoneyBuilder().with(currencyDetail).with(withInterest).build(); + return this; + } + + public LoanRepaymentScheduleInstallmentBuilder withDueDate(LocalDate withDueDate) { + this.dueDate = withDueDate; + return this; + } +} diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java new file mode 100644 index 000000000..40751b132 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/LoanTransactionBuilder.java @@ -0,0 +1,21 @@ +package org.mifosng.platform.loan.domain; + +import org.joda.time.LocalDate; +import org.mifosng.platform.currency.domain.Money; + +public class LoanTransactionBuilder { + + private Money amount = new MoneyBuilder().build(); + private LocalDate paymentDate = LocalDate.now(); + + public LoanTransaction build() { + return LoanTransaction.repayment(amount, paymentDate); + } + + public LoanTransactionBuilder with(Money newAmount) { + this.amount = newAmount; + return this; + } + + +} diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MonetaryCurrencyBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MonetaryCurrencyBuilder.java new file mode 100644 index 000000000..b6ae8ced9 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MonetaryCurrencyBuilder.java @@ -0,0 +1,23 @@ +package org.mifosng.platform.loan.domain; + +import org.mifosng.platform.currency.domain.MonetaryCurrency; + +public class MonetaryCurrencyBuilder { + + private String code = "XOF"; + private int digitsAfterDecimal = 0; + + public MonetaryCurrency build() { + return new MonetaryCurrency(code, digitsAfterDecimal); + } + + public MonetaryCurrencyBuilder withCode(String withCode) { + this.code = withCode; + return this; + } + + public MonetaryCurrencyBuilder withDigitsAfterDecimal(int withDigitsAfterDecimal) { + this.digitsAfterDecimal = withDigitsAfterDecimal; + return this; + } +} diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MoneyBuilder.java b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MoneyBuilder.java new file mode 100644 index 000000000..8e39d75d0 --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loan/domain/MoneyBuilder.java @@ -0,0 +1,26 @@ +package org.mifosng.platform.loan.domain; + +import java.math.BigDecimal; + +import org.mifosng.platform.currency.domain.MonetaryCurrency; +import org.mifosng.platform.currency.domain.Money; + +public class MoneyBuilder { + + private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); + private BigDecimal newAmount = BigDecimal.ZERO; + + public Money build() { + return Money.of(currencyDetail, newAmount); + } + + public MoneyBuilder with(MonetaryCurrency withDetail) { + this.currencyDetail = withDetail; + return this; + } + + public MoneyBuilder with(String withAmount) { + this.newAmount = BigDecimal.valueOf(Double.valueOf(withAmount)); + return this; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/PmtTest.java b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/PmtTest.java new file mode 100644 index 000000000..d9f3ffe0e --- /dev/null +++ b/mifosng-provider/src/test/java/org/mifosng/platform/loanschedule/domain/PmtTest.java @@ -0,0 +1,27 @@ +package org.mifosng.platform.loanschedule.domain; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PmtTest { + + @Test + public void shouldCalculatePmt() { + + double interestRateFraction = Double.valueOf("0.0175"); + + double numberOfPayments = Double.valueOf("12"); + double principal = Double.valueOf("-144300"); + double futureValue = Double.valueOf("0"); + boolean type = false; + + double payment = DecliningBalanceEqualInstallmentsLoanScheduleGenerator.pmt(interestRateFraction, numberOfPayments, principal, futureValue, type); + + assertThat(payment, is(Double.valueOf("13436.317553939027"))); + } +} diff --git a/mifosng-provider/src/test/resources/META-INF/context.xml b/mifosng-provider/src/test/resources/META-INF/context.xml new file mode 100644 index 000000000..2b6b3f1e4 --- /dev/null +++ b/mifosng-provider/src/test/resources/META-INF/context.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file