From 1bd3401f25a8f7e289314686e4bafd2ce416724e Mon Sep 17 00:00:00 2001
From: Ambika
Date: Tue, 17 Mar 2026 22:40:45 +0530
Subject: [PATCH] FINERACT-2494: Standardize operationId for Savings module and
resolve Checkstyle violations
---
.asf.yaml | 3 +
.github/workflows/build-cucumber.yml | 14 +-
.github/workflows/build-docker.yml | 6 +-
.github/workflows/build-documentation.yml | 10 +-
.github/workflows/build-e2e-tests.yml | 12 +-
.github/workflows/build-mariadb.yml | 21 +-
.github/workflows/build-mysql.yml | 12 +-
.github/workflows/build-postgresql.yml | 14 +-
.../workflows/liquibase-only-postgresql.yml | 10 +-
.../mifos-fineract-client-publish.yml | 8 +-
.../pr-one-commit-per-user-check.yml | 57 +
.github/workflows/publish-dockerhub.yml | 6 +-
...tegration-test-sequentially-postgresql.yml | 12 +-
.github/workflows/smoke-messaging.yml | 6 +-
.github/workflows/sonarqube.yml | 6 +-
.github/workflows/stale.yml | 2 +-
.../verify-api-backward-compatibility.yml | 243 +
.github/workflows/verify-commits.yml | 4 +-
...erify-liquibase-backward-compatibility.yml | 8 +-
README.md | 6 +-
build.gradle | 5 +-
.../org.apache.fineract.dependencies.gradle | 10 +-
config/docker/compose/mariadb.yml | 3 +-
config/docker/compose/postgresql.yml | 2 +-
.../docker/mariadb/conf.d/mariadb_compat.cnf | 17 +
docker-compose-mariadb.yml | 3 +-
docker-compose-mysql.yml | 34 +
.../api/GLClosuresApiResourceSwagger.java | 20 +-
.../FinancialActivityAccountsApiResource.java | 6 +-
.../glaccount/api/GLAccountsApiResource.java | 17 +-
...sioningEntriesReadPlatformServiceImpl.java | 57 +-
.../main/avro/loan/v1/LoanChargeDataV1.avsc | 12 +
.../client/feign/FineractFeignClient.java | 10 +
.../fineract/client/util/FineractClient.java | 6 +
.../client/test/FineractClientDemo.java | 2 +-
.../org/apache/fineract/cob/COBConstant.java | 4 +
.../cob/common/CommonPartitioner.java | 101 +
.../cob/converter/COBParameterConverter.java | 1 +
.../cob/data/BusinessStepNameAndOrder.java | 10 +
.../fineract/cob/data/LoanCOBParameter.java | 1 +
.../cob/domain/AbstractLockingService.java | 91 +
.../fineract/cob/domain/AccountLock.java | 110 +
.../cob/domain/AccountLockRepository.java | 49 +
.../CustomLoanAccountLockRepository.java | 2 +-
.../apache/fineract/cob/domain/LockOwner.java | 0
.../fineract/cob/domain/LockingService.java | 12 +-
.../LockCannotBeAppliedException.java | 4 +-
.../cob/exceptions/LockedReadException.java | 4 +-
.../listener/AbstractLoanItemListener.java | 23 +-
.../cob/processor/AbstractItemProcessor.java | 75 +
.../cob/resolver/BusinessDateResolver.java | 4 +-
.../cob/resolver/CatchUpFlagResolver.java | 4 +-
.../service/AbstractAccountLockService.java | 21 +-
.../cob/service/AccountLockService.java | 33 +
.../BeforeStepLockingItemReaderHelper.java | 69 +
.../cob/service/RetrieveIdService.java | 10 +-
.../cob/tasklet/ApplyCommonLockTasklet.java | 119 +
.../module/fineract-cob/persistence.xml | 2 +-
.../CommandPersistenceConfiguration.java | 2 +-
.../fineract/command/CommandBaseTest.java | 8 +-
.../command/CommandStrategyProvider.java | 2 +
.../service/CommandWrapperBuilder.java | 126 +-
...CommandSourceWritePlatformServiceImpl.java | 8 +
.../api/GlobalConfigurationConstants.java | 3 +
.../domain/ConfigurationDomainService.java | 6 +
.../serialization/ThrowableSerialization.java | 0
.../dataqueries/service/CleanupService.java | 27 +
.../infrastructure/jobs/service/JobName.java | 1 +
.../springbatch/PropertyService.java | 0
.../workingdays/domain/WorkingDays.java | 60 +-
.../domain/WorkingDaysEnumerations.java | 4 +-
.../account/PortfolioAccountType.java | 10 -
.../fineract/portfolio/fund/domain/Fund.java | 2 +
.../portfolio/fund/domain/FundRepository.java | 0
.../fund/exception/FundNotFoundException.java | 0
.../fund/service/FundReadPlatformService.java | 0
.../api/PaymentTypeApiResource.java | 2 +-
.../savings/domain/SavingsHelper.java | 3 +
.../savings/service/SavingsEnumerations.java | 9 +
.../useradministration/domain/AppUser.java | 11 +
.../api-backward-compatibility.adoc | 190 +
.../docs/en/chapters/architecture/index.adoc | 2 +
.../src/docs/en/chapters/features/index.adoc | 1 +
.../features/loan-origination-details.adoc | 494 +
.../docs/en/chapters/testing/cucumber.adoc | 13 +-
.../docs/en/chapters/testing/integration.adoc | 2 +-
.../api/ImagesApiResource.java | 3 +-
.../exception/DocumentNotFoundException.java | 4 +-
.../service/ImageReadPlatformServiceImpl.java | 31 +-
.../ImageWritePlatformServiceImpl.java | 18 +-
.../ImageReadPlatformServiceImplTest.java | 73 +
.../ImageWritePlatformServiceImplTest.java | 115 +
fineract-e2e-tests-core/build.gradle | 4 +
.../config/TestDatabaseConfiguration.java | 68 +
.../fineract/test/data/ChargeProductType.java | 3 +-
.../apache/fineract/test/data/LoanStatus.java | 2 +
.../data/codevalue/CodeValueResolver.java | 2 +-
.../fineract/test/data/job/DefaultJob.java | 3 +-
.../data/loanproduct/DefaultLoanProduct.java | 2 +
.../DefaultWorkingCapitalLoanProduct.java | 30 +
.../WorkingCapitalLoanProduct.java | 24 +
.../factory/LoanProductsRequestFactory.java | 5 +
.../test/factory/LoanRequestFactory.java | 2 +-
.../factory/WorkingCapitalRequestFactory.java | 159 +
.../test/helper/ErrorMessageHelper.java | 46 +
.../helper/GlobalConfigurationHelper.java | 4 +-
.../apache/fineract/test/helper/Utils.java | 4 +
.../test/helper/WorkFlowJobHelper.java | 42 +
.../helper/WorkingCapitalLoanTestHelper.java | 98 +
.../BaseFineractInitializerConfiguration.java | 3 +-
.../messaging/event/EventCheckHelper.java | 2 +-
.../fineract/test/service/JobService.java | 2 +-
.../AssetExternalizationStepDef.java | 8 +-
.../test/stepdef/common/BatchApiStepDef.java | 12 +-
.../BusinessStepConfigurationStepDef.java | 131 +
.../test/stepdef/common/ClientStepDef.java | 38 +-
.../test/stepdef/common/CurrencyStepDef.java | 138 +
.../common/GlobalConfigurationStepDef.java | 2 +-
.../stepdef/common/JournalEntriesStepDef.java | 10 +-
.../test/stepdef/common/OfficeStepDef.java | 50 +
.../test/stepdef/common/SchedulerStepDef.java | 35 +
.../test/stepdef/common/UserStepDef.java | 2 +-
.../common/WorkingCapitalLoanCobStepDef.java | 259 +
.../loan/LoanChargeAdjustmentStepDef.java | 7 +-
.../test/stepdef/loan/LoanChargeStepDef.java | 22 +-
.../stepdef/loan/LoanDelinquencyStepDef.java | 4 +-
.../stepdef/loan/LoanOriginationStepDef.java | 33 +-
.../test/stepdef/loan/LoanReAgingStepDef.java | 19 +-
.../loan/LoanReAmortizationStepDef.java | 8 +-
.../stepdef/loan/LoanRepaymentStepDef.java | 14 +-
.../stepdef/loan/LoanRescheduleStepDef.java | 11 +
.../test/stepdef/loan/LoanStepDef.java | 147 +-
.../stepdef/loan/WorkingCapitalStepDef.java | 808 ++
.../stepdef/reporting/ReportingStepDef.java | 136 +
.../stepdef/saving/SavingsAccountStepDef.java | 20 +-
.../fineract/test/support/TestContextKey.java | 13 +
.../fineract-test-application.properties | 7 +
fineract-e2e-tests-runner/build.gradle | 6 +-
.../global/ChargeGlobalInitializerStep.java | 7 +
.../global/CodeGlobalInitializerStep.java | 5 +-
...lActivityMappingGlobalInitializerStep.java | 3 +-
.../global/GLGlobalInitializerStep.java | 2 +-
.../LoanProductGlobalInitializerStep.java | 56 +-
.../WcpCobBusinessStepInitializerStep.java | 50 +
.../global/WorkingCapitalInitializerStep.java | 99 +
.../suite/JobSuiteInitializerStep.java | 2 +-
.../test/resources/features/Currency.feature | 17 +
.../resources/features/EMICalculation.feature | 4 +-
.../resources/features/Loan-Part1.feature | 931 ++
.../resources/features/Loan-Part2.feature | 2581 +++++
.../resources/features/Loan-Part3.feature | 2919 ++++++
.../resources/features/Loan-Part4.feature | 2845 ++++++
.../src/test/resources/features/Loan.feature | 9096 -----------------
.../resources/features/LoanCharge.feature | 60 +
.../features/LoanContractTermination.feature | 71 +
.../features/LoanInterestRateChange.feature | 553 +
.../resources/features/LoanReAging.feature | 51 +-
.../LoanReAgingEqualAmortization.feature | 352 +-
.../features/LoanReAgingPreview.feature | 340 +
.../features/LoanReAmortization.feature | 74 +-
.../LoanReAmortizationAccruals.feature | 1928 +++-
.../resources/features/LoanRepayment.feature | 174 +
.../test/resources/features/Reporting.feature | 279 +
.../WorkingCapitalLoanProduct.feature | 135 +
.../features/WorkingCapital_COB.feature | 218 +
.../api/ExternalAssetOwnersApiResource.java | 2 +
.../api/LoanOriginatorApiResource.java | 10 +-
.../LoanChargeDataV1OriginatorEnricher.java | 72 +
.../cob/loan/ContextAwareTaskDecorator.java | 0
.../cob/service/RetrieveLoanIdService.java | 23 +
.../loanaccount/domain/LoanCharge.java | 12 +
.../LoanRepaymentScheduleInstallment.java | 10 +
.../guarantor/domain/GuarantorType.java | 7 +-
.../loanschedule/domain/AprCalculator.java | 28 +-
.../domain/LoanApplicationTerms.java | 276 +-
.../data/LoanRescheduleRequestData.java | 72 +-
.../LoanRescheduleRequestStatusEnumData.java | 16 +-
.../LoanRescheduleRequestTimelineData.java | 2 +
.../LoanRescheduleModelRepaymentPeriod.java | 2 +
.../domain/LoanRescheduleRequest.java | 90 +-
.../service/LoanBalanceService.java | 27 +
.../api/LoanProductsDetailsApiResource.java | 62 +
.../data/LoanConfigurationDetails.java | 7 +-
.../data/LoanProductBasicDetailsData.java | 36 +
.../domain/ILoanConfigurationDetails.java | 3 +
.../domain/LoanProductRepository.java | 4 +
.../mapper/LoanProductBasicDetailsMapper.java | 45 +
.../LoanProductReadBasicDetailsService.java | 28 +
...oanProductReadBasicDetailsServiceImpl.java | 43 +
.../LoanProductReadPlatformService.java | 1 +
.../domain/AprCalculatorTest.java | 334 +
fineract-mix/build.gradle | 75 +
fineract-mix/dependencies.gradle | 70 +
.../mix/api/MixReportApiResource.java | 15 +-
.../mix/api/MixTaxonomyApiResource.java | 10 +-
.../api/MixTaxonomyMappingApiResource.java | 40 +-
.../MixTaxonomyMappingUpdateCommand.java | 28 +
.../mix/data/MixReportXBRLContextData.java | 42 +
.../fineract/mix/data/MixReportXBRLData.java | 15 +-
.../mix/data/MixReportXBRLNamespaceData.java | 13 +-
.../fineract/mix/data/MixTaxonomyData.java | 16 +-
.../mix/data/MixTaxonomyMappingData.java | 7 +-
.../data/MixTaxonomyMappingUpdateRequest.java | 41 +
.../MixTaxonomyMappingUpdateResponse.java | 7 +-
.../mix/domain/MixReportXBRLNamespace.java | 48 +
.../MixReportXBRLNamespaceRepository.java | 29 +
.../fineract/mix/domain/MixTaxonomy.java | 63 +
.../mix/domain/MixTaxonomyMapping.java | 53 +
.../domain/MixTaxonomyMappingRepository.java | 8 +-
.../mix/domain/MixTaxonomyRepository.java | 28 +
.../MixReportXBRLMappingInvalidException.java | 4 +-
...ixTaxonomyMappingUpdateCommandHandler.java | 51 +
.../mapping/MixReportXBRLNamespaceMapper.java | 30 +
.../mix/mapping/MixTaxonomyMapper.java | 32 +
.../mix/mapping/MixTaxonomyMappingMapper.java | 30 +
...MixTaxonomyMappingUpdateRequestMapper.java | 30 +
.../mix/service/MixReportXBRLBuilder.java | 47 +-
.../MixReportXBRLNamespaceReadService.java | 8 +-
...MixReportXBRLNamespaceReadServiceImpl.java | 40 +
.../service/MixReportXBRLResultService.java | 6 +-
.../MixReportXBRLResultServiceImpl.java | 94 +-
.../MixTaxonomyMappingReadService.java | 2 +-
.../MixTaxonomyMappingReadServiceImpl.java | 40 +
.../MixTaxonomyMappingWriteService.java | 8 +-
.../MixTaxonomyMappingWriteServiceImpl.java | 48 +
.../mix/service/MixTaxonomyReadService.java | 2 +-
.../service/MixTaxonomyReadServiceImpl.java | 46 +
...edPaymentScheduleTransactionProcessor.java | 117 +-
.../LoanConfigurationDetailsMapper.java | 2 +-
.../calc/ProgressiveEMICalculator.java | 100 +-
.../calc/data/OverdueBalanceCorrection.java | 36 +
.../ProgressiveLoanInterestScheduleModel.java | 47 +-
.../calc/data/RepaymentPeriod.java | 28 +-
fineract-provider/build.gradle | 32 +-
fineract-provider/dependencies.gradle | 3 +-
.../api/JournalEntriesApiResource.java | 2 +-
.../JournalEntryReadPlatformServiceImpl.java | 4 +-
...teSavingsAccountChargeCommandStrategy.java | 71 +
.../cob/api/COBCatchUpExecutorHelper.java | 42 +
.../cob/api/InternalCOBApiResource.java | 6 +-
.../cob/api/LoanCOBCatchUpApiResource.java | 24 +-
...rkingCapitalLoanCOBCatchUpApiResource.java | 80 +
.../CustomLoanAccountLockRepositoryImpl.java | 3 +-
.../fineract/cob/domain/LoanAccountLock.java | 49 +-
.../cob/domain/LoanAccountLockRepository.java | 23 +-
.../ChunkProcessingLoanItemListener.java | 13 +-
.../listener/InlineCOBLoanItemListener.java | 10 +-
...apitalChunkProcessingLoanItemListener.java | 46 +
.../cob/loan/AbstractLoanItemProcessor.java | 61 +-
.../cob/loan/AbstractLoanItemReader.java | 14 +-
.../cob/loan/AbstractLoanItemWriter.java | 4 +-
.../cob/loan/ApplyLoanLockTasklet.java | 92 +-
.../cob/loan/InlineCOBLoanItemReader.java | 5 +-
.../cob/loan/InlineCOBLoanItemWriter.java | 4 +-
...neLoanCOBBuildExecutionContextTasklet.java | 33 +-
.../fineract/cob/loan/LoanCOBConstant.java | 3 -
.../cob/loan/LoanCOBManagerConfiguration.java | 10 +-
.../fineract/cob/loan/LoanCOBPartitioner.java | 81 +-
.../cob/loan/LoanCOBWorkerConfiguration.java | 12 +-
.../cob/loan/LoanInlineCOBConfig.java | 12 +-
.../fineract/cob/loan/LoanItemReader.java | 48 +-
.../fineract/cob/loan/LoanItemWriter.java | 4 +-
.../cob/loan/LoanLockingConfiguration.java | 4 +-
.../cob/loan/LoanLockingServiceImpl.java | 67 +-
...=> RetrieveAllNonClosedIdServiceImpl.java} | 12 +-
.../cob/loan/RetrieveLoanIdConfiguration.java | 3 +-
.../cob/loan/StayedLockedLoansTasklet.java | 5 +-
...WorkingCapitalInlineCOBLoanItemReader.java | 44 +
.../WorkingCapitalLoanInlineCOBConfig.java | 147 +
.../cob/service/AsyncCOBExecutorService.java | 26 +
.../AsyncCommonCOBExecutorService.java | 111 +
.../service/AsyncLoanCOBExecutorService.java | 7 +-
.../AsyncLoanCOBExecutorServiceImpl.java | 81 +-
...cWorkingCapitalLoanCOBExecutorService.java | 21 +
...kingCapitalLoanCOBExecutorServiceImpl.java | 61 +
.../cob/service/COBCatchUpService.java | 33 +
.../cob/service/CommonCOBCatchUpService.java | 76 +
...nlineCommonLockableCOBExecutorService.java | 256 +
.../InlineLoanCOBExecutorServiceImpl.java | 224 +-
...kingCapitalLoanCOBExecutorServiceImpl.java | 55 +
.../cob/service/LoanAccountLockService.java | 18 +-
.../cob/service/LoanCOBCatchUpService.java | 15 +-
.../service/LoanCOBCatchUpServiceImpl.java | 51 +-
.../WorkingCapitalLoanCOBCatchUpService.java | 22 +
...rkingCapitalLoanCOBCatchUpServiceImpl.java | 45 +
...ntNumberFormatReadPlatformServiceImpl.java | 16 +-
.../TemplatePopulateImportConstants.java | 1 +
.../guarantor/GuarantorImportHandler.java | 2 +
.../guarantor/GuarantorWorkbookPopulator.java | 5 +-
.../BulkImportWorkbookServiceImpl.java | 13 +-
.../EmailCampaignReadPlatformServiceImpl.java | 108 +-
...lConfigurationReadPlatformServiceImpl.java | 18 +-
.../service/EmailReadPlatformServiceImpl.java | 36 +-
.../sms/domain/SmsCampaignRepository.java | 4 +
...SmsCampaignNameAlreadyExistsException.java | 28 +
.../sms/mapper/SmsCampaignMapper.java | 55 +-
...msCampaignWritePlatformServiceJpaImpl.java | 50 +-
.../codes/api/CodeValuesApiResource.java | 8 +-
.../api/GlobalConfigurationApiResource.java | 6 +-
.../InternalConfigurationsApiResource.java | 2 +
.../async/SpringAsyncConfig.java | 7 +
.../domain/ConfigurationDomainServiceJpa.java | 15 +
.../core/config/SecurityConfig.java | 41 +-
.../core/config/SpringConfig.java | 58 +-
.../core/config/TaskExecutorConstant.java | 1 +
.../CreditBureauIntegrationApiResource.java | 2 +
.../DatatableRejectionCleanupService.java | 55 +
.../service/DatatableWriteServiceImpl.java | 2 +-
.../FineractEntityAccessReadServiceImpl.java | 132 +-
...gsAccountForceWithdrawalBusinessEvent.java | 35 +
.../mapper/loan/LoanChargeDataMapper.java | 1 +
.../jobs/api/SchedulerJobApiResource.java | 13 +-
.../jobs/filter/COBApiFilter.java | 95 +
.../jobs/filter/COBFilterApiMatcher.java | 79 +
.../jobs/filter/COBFilterHelper.java | 36 +
.../jobs/filter/LoanCOBApiFilter.java | 73 +-
.../jobs/filter/LoanCOBFilterHelper.java | 264 +-
.../jobs/filter/LoanCOBFilterHelperImpl.java | 242 +
.../ProgressiveLoanModelCheckerHelper.java | 67 +-
.../WorkingCapitalLoanCOBApiFilter.java | 31 +
.../WorkingCapitalLoanCOBFilterHelper.java | 22 +
...WorkingCapitalLoanCOBFilterHelperImpl.java | 209 +
.../handler/ExecuteJobCommandHandler.java | 46 +
.../jobs/service/InlineJobType.java | 4 +-
.../AbstractJobParameterProvider.java | 6 +-
.../LoanCOBJobParameterProvider.java | 5 +-
.../service/PlatformUserDetailsChecker.java | 39 +
.../service/SmsReadPlatformServiceImpl.java | 34 +-
.../service/InteropServiceImpl.java | 4 +-
.../starter/InteroperationConfiguration.java | 7 +-
.../apache/fineract/mix/data/ContextData.java | 79 -
.../mix/domain/MixTaxonomyMapping.java | 70 -
...axonomyMappingReadPlatformServiceImpl.java | 60 -
...xonomyMappingWritePlatformServiceImpl.java | 55 -
.../MixTaxonomyReadPlatformServiceImpl.java | 71 -
.../NamespaceReadPlatformServiceImpl.java | 66 -
.../mix/starter/MixConfiguration.java | 62 -
...ioningCriteriaReadPlatformServiceImpl.java | 16 +-
.../staff/api/StaffApiResource.java | 15 +-
.../api/WorkingDaysApiResource.java | 42 +-
.../command/WorkingDaysUpdateCommand.java | 28 +
.../workingdays/data/WorkingDayValidator.java | 1 +
.../workingdays/data/WorkingDaysData.java | 36 +-
.../data/WorkingDaysUpdateRequest.java | 47 +
.../WorkingDaysUpdateRequestValidator.java | 62 +
.../data/WorkingDaysUpdateResponse.java | 43 +
.../UpdateWorkingDaysCommandHandler.java | 41 +-
.../WorkingDaysReadPlatformServiceImpl.java | 12 +-
.../WorkingDaysWritePlatformService.java | 8 +-
...WritePlatformServiceJpaRepositoryImpl.java | 53 +-
.../OrganisationWorkingDaysConfiguration.java | 6 +-
.../api/AccountTransfersApiResource.java | 2 +-
.../StandingInstructionDataValidator.java | 10 +-
.../ExecuteStandingInstructionsTasklet.java | 3 +-
.../mapper/AccountTransfersMapper.java | 80 +-
...countTransfersReadPlatformServiceImpl.java | 23 +-
...ountTransfersWritePlatformServiceImpl.java | 18 +-
...ructionHistoryReadPlatformServiceImpl.java | 4 +-
...ingInstructionReadPlatformServiceImpl.java | 4 +-
...ngInstructionWritePlatformServiceImpl.java | 6 +-
.../api/ClientTransactionsApiResource.java | 10 +-
.../client/api/ClientsApiResource.java | 13 +-
.../api/LoanChargesApiResource.java | 36 +-
.../api/LoanTransactionsApiResource.java | 20 +-
.../loanaccount/api/LoansApiResource.java | 19 +-
.../api/LoansApiResourceSwagger.java | 6 +-
.../guarantor/data/GuarantorData.java | 4 +
.../guarantor/domain/Guarantor.java | 39 +-
.../service/GuarantorDomainServiceImpl.java | 2 +-
...ritePlatformServiceJpaRepositoryIImpl.java | 22 +-
.../LoanDisbursementValidator.java | 20 +-
.../LoanTransactionValidatorImpl.java | 11 +-
.../service/LoanDisbursementService.java | 2 +-
.../service/LoanReadPlatformServiceImpl.java | 71 +-
...WritePlatformServiceJpaRepositoryImpl.java | 4 +-
.../api/LoanProductsApiResource.java | 8 +-
...ountOnHoldFundTransactionsApiResource.java | 2 +
...DepositAccountTransactionsApiResource.java | 3 +
.../api/FixedDepositAccountsApiResource.java | 12 +-
.../api/FixedDepositProductsApiResource.java | 11 +-
...DepositAccountTransactionsApiResource.java | 6 +-
.../RecurringDepositAccountsApiResource.java | 15 +-
.../RecurringDepositProductsApiResource.java | 17 +-
.../api/SavingsAccountChargesApiResource.java | 2 +-
...SavingsAccountTransactionsApiResource.java | 82 +-
.../api/SavingsAccountsApiResource.java | 229 +-
.../api/SavingsProductsApiResource.java | 17 +-
.../data/DepositProductDataValidator.java | 4 +
.../domain/DepositAccountAssembler.java | 12 +-
.../savings/domain/FixedDepositAccount.java | 5 +-
.../domain/SavingsAccountAssembler.java | 10 +-
.../SavingsAccountDomainServiceJpa.java | 9 +-
...thdrawalSavingsAccountCommandHandler.java} | 17 +-
...SavingsAccountReadPlatformServiceImpl.java | 5 +-
...WritePlatformServiceJpaRepositoryImpl.java | 63 +
.../SearchReadPlatformServiceImpl.java | 19 +-
.../SelfAccountTransferReadServiceImpl.java | 27 +-
.../api/SelfSavingsProductsApiResource.java | 3 +
.../savings/api/SelfSavingsApiResource.java | 11 +-
.../api/SelfAuthenticationApiResource.java | 5 +-
.../self/spm/api/SelfSpmApiResource.java | 2 +
.../ShareAccountDataSerializer.java | 54 +-
.../ShareAccountReadPlatformServiceImpl.java | 114 +-
.../service/SharesEnumerations.java | 9 +
.../fineract/spm/api/SpmApiResource.java | 2 +-
.../ScorecardReadPlatformServiceImpl.java | 32 +-
.../api/UsersApiResource.java | 18 +-
...WritePlatformServiceJpaRepositoryImpl.java | 17 +-
.../src/main/resources/application.properties | 10 +-
.../tenant-store/changelog-tenant-store.xml | 1 +
...tandardize_character_set_and_collation.xml | 28 +
.../db/changelog/tenant/changelog-tenant.xml | 7 +
.../tenant/parts/0002_initial_data.xml | 3478 ++-----
.../0003_postgresql_specific_initial_data.xml | 582 +-
.../0212_add_force_password_reset_config.xml | 42 +
...transaction_summary_adding_originators.xml | 2649 +++++
...ial_balance_summary_adding_originators.xml | 439 +
..._summary_reports_add_buydown_fee_types.xml | 3263 ++++++
...dd_unique_constraint_sms_campaign_name.xml | 29 +
.../parts/0217_force_withdrawal_configs.xml | 74 +
...tandardize_character_set_and_collation.xml | 6 +-
.../module/fineract-provider/persistence.xml | 13 +-
.../command/CommandStrategyProviderTest.java | 3 +
...vingsAccountChargeCommandStrategyTest.java | 116 +
.../LoanItemListenerStepDefinitions.java | 12 +-
.../ApplyLoanLockTaskletStepDefinitions.java | 19 +-
.../cob/loan/LoanCOBPartitionerTest.java | 24 +-
.../loan/LoanItemReaderStepDefinitions.java | 18 +-
.../fineract/cob/loan/LoanItemReaderTest.java | 28 +-
.../loan/LoanItemWriterStepDefinitions.java | 3 +-
...ieveAllNonClosedLoanIdServiceImplTest.java | 3 +-
.../InlineLoanCOBExecutorServiceImplTest.java | 11 +-
.../core/config/SpringConfigTest.java | 316 +
.../DatatableWriteServiceImplTest.java | 53 +
...entConfigurationValidationServiceTest.java | 4 +-
.../jobs/filter/LoanCOBApiFilterTest.java | 22 +-
.../jobs/filter/LoanCOBFilterHelperTest.java | 6 +-
.../handler/ExecuteJobCommandHandlerTest.java | 64 +
.../report/MixXbrlBuilderStepDefinitions.java | 16 +-
.../MixXbrlTaxonomyStepDefinitions.java | 6 +-
.../LoanAdjustmentServiceImplTest.java | 4 +-
.../resources/application-test.properties | 2 +-
.../FloatingRatesReadPlatformServiceImpl.java | 104 +-
.../domain/InterestRateChart.java | 3 -
.../domain/InterestRateChartFields.java | 17 +-
.../domain/InterestRateChartSlabFields.java | 5 -
.../SavingsTransactionBooleanValues.java | 18 +
.../savings/domain/DepositTermDetail.java | 4 +-
.../savings/domain/SavingsAccount.java | 51 +-
.../SavingsAccountWritePlatformService.java | 5 +-
.../InterestRateChartValidationTest.java | 22 +
.../api/AuthenticationApiResource.java | 3 +
.../PasswordResetRequiredException.java | 43 +
.../PasswordResetRequiredExceptionMapper.java | 57 +
...SpringSecurityPlatformSecurityContext.java | 12 +-
fineract-war/build.gradle | 1 +
.../dependencies.gradle | 48 +
...gCapitalLoanAccountLockRepositoryImpl.java | 56 +
.../WorkingCapitalAccountLockRepository.java | 27 +
.../domain/WorkingCapitalLoanAccountLock.java | 38 +
...kingCapitalLoanCOBWorkerItemProcessor.java | 36 +
...WorkingCapitalLoanCOBWorkerItemWriter.java | 55 +
.../ApplyWorkingCapitalLoanLockTasklet.java | 48 +
...rkingCapitalLoanCOBWorkerItemListener.java | 40 +
...WorkingCapitalLoanCOBWorkerItemWriter.java | 41 +
.../WorkingCapitalAccountLockServiceImpl.java | 34 +
.../WorkingCapitalLoanCOBConstant.java | 45 +
...COBCustomJobParametersResolverTasklet.java | 42 +
...ingCapitalLoanCOBManagerConfiguration.java | 108 +
.../WorkingCapitalLoanCOBPartitioner.java | 61 +
...kingCapitalLoanCOBWorkerConfiguration.java | 146 +
...rkingCapitalLoanCOBWorkerItemListener.java | 40 +
...kingCapitalLoanCOBWorkerItemProcessor.java | 36 +
...WorkingCapitalLoanCOBWorkerItemReader.java | 68 +
...WorkingCapitalLoanCOBWorkerItemWriter.java | 41 +
...pitalLoanInlineCOBWorkerItemProcessor.java | 36 +
...orkingCapitalLoanLockingConfiguration.java | 44 +
.../WorkingCapitalLoanLockingServiceImpl.java | 53 +
...ingCapitalLoanRetrieveIdConfiguration.java | 40 +
.../WorkingCapitalLoanRetrieveIdService.java | 23 +
...rkingCapitalLoanRetrieveIdServiceImpl.java | 112 +
.../businessstep/DummyBusinessStep.java | 46 +
.../WorkingCapitalLoanCOBBusinessStep.java | 24 +
.../WorkingCapitalLoanProductConstants.java | 69 +
.../WorkingCapitalLoanProductApiResource.java | 249 +
...gCapitalLoanProductApiResourceSwagger.java | 457 +
...LoanProductConfigurableAttributesData.java | 43 +
.../data/WorkingCapitalLoanProductData.java | 101 +
.../WorkingCapitalPaymentAllocationData.java | 44 +
...lAdvancedPaymentAllocationsJsonParser.java | 123 +
...alAdvancedPaymentAllocationsValidator.java | 109 +
.../WorkingCapitalAmortizationType.java | 63 +
.../domain/WorkingCapitalLoan.java | 45 +-
...WorkingCapitalLoanPeriodFrequencyType.java | 63 +
.../domain/WorkingCapitalLoanProduct.java | 119 +-
...italLoanProductConfigurableAttributes.java | 61 +
...ngCapitalLoanProductMinMaxConstraints.java | 53 +
...pitalLoanProductPaymentAllocationRule.java | 59 +
...orkingCapitalLoanProductRelatedDetail.java | 69 +
.../WorkingCapitalPaymentAllocationType.java | 39 +
...talPaymentAllocationTypeListConverter.java | 42 +
...anProductDuplicateExternalIdException.java | 32 +
...italLoanProductDuplicateNameException.java | 31 +
...oanProductDuplicateShortNameException.java | 32 +
...ngCapitalLoanProductNotFoundException.java | 37 +
...rkingCapitalLoanProductCommandHandler.java | 42 +
...rkingCapitalLoanProductCommandHandler.java | 42 +
...rkingCapitalLoanProductCommandHandler.java | 42 +
...gCapitalLoanProductBasicDetailsMapper.java | 45 +
.../WorkingCapitalLoanProductMapper.java | 140 +
...roductPaymentAllocationRuleRepository.java | 29 +
.../WorkingCapitalLoanProductRepository.java | 76 +
.../WorkingCapitalLoanRepository.java | 67 +
...orkingCapitalLoanProductDataValidator.java | 537 +
...oanProductReadBasicDetailsServiceImpl.java | 45 +
...CapitalLoanProductReadPlatformService.java | 35 +
...talLoanProductReadPlatformServiceImpl.java | 102 +
.../WorkingCapitalLoanProductUpdateUtil.java | 204 +
...apitalLoanProductWritePlatformService.java | 31 +
...alLoanProductWritePlatformServiceImpl.java | 397 +
.../module-changelog-master.xml | 5 +-
.../parts/0001_loan_product.xml | 233 +
.../parts/0002_wc_loan_schema.xml | 76 +
.../parts/0003_working_capital_loan_cob.xml | 89 +
...004_extend_working_capital_loan_entity.xml | 58 +
.../persistence.xml | 3 +
...ngCapitalLoanProductDataValidatorTest.java | 352 +
integration-tests/build.gradle | 2 +
integration-tests/dependencies.gradle | 2 +-
...ntAllocationLoanRepaymentScheduleTest.java | 10 +
.../AuditIntegrationTest.java | 18 +
.../BaseLoanIntegrationTest.java | 24 +-
.../ClientLoanIntegrationTest.java | 4 +-
.../CreditBureauConfigurationTest.java | 37 +-
.../integrationtests/CreditBureauTest.java | 88 +-
.../integrationtests/CurrenciesTest.java | 86 -
...FloatingRateInterestRecalculationTest.java | 227 +
.../GroupSavingsIntegrationTest.java | 266 +-
.../LoanProductBasicDetailsTest.java | 41 +
.../integrationtests/MakercheckerTest.java | 76 +
.../PasswordResetIntegrationTest.java | 126 +
.../SavingsAccountForceWithdrawalTest.java | 113 +
.../SavingsAccountsExternalIdTest.java | 27 +-
.../integrationtests/SavingsAccountsTest.java | 6 +-
.../integrationtests/SearchResourcesTest.java | 83 +
.../SmsCampaignIntegrationTest.java | 115 +
.../UserAdministrationTest.java | 14 +-
.../WorkingCapitalLoanProductCRUDTest.java | 486 +
...rkingCapitalLoanProductValidationTest.java | 785 ++
.../client/ClientSearchTest.java | 8 +-
.../integrationtests/client/ClientTest.java | 4 +-
.../integrationtests/client/DocumentTest.java | 2 +-
.../client/FeignDocumentTest.java | 2 +-
.../client/FeignImageTest.java | 2 +-
.../integrationtests/client/ImageTest.java | 2 +-
.../client/IntegrationTest.java | 23 +
.../integrationtests/client/ReportsTest.java | 4 +-
.../integrationtests/client/StaffTest.java | 4 +-
.../feign/helpers/FeignAccountHelper.java | 2 +-
.../feign/helpers/FeignClientHelper.java | 4 +-
.../helpers/FeignExternalEventHelper.java | 59 +
.../FeignGlobalConfigurationHelper.java | 2 +-
.../helpers/FeignJournalEntryHelper.java | 2 +-
.../helpers/FeignLoanOriginatorHelper.java | 18 +-
.../feign/helpers/FeignSchedulerHelper.java | 4 +-
.../helpers/InternalExternalEventsApi.java | 41 +
...FeignLoanChargeOriginatorEnricherTest.java | 196 +
.../FeignTrialBalanceSummaryReportTest.java | 12 +-
.../integrationtests/common/CenterDomain.java | 4 +-
.../integrationtests/common/ClientHelper.java | 17 +-
.../CreditBureauConfigurationHelper.java | 200 +-
.../common/CreditBureauIntegrationHelper.java | 53 +-
.../common/CurrenciesHelper.java | 106 -
.../common/CurrencyDomain.java | 158 -
.../common/ExternalAssetOwnerHelper.java | 8 +-
.../ExternalEventConfigurationHelper.java | 20 +-
.../common/GlobalConfigurationHelper.java | 28 +-
.../common/PaymentTypeHelper.java | 2 +-
.../integrationtests/common/SurveyHelper.java | 4 +-
.../integrationtests/common/Utils.java | 125 +-
.../common/accounting/AccountHelper.java | 6 +-
.../FinancialActivityAccountHelper.java | 6 +-
.../common/accounting/JournalEntryHelper.java | 2 +-
.../common/commands/MakercheckersHelper.java | 7 +
.../common/loans/LoanProductHelper.java | 12 +-
.../common/loans/LoanTransactionHelper.java | 273 +-
.../common/organisation/CampaignsHelper.java | 21 +-
.../common/savings/SavingsAccountHelper.java | 26 +-
.../SavingsTestLifecycleExtension.java | 4 +-
.../shares/ShareAccountIntegrationTests.java | 415 +
.../WorkingCapitalLoanProductHelper.java | 83 +
.../WorkingCapitalLoanProductTestBuilder.java | 350 +
.../guarantor/GuarantorHelper.java | 19 +
.../guarantor/GuarantorTestBuilder.java | 12 +-
.../reaging/LoanReAgingIntegrationTest.java | 247 +
.../LoanReAmortizationIntegrationTest.java | 94 +
.../AccountTransferWithdrawalFeeTest.java | 16 +-
.../base/BaseSavingsIntegrationTest.java | 18 +-
kubernetes/fineractmysql-deployment.yml | 4 +-
oauth2-tests/dependencies.gradle | 2 +-
renovate.json | 42 +-
settings.gradle | 1 +
twofactor-tests/dependencies.gradle | 2 +-
603 files changed, 42664 insertions(+), 17328 deletions(-)
create mode 100644 .github/workflows/pr-one-commit-per-user-check.yml
create mode 100644 .github/workflows/verify-api-backward-compatibility.yml
create mode 100644 config/docker/mariadb/conf.d/mariadb_compat.cnf
create mode 100644 docker-compose-mysql.yml
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java (96%)
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java (99%)
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/domain/LockOwner.java (100%)
rename fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java => fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java (71%)
rename fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java => fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java (85%)
rename fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java => fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java (90%)
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java (84%)
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java (93%)
rename {fineract-provider => fineract-cob}/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java (92%)
rename fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java => fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java (71%)
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java
rename fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java => fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java (85%)
create mode 100644 fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java
rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java (100%)
create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java (100%)
rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/fund/domain/FundRepository.java (100%)
rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/fund/exception/FundNotFoundException.java (100%)
rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/fund/service/FundReadPlatformService.java (100%)
create mode 100644 fineract-doc/src/docs/en/chapters/architecture/api-backward-compatibility.adoc
create mode 100644 fineract-doc/src/docs/en/chapters/features/loan-origination-details.adoc
create mode 100644 fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImplTest.java
create mode 100644 fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImplTest.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/WorkingCapitalLoanProduct.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/CurrencyStepDef.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/OfficeStepDef.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/WorkingCapitalLoanCobStepDef.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/reporting/ReportingStepDef.java
create mode 100644 fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WcpCobBusinessStepInitializerStep.java
create mode 100644 fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Currency.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Loan-Part1.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Loan-Part2.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Loan-Part3.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Loan-Part4.feature
delete mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/Reporting.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanProduct.feature
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature
create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanChargeDataV1OriginatorEnricher.java
rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/cob/loan/ContextAwareTaskDecorator.java (100%)
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/cob/service/RetrieveLoanIdService.java
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsDetailsApiResource.java
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductBasicDetailsData.java
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/LoanProductBasicDetailsMapper.java
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsService.java
create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsServiceImpl.java
create mode 100644 fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AprCalculatorTest.java
create mode 100644 fineract-mix/build.gradle
create mode 100644 fineract-mix/dependencies.gradle
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java (78%)
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java (79%)
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java (53%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/command/MixTaxonomyMappingUpdateCommand.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLContextData.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/data/XBRLData.java => fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLData.java (76%)
rename fineract-provider/src/main/java/org/apache/fineract/mix/data/NamespaceData.java => fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLNamespaceData.java (79%)
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java (78%)
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java (90%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateRequest.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyRequest.java => fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateResponse.java (89%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespace.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespaceRepository.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomy.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMapping.java
rename {fineract-provider => fineract-mix}/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java (79%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyRepository.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/exception/XBRLMappingInvalidException.java => fineract-mix/src/main/java/org/apache/fineract/mix/exception/MixReportXBRLMappingInvalidException.java (86%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/handler/MixTaxonomyMappingUpdateCommandHandler.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixReportXBRLNamespaceMapper.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMapper.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingMapper.java
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingUpdateRequestMapper.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLBuilder.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java (80%)
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/NamespaceReadPlatformService.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadService.java (80%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadServiceImpl.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultService.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultService.java (82%)
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultServiceImpl.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultServiceImpl.java (56%)
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadPlatformService.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadService.java (94%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadServiceImpl.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWritePlatformService.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteService.java (75%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteServiceImpl.java
rename fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadPlatformService.java => fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadService.java (95%)
create mode 100644 fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadServiceImpl.java
create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OverdueBalanceCorrection.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateSavingsAccountChargeCommandStrategy.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/api/COBCatchUpExecutorHelper.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/api/WorkingCapitalLoanCOBCatchUpApiResource.java
rename {fineract-cob => fineract-provider}/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java (98%)
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java
rename fineract-provider/src/main/java/org/apache/fineract/cob/loan/{RetrieveAllNonClosedLoanIdServiceImpl.java => RetrieveAllNonClosedIdServiceImpl.java} (93%)
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalInlineCOBLoanItemReader.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalLoanInlineCOBConfig.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCOBExecutorService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCommonCOBExecutorService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorServiceImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/COBCatchUpService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/CommonCOBCatchUpService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineCommonLockableCOBExecutorService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineWorkingCapitalLoanCOBExecutorServiceImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpServiceImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/exception/SmsCampaignNameAlreadyExistsException.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableRejectionCleanupService.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/savings/transaction/SavingsAccountForceWithdrawalBusinessEvent.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/COBApiFilter.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/COBFilterApiMatcher.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/COBFilterHelper.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelperImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/WorkingCapitalLoanCOBApiFilter.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/WorkingCapitalLoanCOBFilterHelper.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/WorkingCapitalLoanCOBFilterHelperImpl.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/handler/ExecuteJobCommandHandler.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/PlatformUserDetailsChecker.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/data/ContextData.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMapping.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadPlatformServiceImpl.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWritePlatformServiceImpl.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadPlatformServiceImpl.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/service/NamespaceReadPlatformServiceImpl.java
delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/mix/starter/MixConfiguration.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/command/WorkingDaysUpdateCommand.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/data/WorkingDaysUpdateRequest.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/data/WorkingDaysUpdateRequestValidator.java
create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/data/WorkingDaysUpdateResponse.java
rename fineract-provider/src/main/java/org/apache/fineract/{mix/handler/UpdateTaxonomyMappingCommandHandler.java => portfolio/savings/handler/ForceWithdrawalSavingsAccountCommandHandler.java} (68%)
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0011_standardize_character_set_and_collation.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0212_add_force_password_reset_config.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0213_transaction_summary_adding_originators.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0214_trial_balance_summary_adding_originators.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0215_transaction_summary_reports_add_buydown_fee_types.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0216_add_unique_constraint_sms_campaign_name.xml
create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0217_force_withdrawal_configs.xml
rename fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0001_initial_schema.xml => fineract-provider/src/main/resources/db/changelog/tenant/parts/0218_standardize_character_set_and_collation.xml (86%)
create mode 100644 fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/CreateSavingsAccountChargeCommandStrategyTest.java
create mode 100644 fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/SpringConfigTest.java
create mode 100644 fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/handler/ExecuteJobCommandHandlerTest.java
create mode 100644 fineract-security/src/main/java/org/apache/fineract/infrastructure/security/exception/PasswordResetRequiredException.java
create mode 100644 fineract-security/src/main/java/org/apache/fineract/infrastructure/security/exception/PasswordResetRequiredExceptionMapper.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/CustomWorkingCapitalLoanAccountLockRepositoryImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalAccountLockRepository.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/domain/WorkingCapitalLoanAccountLock.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/AbstractWorkingCapitalLoanCOBWorkerItemProcessor.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/AbstractWorkingCapitalLoanCOBWorkerItemWriter.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/ApplyWorkingCapitalLoanLockTasklet.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/InlineWorkingCapitalLoanCOBWorkerItemListener.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/InlineWorkingCapitalLoanCOBWorkerItemWriter.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalAccountLockServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBConstant.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBCustomJobParametersResolverTasklet.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBManagerConfiguration.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBPartitioner.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerConfiguration.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemListener.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemProcessor.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemReader.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanCOBWorkerItemWriter.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanInlineCOBWorkerItemProcessor.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingConfiguration.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanLockingServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdConfiguration.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdService.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/WorkingCapitalLoanRetrieveIdServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DummyBusinessStep.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/WorkingCapitalLoanCOBBusinessStep.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/WorkingCapitalLoanProductConstants.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResource.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/api/WorkingCapitalLoanProductApiResourceSwagger.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductConfigurableAttributesData.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalLoanProductData.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/data/WorkingCapitalPaymentAllocationData.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsJsonParser.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsValidator.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAmortizationType.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanPeriodFrequencyType.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductConfigurableAttributes.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductMinMaxConstraints.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductPaymentAllocationRule.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoanProductRelatedDetail.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationTypeListConverter.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/exception/WorkingCapitalLoanProductDuplicateExternalIdException.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/exception/WorkingCapitalLoanProductDuplicateNameException.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/exception/WorkingCapitalLoanProductDuplicateShortNameException.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/exception/WorkingCapitalLoanProductNotFoundException.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/handler/CreateWorkingCapitalLoanProductCommandHandler.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/handler/DeleteWorkingCapitalLoanProductCommandHandler.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/handler/UpdateWorkingCapitalLoanProductCommandHandler.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductBasicDetailsMapper.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/mapper/WorkingCapitalLoanProductMapper.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanProductPaymentAllocationRuleRepository.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanProductRepository.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/repository/WorkingCapitalLoanRepository.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidator.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductReadBasicDetailsServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductReadPlatformService.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductReadPlatformServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductUpdateUtil.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformService.java
create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/service/WorkingCapitalLoanProductWritePlatformServiceImpl.java
create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0001_loan_product.xml
create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0002_wc_loan_schema.xml
create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0003_working_capital_loan_cob.xml
create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0004_extend_working_capital_loan_entity.xml
create mode 100644 fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloanproduct/serialization/WorkingCapitalLoanProductDataValidatorTest.java
delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/CurrenciesTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/FloatingRateInterestRecalculationTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductBasicDetailsTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/PasswordResetIntegrationTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountForceWithdrawalTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/SmsCampaignIntegrationTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignExternalEventHelper.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/InternalExternalEventsApi.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanChargeOriginatorEnricherTest.java
delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrenciesHelper.java
delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrencyDomain.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductHelper.java
create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java
diff --git a/.asf.yaml b/.asf.yaml
index 0d7d6037d8b..8ec1b8347e3 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -15,3 +15,6 @@ github:
- savings
- social-impact
- tech4good
+notifications:
+ commits: commits@fineract.apache.org
+ pullrequests: issues@fineract.apache.org
diff --git a/.github/workflows/build-cucumber.yml b/.github/workflows/build-cucumber.yml
index 788e998d81f..fb063ffd1fe 100644
--- a/.github/workflows/build-cucumber.yml
+++ b/.github/workflows/build-cucumber.yml
@@ -27,19 +27,19 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Cache Gradle dependencies
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.gradle/caches
@@ -47,7 +47,7 @@ jobs:
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
@@ -87,7 +87,7 @@ jobs:
- name: Archive test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.task }}
path: |
@@ -98,7 +98,7 @@ jobs:
- name: Archive Progressive Loan JAR
if: matrix.job_type == 'progressive-loan' && always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: progressive-loan-jar
path: ${{ env.EMBEDDABLE_JAR_FILE }}
@@ -107,7 +107,7 @@ jobs:
- name: Archive server logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-logs-${{ matrix.task }}
path: '**/build/cargo/'
diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml
index 43e5717b2d7..18a6d6b6656 100644
--- a/.github/workflows/build-docker.yml
+++ b/.github/workflows/build-docker.yml
@@ -26,18 +26,18 @@ jobs:
IMAGE_NAME: fineract
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Build the image
run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber
diff --git a/.github/workflows/build-documentation.yml b/.github/workflows/build-documentation.yml
index cd67d21c115..8ff6efdc900 100644
--- a/.github/workflows/build-documentation.yml
+++ b/.github/workflows/build-documentation.yml
@@ -10,23 +10,23 @@ jobs:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Congfigure vega-cli
run: npm i -g vega-cli --unsafe
- name: Validate Gradle wrapper
- uses: gradle/actions/wrapper-validation@v5.0.1
+ uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Install additional software
run: |
sudo apt-get update
diff --git a/.github/workflows/build-e2e-tests.yml b/.github/workflows/build-e2e-tests.yml
index 0c89f02bb9c..0ecd0b9afad 100644
--- a/.github/workflows/build-e2e-tests.yml
+++ b/.github/workflows/build-e2e-tests.yml
@@ -33,18 +33,18 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Make scripts executable
run: chmod +x scripts/split-features.sh
@@ -146,7 +146,7 @@ jobs:
- name: Upload test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: allure-results-shard-${{ matrix.shard_index }}
path: |
@@ -159,7 +159,7 @@ jobs:
- name: Upload Allure Report
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: allure-report-shard-${{ matrix.shard_index }}
path: allure-report-shard-${{ matrix.shard_index }}
@@ -167,7 +167,7 @@ jobs:
- name: Upload logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: logs-shard-${{ matrix.shard_index }}
path: |
diff --git a/.github/workflows/build-mariadb.yml b/.github/workflows/build-mariadb.yml
index d1c638994c2..ee057286024 100644
--- a/.github/workflows/build-mariadb.yml
+++ b/.github/workflows/build-mariadb.yml
@@ -17,7 +17,7 @@ jobs:
services:
mariadb:
- image: mariadb:11.5.2
+ image: mariadb:12.2
ports:
- 3306:3306
env:
@@ -44,19 +44,19 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Cache Gradle dependencies
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.gradle/caches
@@ -64,16 +64,21 @@ jobs:
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
- name: Verify MariaDB connection
run: |
- while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do
+ while ! mysqladmin ping -h"127.0.0.1" -P3306 -uroot -pmysql --silent; do
sleep 1
done
+ - name: Configure MariaDB compatibility defaults
+ run: |
+ mysql -h127.0.0.1 -P3306 -uroot -pmysql -e "SET GLOBAL innodb_snapshot_isolation=OFF;"
+ mysql -h127.0.0.1 -P3306 -uroot -pmysql -e "SHOW VARIABLES LIKE 'innodb_snapshot_isolation';"
+
- name: Initialise databases
run: |
./gradlew --no-daemon -q createDB -PdbName=fineract_tenants
@@ -135,7 +140,7 @@ jobs:
- name: Archive test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.task }}
path: '**/build/reports/'
@@ -143,7 +148,7 @@ jobs:
- name: Archive server logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-logs-${{ matrix.task }}
path: '**/build/cargo/'
diff --git a/.github/workflows/build-mysql.yml b/.github/workflows/build-mysql.yml
index f5e00117de4..065b33ab1a3 100644
--- a/.github/workflows/build-mysql.yml
+++ b/.github/workflows/build-mysql.yml
@@ -44,19 +44,19 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Cache Gradle dependencies
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.gradle/caches
@@ -64,7 +64,7 @@ jobs:
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
@@ -135,7 +135,7 @@ jobs:
- name: Archive test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.task }}
path: '**/build/reports/'
@@ -143,7 +143,7 @@ jobs:
- name: Archive server logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-logs-${{ matrix.task }}
path: '**/build/cargo/'
diff --git a/.github/workflows/build-postgresql.yml b/.github/workflows/build-postgresql.yml
index 378bd181e43..78a333e8c41 100644
--- a/.github/workflows/build-postgresql.yml
+++ b/.github/workflows/build-postgresql.yml
@@ -17,7 +17,7 @@ jobs:
services:
postgresql:
- image: postgres:17.4
+ image: postgres:18.3
ports:
- 5432:5432
env:
@@ -45,19 +45,19 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Cache Gradle dependencies
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.gradle/caches
@@ -65,7 +65,7 @@ jobs:
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
@@ -136,7 +136,7 @@ jobs:
- name: Archive test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results-${{ matrix.task }}
path: '**/build/reports/'
@@ -144,7 +144,7 @@ jobs:
- name: Archive server logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-logs-${{ matrix.task }}
path: '**/build/cargo/'
diff --git a/.github/workflows/liquibase-only-postgresql.yml b/.github/workflows/liquibase-only-postgresql.yml
index a675141557d..d1215876cad 100644
--- a/.github/workflows/liquibase-only-postgresql.yml
+++ b/.github/workflows/liquibase-only-postgresql.yml
@@ -12,7 +12,7 @@ jobs:
services:
postgresql:
- image: postgres:17.4
+ image: postgres:18.3
ports:
- 5432:5432
env:
@@ -26,19 +26,19 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Cache Gradle dependencies
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
~/.gradle/caches
@@ -46,7 +46,7 @@ jobs:
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
diff --git a/.github/workflows/mifos-fineract-client-publish.yml b/.github/workflows/mifos-fineract-client-publish.yml
index 1394c8b089c..1e4bca410ed 100644
--- a/.github/workflows/mifos-fineract-client-publish.yml
+++ b/.github/workflows/mifos-fineract-client-publish.yml
@@ -18,19 +18,19 @@ jobs:
steps:
- name: Checkout Source Code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Generate build number
- uses: onyxmueller/build-tag-number@v1
+ uses: onyxmueller/build-tag-number@4a0c81c9af350d967032d49204c83c38e6b0c8e4 # v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Build the image
run: ./gradlew publish -Pfineract.config.username=$ARTIFACTORY_USERNAME -Pfineract.config.password=$ARTIFACTORY_PASSWORD -Pfineract.release.version=${BUILD_NUMBER}
diff --git a/.github/workflows/pr-one-commit-per-user-check.yml b/.github/workflows/pr-one-commit-per-user-check.yml
new file mode 100644
index 00000000000..3025a9d75f9
--- /dev/null
+++ b/.github/workflows/pr-one-commit-per-user-check.yml
@@ -0,0 +1,57 @@
+name: Fineract PR One Commit Per User Check
+
+
+on:
+ pull_request:
+ types: [opened, reopened, synchronize]
+
+
+permissions:
+ pull-requests: write
+
+
+jobs:
+ verify-commits:
+ name: Validate One Commit Per User
+ runs-on: ubuntu-latest
+ timeout-minutes: 1
+ steps:
+ - name: Verify Commit Policy
+ id: check
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+
+ commits=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/commits") || { echo "::error::GitHub API request failed"; exit 1; }
+
+ if echo "$commits" | jq -e '.[] | select(.author == null)' > /dev/null; then
+ echo "null_authors=true" >> $GITHUB_OUTPUT
+ echo "::error::Some commits have a git email that is not linked to a GitHub account. Please ensure your git email matches one of your GitHub Account emails.\n\nPlease also squash your commits to prevent this message again."
+ exit 1
+ fi
+
+ user_ids=$(echo "$commits" | jq -r '.[] | select(.author.type != "Bot") | .author.id')
+ if echo "$user_ids" | sort | uniq -d | grep -q .; then
+ echo "multiple_commits=true" >> $GITHUB_OUTPUT
+ echo "::error::Multiple commits from the same author have been detected."
+ exit 1
+ fi
+
+ echo "Success: Each author has exactly one commit."
+
+ - name: Comment on PR
+ if: failure()
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+
+ if [ "${{ steps.check.outputs.null_authors }}" == "true" ]; then
+ gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body \
+ $'**One Commit Per User Check Failed**\n\nSome committers have a git email that does not match their GitHub account. Please ensure your git email matches one of your GitHub Account emails. Please also squash your commits to prevent this message again.'
+ fi
+
+
+ if [ "${{ steps.check.outputs.multiple_commits }}" == "true" ]; then
+ gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body \
+ $'**One Commit Per User Check Failed**\n\nEach user may only have one commit per PR. Please squash your commits.'
+ fi
diff --git a/.github/workflows/publish-dockerhub.yml b/.github/workflows/publish-dockerhub.yml
index c8776878633..e38c399bc68 100644
--- a/.github/workflows/publish-dockerhub.yml
+++ b/.github/workflows/publish-dockerhub.yml
@@ -15,18 +15,18 @@ jobs:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
steps:
- name: Checkout Source Code
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Get Git Hashes
run: |
diff --git a/.github/workflows/run-integration-test-sequentially-postgresql.yml b/.github/workflows/run-integration-test-sequentially-postgresql.yml
index 29afa1fcfcd..e409eabd259 100644
--- a/.github/workflows/run-integration-test-sequentially-postgresql.yml
+++ b/.github/workflows/run-integration-test-sequentially-postgresql.yml
@@ -14,7 +14,7 @@ jobs:
services:
postgresql:
- image: postgres:17.4
+ image: postgres:18.3
ports:
- 5432:5432
env:
@@ -33,18 +33,18 @@ jobs:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
- name: Verify PostgreSQL connection
@@ -84,7 +84,7 @@ jobs:
./gradlew --no-daemon --console=plain :oauth2-tests:test -PdbType=postgresql
- name: Archive test results
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results
retention-days: 5
@@ -95,7 +95,7 @@ jobs:
oauth2-tests/build/reports/
- name: Archive server logs
if: always()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-logs
retention-days: 5
diff --git a/.github/workflows/smoke-messaging.yml b/.github/workflows/smoke-messaging.yml
index 10a2e78711f..c74daad6c20 100644
--- a/.github/workflows/smoke-messaging.yml
+++ b/.github/workflows/smoke-messaging.yml
@@ -24,18 +24,18 @@ jobs:
IMAGE_NAME: fineract
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Build the image
run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 5394ba8eb95..c48ee8e94d5 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -20,16 +20,16 @@ jobs:
JAVA_BINARIES: .
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: '21'
distribution: 'zulu'
- name: Setup Gradle and Validate Wrapper
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
+ uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
validate-wrappers: true
- name: Build
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index d4691e97540..c92027ecb10 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
+ - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# stale-issue-message: 'Stale issue message'
diff --git a/.github/workflows/verify-api-backward-compatibility.yml b/.github/workflows/verify-api-backward-compatibility.yml
new file mode 100644
index 00000000000..7f3d71d04ab
--- /dev/null
+++ b/.github/workflows/verify-api-backward-compatibility.yml
@@ -0,0 +1,243 @@
+name: Verify API Backward Compatibility
+
+on: [pull_request]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ api-compatibility-check:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+
+ env:
+ TZ: Asia/Kolkata
+
+ steps:
+ - name: Checkout base branch
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: ${{ github.event.pull_request.base.repo.full_name }}
+ ref: ${{ github.event.pull_request.base.ref }}
+ fetch-depth: 0
+ path: baseline
+
+ - name: Checkout PR branch
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 0
+ path: current
+
+ - name: Compute merge-base commit
+ id: merge-base
+ run: |
+ cd baseline
+ # For fork PRs, fetch PR head from the local current/ checkout
+ git fetch "${GITHUB_WORKSPACE}/current" HEAD --no-tags 2>/dev/null || true
+ MERGE_BASE=$(git merge-base ${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.sha }})
+ echo "Merge-base commit: ${MERGE_BASE}"
+ echo "sha=${MERGE_BASE}" >> "$GITHUB_OUTPUT"
+ BASE_HEAD=$(git rev-parse ${{ github.event.pull_request.base.ref }})
+ if [ "${MERGE_BASE}" != "${BASE_HEAD}" ]; then
+ echo "::notice::PR is not rebased on latest ${{ github.event.pull_request.base.ref }}. Using merge-base ${MERGE_BASE} as baseline (branch HEAD: ${BASE_HEAD})."
+ fi
+
+ - name: Reset baseline to merge-base
+ working-directory: baseline
+ run: git checkout ${{ steps.merge-base.outputs.sha }}
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
+ with:
+ distribution: 'zulu'
+ java-version: '21'
+
+ - name: Generate baseline spec
+ working-directory: baseline
+ run: ./gradlew :fineract-provider:resolve --no-daemon
+
+ - name: Generate PR spec
+ working-directory: current
+ run: ./gradlew :fineract-provider:resolve --no-daemon
+
+ - name: Sanitize specs
+ run: |
+ python3 -c "
+ import json, sys
+
+ def sanitize(path):
+ with open(path) as f:
+ spec = json.load(f)
+ fixed = 0
+ for path_item in spec.get('paths', {}).values():
+ for op in path_item.values():
+ if not isinstance(op, dict) or 'requestBody' not in op:
+ continue
+ for media in op['requestBody'].get('content', {}).values():
+ if 'schema' not in media:
+ media['schema'] = {'type': 'object'}
+ fixed += 1
+ if fixed:
+ with open(path, 'w') as f:
+ json.dump(spec, f)
+ print(f'{path}: fixed {fixed} entries')
+
+ sanitize('${GITHUB_WORKSPACE}/baseline/fineract-provider/build/resources/main/static/fineract.json')
+ sanitize('${GITHUB_WORKSPACE}/current/fineract-provider/build/resources/main/static/fineract.json')
+ "
+
+ - name: Check breaking changes
+ id: breaking-check
+ continue-on-error: true
+ working-directory: current
+ run: |
+ set -o pipefail
+ ./gradlew :fineract-provider:checkBreakingChanges \
+ -PapiBaseline="${GITHUB_WORKSPACE}/baseline/fineract-provider/build/resources/main/static/fineract.json" \
+ --no-daemon --quiet 2>&1 | tail -50
+
+ - name: Build report
+ if: steps.breaking-check.outcome == 'failure'
+ id: report
+ run: |
+ REPORT_DIR="current/fineract-provider/build/swagger-brake"
+
+ python3 -c "
+ import json, glob, os
+ from collections import defaultdict
+
+ RULE_DESC = {
+ 'R001': 'Standard API changed to beta',
+ 'R002': 'Path deleted',
+ 'R003': 'Request media type deleted',
+ 'R004': 'Request parameter deleted',
+ 'R005': 'Request parameter enum value deleted',
+ 'R006': 'Request parameter location changed',
+ 'R007': 'Request parameter made required',
+ 'R008': 'Request parameter type changed',
+ 'R009': 'Request attribute removed',
+ 'R010': 'Request type changed',
+ 'R011': 'Request enum value deleted',
+ 'R012': 'Response code deleted',
+ 'R013': 'Response media type deleted',
+ 'R014': 'Response attribute removed',
+ 'R015': 'Response type changed',
+ 'R016': 'Response enum value deleted',
+ 'R017': 'Request parameter constraint changed',
+ }
+
+ report_dir = '${REPORT_DIR}'
+ files = sorted(glob.glob(os.path.join(report_dir, '*.json')))
+ if not files:
+ body = 'Breaking change detected but no report file found.'
+ else:
+ with open(files[0]) as f:
+ data = json.load(f)
+
+ all_changes = []
+ for items in data.get('breakingChanges', {}).values():
+ all_changes.extend(items)
+
+ if not all_changes:
+ body = 'Breaking change detected but no details available in report.'
+ else:
+ def detail(c):
+ for key in ('attributeName', 'attribute', 'name', 'mediaType', 'enumValue', 'code'):
+ v = c.get(key)
+ if v:
+ val = v.rsplit('.', 1)[-1]
+ if key in ('attributeName', 'attribute', 'name'):
+ return val
+ return f'{key}={val}'
+ return '-'
+
+ groups = defaultdict(list)
+ for c in all_changes:
+ groups[(c.get('ruleCode', '?'), detail(c))].append(c)
+
+ lines = []
+ lines.append('| Rule | Description | Detail | Affected endpoints | Count |')
+ lines.append('|------|-------------|--------|--------------------|-------|')
+ for (rule, det), items in sorted(groups.items()):
+ desc = RULE_DESC.get(rule, '')
+ eps = sorted(set(
+ f'{c.get(\"method\",\"\")} {c.get(\"path\",\"\")}'
+ for c in items if c.get('path')
+ ))
+ ep_str = ', '.join(f'\`{e}\`' for e in eps[:5])
+ if len(eps) > 5:
+ ep_str += f' +{len(eps)-5} more'
+ lines.append(f'| {rule} | {desc} | \`{det}\` | {ep_str} | {len(items)} |')
+
+ lines.append('')
+ lines.append(f'**Total: {len(all_changes)} violations across {len(groups)} unique changes**')
+ body = '\n'.join(lines)
+
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
+ f.write('has_report=true\n')
+
+ report_file = '${GITHUB_WORKSPACE}/breaking-changes-report.md'
+ with open(report_file, 'w') as f:
+ f.write('## Breaking API Changes Detected\n\n')
+ f.write(body)
+ f.write('\n\n> **Note:** This check is informational only and does not block the PR.\n')
+
+ # Also write to step summary
+ with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
+ f.write('## Breaking API Changes Detected\n\n')
+ f.write(body)
+ f.write('\n\n> **Note:** This check is informational only and does not block the PR.\n')
+ "
+
+ - name: Comment on PR
+ if: always()
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ MARKER=""
+
+ # Find existing comment by marker
+ COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
+ --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1)
+
+ if [ "${{ steps.breaking-check.outcome }}" == "failure" ] && [ -f "${GITHUB_WORKSPACE}/breaking-changes-report.md" ]; then
+ # Prepend marker to the report
+ BODY="${MARKER}
+ $(cat ${GITHUB_WORKSPACE}/breaking-changes-report.md)"
+
+ if [ -n "$COMMENT_ID" ]; then
+ gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
+ -X PATCH -f body="${BODY}"
+ else
+ gh pr comment "${PR_NUMBER}" --repo ${{ github.repository }} --body "${BODY}"
+ fi
+ elif [ -n "$COMMENT_ID" ]; then
+ # No breaking changes anymore, delete the old comment
+ gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" -X DELETE
+ fi
+
+ - name: Report no breaking changes
+ if: steps.breaking-check.outcome == 'success'
+ run: |
+ echo "## No Breaking API Changes Detected" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "The API contract is backward compatible." >> $GITHUB_STEP_SUMMARY
+
+ - name: Archive breaking change report
+ if: always()
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: api-compatibility-report
+ path: current/fineract-provider/build/swagger-brake/
+ retention-days: 30
+
+ - name: Fail if breaking changes detected
+ if: steps.breaking-check.outcome == 'failure'
+ run: |
+ echo "::error::Breaking API changes detected. See the report above for details."
+ exit 1
diff --git a/.github/workflows/verify-commits.yml b/.github/workflows/verify-commits.yml
index 69dfca6e10e..0efc73d081e 100644
--- a/.github/workflows/verify-commits.yml
+++ b/.github/workflows/verify-commits.yml
@@ -31,7 +31,7 @@ jobs:
timeout-minutes: 1
steps:
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -43,5 +43,5 @@ jobs:
chmod +x scripts/verify-signed-commits.sh
./scripts/verify-signed-commits.sh \
--base-ref origin/${{ github.base_ref }} \
- --head-ref ${{ github.sha }} \
+ --head-ref ${{ github.event.pull_request.head.sha }} \
--strict
diff --git a/.github/workflows/verify-liquibase-backward-compatibility.yml b/.github/workflows/verify-liquibase-backward-compatibility.yml
index 400c9f283ce..56d7e25824e 100644
--- a/.github/workflows/verify-liquibase-backward-compatibility.yml
+++ b/.github/workflows/verify-liquibase-backward-compatibility.yml
@@ -12,7 +12,7 @@ jobs:
services:
postgresql:
- image: postgres:17.4
+ image: postgres:18.3
ports:
- 5432:5432
env:
@@ -32,14 +32,14 @@ jobs:
steps:
- name: Checkout the base branch (`develop`)
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
- name: Set up JDK 21
- uses: actions/setup-java@v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'zulu'
java-version: '21'
@@ -107,7 +107,7 @@ jobs:
sleep 10
- name: Checkout the PR branch
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
diff --git a/README.md b/README.md
index ca7a95b6151..20b246cdd73 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ In the moment you get started writing code, please consult our [CONTRIBUTING](CO
REQUIREMENTS
============
* min. 16GB RAM and 8 core CPU
-* `MariaDB >= 11.5.2` or `PostgreSQL >= 17.0`
+* `MariaDB >= 12.2` or `PostgreSQL >= 18.0`
* `Java >= 21` (Azul Zulu JVM is tested by our CI on GitHub Actions)
Tomcat (min. v10) is only required, if you wish to deploy the Fineract WAR to a separate external servlet container. You do not need to install Tomcat to run Fineract. We recommend the use of the self-contained JAR, which transparently embeds a servlet container using Spring Boot.
@@ -293,11 +293,11 @@ DATABASE AND TABLES
You can run the required version of the database server in a container, instead of having to install it, like this:
- docker run --name mariadb-11.5 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:11.5.2
+ docker run --name mariadb-12.2 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:12.2.2 --innodb-snapshot-isolation=OFF
and stop and destroy it like this:
- docker rm -f mariadb-11.5
+ docker rm -f mariadb-12.2
Beware that this container database keeps its state inside the container and not on the host filesystem. It is lost when you destroy (rm) this container. This is typically fine for development. See [Caveats: Where to Store Data on the database container documentation](https://hub.docker.com/_/mariadb) regarding how to make it persistent instead of ephemeral.
diff --git a/build.gradle b/build.gradle
index aa541da18d1..e72d73b1163 100644
--- a/build.gradle
+++ b/build.gradle
@@ -42,6 +42,7 @@ buildscript {
'fineract-loan',
'fineract-savings',
'fineract-report',
+ 'fineract-mix',
'integration-tests',
'twofactor-tests',
'oauth2-tests',
@@ -75,6 +76,7 @@ buildscript {
'fineract-loan',
'fineract-savings',
'fineract-report',
+ 'fineract-mix',
'fineract-branch',
'fineract-document',
'fineract-progressive-loan',
@@ -121,9 +123,10 @@ plugins {
id 'se.thinkcode.cucumber-runner' version '0.0.11' apply false
id "com.github.davidmc24.gradle.plugin.avro-base" version "1.9.1" apply false
id 'org.openapi.generator' version '7.8.0' apply false
- id 'com.gradleup.shadow' version '8.3.5' apply false
+ id 'com.gradleup.shadow' version '9.3.2' apply false
id 'me.champeau.jmh' version '0.7.1' apply false
id 'org.cyclonedx.bom' version '3.1.0' apply false
+ id 'com.docktape.swagger-brake' version '2.7.0' apply false
}
apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.release.gradle"
diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index 271bf5d8a42..cfd2b590c11 100644
--- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
+++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
@@ -246,11 +246,11 @@ dependencyManagement {
dependency "org.apache.avro:avro:1.12.0"
- dependency ('org.mariadb.jdbc:mariadb-java-client:3.5.2') {
+ dependency ('org.mariadb.jdbc:mariadb-java-client:3.5.7') {
exclude 'org.slf4j:jcl-over-slf4j'
exclude 'org.slf4j:slf4j-api'
}
- dependency 'org.postgresql:postgresql:42.7.8'
+ dependency 'org.postgresql:postgresql:42.7.9'
dependency 'com.mysql:mysql-connector-j:9.3.0'
@@ -303,8 +303,8 @@ dependencyManagement {
// Force lz4-java version: CVE-2025-12183
dependency 'at.yawk.lz4:lz4-java:1.10.1'
// Force tomcat-embed-core version: CVE-2025-24813
- dependency 'org.apache.tomcat.embed:tomcat-embed-core:10.1.47'
- dependency 'org.apache.tomcat.embed:tomcat-embed-el:10.1.47'
- dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:10.1.47'
+ dependency 'org.apache.tomcat.embed:tomcat-embed-core:10.1.49'
+ dependency 'org.apache.tomcat.embed:tomcat-embed-el:10.1.49'
+ dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:10.1.49'
}
}
diff --git a/config/docker/compose/mariadb.yml b/config/docker/compose/mariadb.yml
index 6756974f784..3b772cf6039 100644
--- a/config/docker/compose/mariadb.yml
+++ b/config/docker/compose/mariadb.yml
@@ -18,9 +18,10 @@ version: "3.8"
services:
mariadb:
container_name: mariadb
- image: mariadb:11.4
+ image: mariadb:12.2
volumes:
- ${PWD}/config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
+ - ${PWD}/config/docker/mariadb/conf.d/mariadb_compat.cnf:/etc/mysql/conf.d/mariadb_compat.cnf:ro
- ${PWD}/config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
restart: always
env_file:
diff --git a/config/docker/compose/postgresql.yml b/config/docker/compose/postgresql.yml
index 878c88264fd..fecd9ca5c03 100644
--- a/config/docker/compose/postgresql.yml
+++ b/config/docker/compose/postgresql.yml
@@ -20,7 +20,7 @@ version: "3.8"
services:
postgresql:
- image: postgres:16.1
+ image: postgres:18.3
volumes:
- ${PWD}/config/docker/postgresql/docker-entrypoint-initdb.d/01-init.sh:/docker-entrypoint-initdb.d/01-init.sh:Z,ro
restart: always
diff --git a/config/docker/mariadb/conf.d/mariadb_compat.cnf b/config/docker/mariadb/conf.d/mariadb_compat.cnf
new file mode 100644
index 00000000000..a4293b2e8a1
--- /dev/null
+++ b/config/docker/mariadb/conf.d/mariadb_compat.cnf
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[mysqld]
+innodb_snapshot_isolation=OFF
diff --git a/docker-compose-mariadb.yml b/docker-compose-mariadb.yml
index 59f18160b9f..fa4d5eca3c3 100644
--- a/docker-compose-mariadb.yml
+++ b/docker-compose-mariadb.yml
@@ -19,9 +19,10 @@
services:
mariadb:
container_name: mariadb
- image: mariadb:11.4
+ image: mariadb:12.2
volumes:
- ./config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
+ - ./config/docker/mariadb/conf.d/mariadb_compat.cnf:/etc/mysql/conf.d/mariadb_compat.cnf:ro
- ./config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
restart: always
env_file:
diff --git a/docker-compose-mysql.yml b/docker-compose-mysql.yml
new file mode 100644
index 00000000000..5f0521724ad
--- /dev/null
+++ b/docker-compose-mysql.yml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+services:
+ mysql:
+ container_name: mysql
+ image: mysql:8
+ volumes:
+ - ${PWD}/config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro
+ - ${PWD}/config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro
+ restart: always
+ env_file:
+ - ${PWD}/config/docker/env/mysql.env
+ healthcheck:
+ test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
+ timeout: 10s
+ retries: 10
+ ports:
+ - "3306:3306"
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/closure/api/GLClosuresApiResourceSwagger.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/closure/api/GLClosuresApiResourceSwagger.java
index 8faa5050fa9..5e2f366961d 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/closure/api/GLClosuresApiResourceSwagger.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/closure/api/GLClosuresApiResourceSwagger.java
@@ -21,22 +21,20 @@
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
+/**
+ * Swagger response and request schemas for GL Closures.
+ */
final class GLClosuresApiResourceSwagger {
private GLClosuresApiResourceSwagger() {
// don't allow to instantiate; use only for live API documentation
}
- /**
- * TODO: describe where this belongs: {@link GLClosuresApiResource } {@link GLClosuresApiResource}
- */
- // Check !!
-
@Schema(description = "GetGLClosureResponse")
public static final class GetGlClosureResponse {
private GetGlClosureResponse() {
- // dont allow to initiatiate
+ // dont allow to instantiation
}
@Schema(example = "7")
@@ -45,13 +43,13 @@ private GetGlClosureResponse() {
public Long officeId;
@Schema(example = "Head Office")
public String officeName;
- @Schema(example = "2013,1,2")
+ @Schema(example = "2013-01-02")
public LocalDate closingDate;
@Schema(example = "false")
public boolean deleted;
- @Schema(example = "2013,1,3")
+ @Schema(example = "2013-1-3")
public LocalDate createdDate;
- @Schema(example = "2013,1,3")
+ @Schema(example = "2013-1-3")
public LocalDate lastUpdatedDate;
@Schema(example = "1")
public Long createdByUserId;
@@ -66,7 +64,7 @@ private GetGlClosureResponse() {
}
- @Schema(description = "PostGLCLosuresRequest")
+ @Schema(description = "PostGLClosuresRequest")
public static final class PostGlClosuresRequest {
private PostGlClosuresRequest() {
@@ -77,7 +75,7 @@ private PostGlClosuresRequest() {
public Long officeId;
@Schema(example = "06 December 2012")
public LocalDate closingDate;
- @Schema(example = "The accountants are heading for a carribean vacation")
+ @Schema(example = "The accountants are heading for a Caribbean vacation")
public String comments;
@Schema(example = "en")
public String locale;
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/financialactivityaccount/api/FinancialActivityAccountsApiResource.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/financialactivityaccount/api/FinancialActivityAccountsApiResource.java
index 41adc66c32f..889db5dd564 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/financialactivityaccount/api/FinancialActivityAccountsApiResource.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/financialactivityaccount/api/FinancialActivityAccountsApiResource.java
@@ -119,7 +119,7 @@ public FinancialActivityAccountData retreive(@PathParam("mappingId") @Parameter(
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "Create a new Financial Activity to Accounts Mapping", description = """
+ @Operation(summary = "Create a new Financial Activity to Accounts Mapping", operationId = "createGLAccountMappingFinancialActivityAccount", description = """
Mandatory Fields
financialActivityId, glAccountId""")
@RequestBody(content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PostFinancialActivityAccountsRequest.class)))
@@ -135,7 +135,7 @@ public CommandProcessingResult createGLAccount(@Parameter(hidden = true) Financi
@Path("{mappingId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "Update a Financial Activity to Account Mapping", description = "the API updates the Ledger account linked to a Financial Activity")
+ @Operation(summary = "Update a Financial Activity to Account Mapping", operationId = "updateGLAccountMappingFinancialActivityAccount", description = "the API updates the Ledger account linked to a Financial Activity")
@RequestBody(content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PostFinancialActivityAccountsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PutFinancialActivityAccountsResponse.class)))
public CommandProcessingResult updateGLAccount(@PathParam("mappingId") @Parameter(description = "mappingId") final Long mappingId,
@@ -150,7 +150,7 @@ public CommandProcessingResult updateGLAccount(@PathParam("mappingId") @Paramete
@Path("{mappingId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "Delete a Financial Activity to Account Mapping")
+ @Operation(summary = "Delete a Financial Activity to Account Mapping", operationId = "deleteGLAccountMappingFinancialActivityAccount")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.DeleteFinancialActivityAccountsResponse.class)))
public CommandProcessingResult deleteGLAccount(@PathParam("mappingId") @Parameter(description = "mappingId") final Long mappingId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteOfficeToGLAccountMapping(mappingId).build();
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResource.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResource.java
index 03ec4c66bba..474add0243c 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResource.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResource.java
@@ -181,11 +181,12 @@ public GLAccountData retreiveAccount(@PathParam("glAccountId") @Parameter(descri
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(tags = { "General Ledger Account" }, summary = "Create a General Ledger Account", description = """
- Note: You may optionally create Hierarchical Chart of Accounts by using the "parentId" property of an Account
- Mandatory Fields:
- name, glCode, type, usage and manualEntriesAllowed
- """)
+ @Operation(tags = {
+ "General Ledger Account" }, summary = "Create a General Ledger Account", operationId = "createGLAccount", description = """
+ Note: You may optionally create Hierarchical Chart of Accounts by using the "parentId" property of an Account
+ Mandatory Fields:
+ name, glCode, type, usage and manualEntriesAllowed
+ """)
@RequestBody(content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PostGLAccountsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PostGLAccountsResponse.class)))
public CommandProcessingResult createGLAccount(@Parameter(hidden = true) GLAccountCommand glAccountCommand) {
@@ -198,7 +199,8 @@ public CommandProcessingResult createGLAccount(@Parameter(hidden = true) GLAccou
@Path("{glAccountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(tags = { "General Ledger Account" }, summary = "Update a GL Account", description = "Updates a GL Account")
+ @Operation(tags = {
+ "General Ledger Account" }, summary = "Update a GL Account", operationId = "updateGLAccount", description = "Updates a GL Account")
@RequestBody(content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PutGLAccountsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PutGLAccountsResponse.class)))
public CommandProcessingResult updateGLAccount(@PathParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId,
@@ -212,7 +214,8 @@ public CommandProcessingResult updateGLAccount(@PathParam("glAccountId") @Parame
@Path("{glAccountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(tags = { "General Ledger Account" }, summary = "Delete a GL Account", description = "Deletes a GL Account")
+ @Operation(tags = {
+ "General Ledger Account" }, summary = "Delete a GL Account", operationId = "deleteGLAccount", description = "Deletes a GL Account")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.DeleteGLAccountsResponse.class)))
public CommandProcessingResult deleteGLAccount(
@PathParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId) {
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/provisioning/service/ProvisioningEntriesReadPlatformServiceImpl.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/provisioning/service/ProvisioningEntriesReadPlatformServiceImpl.java
index bd2bf8ad610..d34c559719a 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/provisioning/service/ProvisioningEntriesReadPlatformServiceImpl.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/provisioning/service/ProvisioningEntriesReadPlatformServiceImpl.java
@@ -114,11 +114,11 @@ public ProvisioningEntryData retrieveProvisioningEntryData(Long entryId) {
private static final class ProvisioningEntryDataMapper implements RowMapper {
- private final StringBuilder sqlQuery = new StringBuilder()
- .append(" entry.id, entry.journal_entry_created, entry.createdby_id, entry.created_date, created.username as createduser,")
- .append("entry.lastmodifiedby_id, modified.username as modifieduser, entry.lastmodified_date ")
- .append("from m_provisioning_history entry ").append("left JOIN m_appuser created ON created.id = entry.createdby_id ")
- .append("left JOIN m_appuser modified ON modified.id = entry.lastmodifiedby_id ");
+ private static final String PROVISIONING_ENTRY_SCHEMA = """
+ entry.id, entry.journal_entry_created, entry.createdby_id, entry.created_date, created.username as createduser,
+ entry.lastmodifiedby_id, modified.username as modifieduser, entry.lastmodified_date
+ from m_provisioning_history entry left JOIN m_appuser created ON created.id = entry.createdby_id
+ left JOIN m_appuser modified ON modified.id = entry.lastmodifiedby_id\s""";
@Override
@SuppressWarnings("unused")
@@ -138,22 +138,22 @@ public ProvisioningEntryData mapRow(ResultSet rs, int rowNum) throws SQLExceptio
}
public String getSchema() {
- return sqlQuery.toString();
+ return PROVISIONING_ENTRY_SCHEMA;
}
}
private static final class LoanProductProvisioningEntryRowMapper implements RowMapper {
- private final StringBuilder sqlQuery = new StringBuilder().append(
- " entry.id, entry.history_id as historyId, office_id, entry.criteria_id as criteriaid, office.name as officename, product.name as productname, entry.product_id, ")
- .append("category_id, category.category_name, liability.id as liabilityid, liability.gl_code as liabilitycode, liability.name as liabilityname, ")
- .append("expense.id as expenseid, expense.gl_code as expensecode, expense.name as expensename, entry.currency_code, entry.overdue_in_days, entry.reseve_amount from m_loanproduct_provisioning_entry entry ")
- .append("left join m_office office ON office.id = entry.office_id ")
- .append("left join m_product_loan product ON product.id = entry.product_id ")
- .append("left join m_provision_category category ON category.id = entry.category_id ")
- .append("left join acc_gl_account liability ON liability.id = entry.liability_account ")
- .append("left join acc_gl_account expense ON expense.id = entry.expense_account ");
+ private static final String LOAN_PRODUCT_PROVISIONING_ENTRY_SCHEMA = """
+ entry.id, entry.history_id as historyId, office_id, entry.criteria_id as criteriaid, office.name as officename, product.name as productname, entry.product_id,
+ category_id, category.category_name, liability.id as liabilityid, liability.gl_code as liabilitycode, liability.name as liabilityname,
+ expense.id as expenseid, expense.gl_code as expensecode, expense.name as expensename, entry.currency_code, entry.overdue_in_days, entry.reseve_amount from m_loanproduct_provisioning_entry entry
+ left join m_office office ON office.id = entry.office_id
+ left join m_product_loan product ON product.id = entry.product_id
+ left join m_provision_category category ON category.id = entry.category_id
+ left join acc_gl_account liability ON liability.id = entry.liability_account
+ left join acc_gl_account expense ON expense.id = entry.expense_account\s""";
@Override
@SuppressWarnings("unused")
@@ -185,19 +185,19 @@ public LoanProductProvisioningEntryData mapRow(ResultSet rs, int rowNum) throws
}
public String getSchema() {
- return sqlQuery.toString();
+ return LOAN_PRODUCT_PROVISIONING_ENTRY_SCHEMA;
}
}
private static final class ProvisioningEntryDataMapperWithSumReserved implements RowMapper {
- private final StringBuilder sqlQuery = new StringBuilder()
- .append(" entry.id, journal_entry_created, createdby_id, created_date, created.username as createduser,")
- .append("lastmodifiedby_id, modified.username as modifieduser, lastmodified_date, SUM(reserved.reseve_amount) as totalreserved ")
- .append("from m_provisioning_history entry ")
- .append("JOIN m_loanproduct_provisioning_entry reserved on entry.id = reserved.history_id ")
- .append("left JOIN m_appuser created ON created.id = entry.createdby_id ")
- .append("left JOIN m_appuser modified ON modified.id = entry.lastmodifiedby_id ");
+ private static final String PROVISIONING_ENTRY_SUM_RESERVED_SCHEMA = """
+ entry.id, journal_entry_created, createdby_id, created_date, created.username as createduser,
+ lastmodifiedby_id, modified.username as modifieduser, lastmodified_date, SUM(reserved.reseve_amount) as totalreserved
+ from m_provisioning_history entry
+ JOIN m_loanproduct_provisioning_entry reserved on entry.id = reserved.history_id
+ left JOIN m_appuser created ON created.id = entry.createdby_id
+ left JOIN m_appuser modified ON modified.id = entry.lastmodifiedby_id\s""";
@Override
@SuppressWarnings("unused")
@@ -217,7 +217,7 @@ public ProvisioningEntryData mapRow(ResultSet rs, int rowNum) throws SQLExceptio
}
public String getSchema() {
- return sqlQuery.toString();
+ return PROVISIONING_ENTRY_SUM_RESERVED_SCHEMA;
}
}
@@ -286,9 +286,10 @@ public ProvisioningEntryData retrieveExistingProvisioningIdDateWithJournals() {
private static final class ProvisioningEntryIdDateRowMapper implements RowMapper {
- StringBuilder buff = new StringBuilder().append("select history1.id, history1.created_date from m_provisioning_history history1 ")
- .append("where history1.created_date = (select max(history2.created_date) from m_provisioning_history history2 ")
- .append("where history2.journal_entry_created='1')");
+ private static final String PROVISIONING_ENTRY_ID_DATE_SCHEMA = """
+ select history1.id, history1.created_date from m_provisioning_history history1
+ where history1.created_date = (select max(history2.created_date) from m_provisioning_history history2
+ where history2.journal_entry_created='1')\s""";
@Override
public ProvisioningEntryData mapRow(ResultSet rs, int rowNum) throws SQLException {
@@ -306,7 +307,7 @@ public ProvisioningEntryData mapRow(ResultSet rs, int rowNum) throws SQLExceptio
}
public String schema() {
- return buff.toString();
+ return PROVISIONING_ENTRY_ID_DATE_SCHEMA;
}
}
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
index 9eaa4adbced..e4f8022e1e2 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
@@ -267,6 +267,18 @@
"type": "map"
}
]
+ },
+ {
+ "default": null,
+ "name": "originators",
+ "doc": "List of originators attached to the parent loan for revenue sharing",
+ "type": [
+ "null",
+ {
+ "type": "array",
+ "items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1"
+ }
+ ]
}
]
}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java
index 61a3108f5ee..c590c61f93f 100644
--- a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java
@@ -172,6 +172,8 @@
import org.apache.fineract.client.feign.services.TwoFactorApi;
import org.apache.fineract.client.feign.services.UserGeneratedDocumentsApi;
import org.apache.fineract.client.feign.services.UsersApi;
+import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi;
+import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
import org.apache.fineract.client.feign.services.WorkingDaysApi;
/**
@@ -837,6 +839,14 @@ public UsersApi users() {
return create(UsersApi.class);
}
+ public WorkingCapitalLoanProductsApi workingCapitalLoanProducts() {
+ return create(WorkingCapitalLoanProductsApi.class);
+ }
+
+ public WorkingCapitalLoanCobCatchUpApi workingCapitalLoanCobCatchUpApi() {
+ return create(WorkingCapitalLoanCobCatchUpApi.class);
+ }
+
public WorkingDaysApi workingDays() {
return create(WorkingDaysApi.class);
}
diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
index 23f3dff5b3e..6f47fffb876 100644
--- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
+++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
@@ -59,6 +59,7 @@
import org.apache.fineract.client.services.CodeValuesApi;
import org.apache.fineract.client.services.CodesApi;
import org.apache.fineract.client.services.CreditBureauConfigurationApi;
+import org.apache.fineract.client.services.CreditBureauIntegrationApi;
import org.apache.fineract.client.services.CurrencyApi;
import org.apache.fineract.client.services.DataTablesApi;
import org.apache.fineract.client.services.DefaultApi;
@@ -95,6 +96,7 @@
import org.apache.fineract.client.services.LoanDisbursementDetailsApi;
import org.apache.fineract.client.services.LoanInterestPauseApi;
import org.apache.fineract.client.services.LoanProductsApi;
+import org.apache.fineract.client.services.LoanProductsDetailsApi;
import org.apache.fineract.client.services.LoanReschedulingApi;
import org.apache.fineract.client.services.LoanTransactionsApi;
import org.apache.fineract.client.services.LoansApi;
@@ -201,6 +203,7 @@ public final class FineractClient {
public final ChargesApi charges;
public final ClientApi clients;
public final CreditBureauConfigurationApi creditBureauConfiguration;
+ public final CreditBureauIntegrationApi creditBureauIntegration;
public final ClientSearchV2Api clientSearchV2;
public final ClientChargesApi clientCharges;
@@ -238,6 +241,7 @@ public final class FineractClient {
public final LoanCollateralApi loanCollaterals;
public final LoanCapitalizedIncomeApi loanCapitalizedIncome;
public final LoanProductsApi loanProducts;
+ public final LoanProductsDetailsApi loanProductsDetails;
public final LoanReschedulingApi loanSchedules;
public final LoansPointInTimeApi loansPointInTimeApi;
public final LoansApi loans;
@@ -336,6 +340,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
charges = retrofit.create(ChargesApi.class);
clients = retrofit.create(ClientApi.class);
creditBureauConfiguration = retrofit.create(CreditBureauConfigurationApi.class);
+ creditBureauIntegration = retrofit.create(CreditBureauIntegrationApi.class);
clientSearchV2 = retrofit.create(ClientSearchV2Api.class);
clientCharges = retrofit.create(ClientChargesApi.class);
clientIdentifiers = retrofit.create(ClientIdentifierApi.class);
@@ -371,6 +376,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
loanCollaterals = retrofit.create(LoanCollateralApi.class);
loanCapitalizedIncome = retrofit.create(LoanCapitalizedIncomeApi.class);
loanProducts = retrofit.create(LoanProductsApi.class);
+ loanProductsDetails = retrofit.create(LoanProductsDetailsApi.class);
loanSchedules = retrofit.create(LoanReschedulingApi.class);
loansPointInTimeApi = retrofit.create(LoansPointInTimeApi.class);
loans = retrofit.create(LoansApi.class);
diff --git a/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientDemo.java b/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientDemo.java
index 9e4950b56f8..93676262e94 100644
--- a/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientDemo.java
+++ b/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientDemo.java
@@ -41,7 +41,7 @@ void demoClient() {
// tag::documentation[]
FineractClient fineract = FineractClient.builder().baseURL("https://demo.fineract.dev/fineract-provider/api/v1/").tenant("default")
.basicAuth("mifos", "password").build();
- List staff = Calls.ok(fineract.staff.retrieveAll16(1L, true, false, "ACTIVE"));
+ List staff = Calls.ok(fineract.staff.retrieveAllStaff(1L, true, false, "ACTIVE"));
String name = staff.get(0).getDisplayName();
log.info("Display name: {}", name);
// end::documentation[]
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java b/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java
index 0e874850aac..8af61de5a03 100644
--- a/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/COBConstant.java
@@ -27,7 +27,11 @@ public class COBConstant {
public static final String COB_CUSTOM_JOB_PARAMETER_KEY = "CUSTOM_JOB_PARAMETER_ID";
+ public static final String INLINE_IDS_PARAMETER_NAME = "LoanIds";
+ public static final String COB_PARAMETER = "loanCobParameter";
public static final Long NUMBER_OF_DAYS_BEHIND = 1L;
+ public static final String PARTITION_KEY = "partition";
+ public static final String PARTITION_PREFIX = "partition_";
protected COBConstant() {
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java b/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java
new file mode 100644
index 00000000000..5d27f3973d7
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/common/CommonPartitioner.java
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.common;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
+import org.apache.fineract.cob.data.COBParameter;
+import org.apache.fineract.cob.data.COBPartition;
+import org.apache.fineract.cob.resolver.BusinessDateResolver;
+import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.apache.fineract.cob.service.RetrieveIdService;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.launch.JobExecutionNotRunningException;
+import org.springframework.batch.core.launch.JobOperator;
+import org.springframework.batch.core.launch.NoSuchJobExecutionException;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.util.StopWatch;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class CommonPartitioner {
+
+ private final JobOperator jobOperator;
+ private final StepExecution stepExecution;
+ private final Long numberOfDays;
+ private final RetrieveIdService retrieveIdService;
+
+ public Map getPartitions(int partitionSize, Set cobBusinessSteps) {
+ if (cobBusinessSteps.isEmpty()) {
+ stopJobExecution();
+ return Map.of();
+ }
+ LocalDate businessDate = BusinessDateResolver.resolve(stepExecution);
+ boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution);
+ StopWatch sw = new StopWatch();
+ sw.start();
+ List partitions = new ArrayList<>(
+ retrieveIdService.retrieveLoanCOBPartitions(numberOfDays, businessDate, isCatchUp, partitionSize));
+ sw.stop();
+ // if there is no loan to be closed, we still would like to create at least one partition
+
+ if (partitions.isEmpty()) {
+ partitions.add(new COBPartition(0L, 0L, 1L, 0L));
+ }
+ log.info(
+ "{}} found {} loans to be processed as part of COB. {} partitions were created using partition size {}. RetrieveLoanCOBPartitions was executed in {} ms.",
+ getClass().getName(), getLoanCount(partitions), partitions.size(), partitionSize, sw.getTotalTimeMillis());
+ return partitions.stream().collect(Collectors.toMap(l -> COBConstant.PARTITION_PREFIX + l.getPageNo(),
+ l -> createExecutionContextForPartition(cobBusinessSteps, l, businessDate, isCatchUp)));
+ }
+
+ private long getLoanCount(List loanCOBPartitions) {
+ return loanCOBPartitions.stream().map(COBPartition::getCount).reduce(0L, Long::sum);
+ }
+
+ private ExecutionContext createExecutionContextForPartition(Set cobBusinessSteps,
+ COBPartition loanCOBPartition, LocalDate businessDate, boolean isCatchUp) {
+ ExecutionContext executionContext = new ExecutionContext();
+ executionContext.put(COBConstant.BUSINESS_STEPS, cobBusinessSteps);
+ executionContext.put(COBConstant.COB_PARAMETER, new COBParameter(loanCOBPartition.getMinId(), loanCOBPartition.getMaxId()));
+ executionContext.put(COBConstant.PARTITION_KEY, COBConstant.PARTITION_PREFIX + loanCOBPartition.getPageNo());
+ executionContext.put(COBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDate.toString());
+ executionContext.put(COBConstant.IS_CATCH_UP_PARAMETER_NAME, Boolean.toString(isCatchUp));
+ return executionContext;
+ }
+
+ private void stopJobExecution() {
+ Long jobId = stepExecution.getJobExecution().getId();
+ try {
+ jobOperator.stop(jobId);
+ } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
+ log.error("There is no running execution for the given execution ID. Execution ID: {}", jobId);
+ throw new RuntimeException(e);
+ }
+
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java b/fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java
similarity index 96%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java
index 70639d9fb4e..652d50bbee3 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/converter/COBParameterConverter.java
@@ -29,6 +29,7 @@ public static COBParameter convert(Object obj) {
if (obj instanceof COBParameter) {
return (COBParameter) obj;
} else if (obj instanceof LoanCOBParameter loanCOBParameter) {
+ // for backward compatibility
return loanCOBParameter.toCOBParameter();
}
return null;
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java b/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java
index 5f084f458b0..ddad257e18b 100644
--- a/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/data/BusinessStepNameAndOrder.java
@@ -19,6 +19,10 @@
package org.apache.fineract.cob.data;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -31,4 +35,10 @@ public class BusinessStepNameAndOrder {
private String stepName;
private Long stepOrder;
+
+ public static TreeMap getBusinessStepMap(Set businessSteps) {
+ Map businessStepMap = businessSteps.stream()
+ .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName));
+ return new TreeMap<>(businessStepMap);
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java b/fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
similarity index 99%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
index a17c5a9bf75..6c9d7eb6d9a 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
@@ -29,6 +29,7 @@
@NoArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@EqualsAndHashCode
+@Deprecated
public class LoanCOBParameter {
private Long minLoanId;
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java
new file mode 100644
index 00000000000..3fdfab8c177
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AbstractLockingService.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.domain;
+
+import java.sql.PreparedStatement;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+@RequiredArgsConstructor
+@Slf4j
+public abstract class AbstractLockingService implements LockingService {
+
+ private final JdbcTemplate jdbcTemplate;
+ private final FineractProperties fineractProperties;
+ private final AccountLockRepository loanAccountLockRepository;
+
+ protected abstract String getBatchLoanLockUpgrade();
+
+ protected abstract String getBatchLoanLockInsert();
+
+ @Override
+ public void upgradeLock(List accountsToLock, LockOwner lockOwner) {
+ jdbcTemplate.batchUpdate(getBatchLoanLockUpgrade(), accountsToLock, getInClauseParameterSizeLimit(), (ps, id) -> {
+ ps.setString(1, lockOwner.name());
+ ps.setObject(2, DateUtils.getAuditOffsetDateTime());
+ ps.setLong(3, id);
+ });
+ }
+
+ @Override
+ public List findAllByLoanIdIn(List loanIds) {
+ return loanAccountLockRepository.findAllByLoanIdIn(loanIds);
+ }
+
+ @Override
+ public T findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner) {
+ return loanAccountLockRepository.findByLoanIdAndLockOwner(loanId, lockOwner).orElseGet(() -> {
+ log.warn("There is no lock for loan account with id: {}", loanId);
+ return null;
+ });
+ }
+
+ @Override
+ public List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) {
+ return loanAccountLockRepository.findAllByLoanIdInAndLockOwner(loanIds, lockOwner);
+ }
+
+ @Override
+ public void applyLock(List loanIds, LockOwner lockOwner) {
+ LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
+ jdbcTemplate.batchUpdate(getBatchLoanLockInsert(), loanIds, loanIds.size(), (PreparedStatement ps, Long loanId) -> {
+ ps.setLong(1, loanId);
+ ps.setLong(2, 1);
+ ps.setString(3, lockOwner.name());
+ ps.setObject(4, DateUtils.getAuditOffsetDateTime());
+ ps.setObject(5, cobBusinessDate);
+ });
+ }
+
+ @Override
+ public void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) {
+ loanAccountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, lockOwner);
+ }
+
+ private int getInClauseParameterSizeLimit() {
+ return fineractProperties.getQuery().getInClauseParameterSizeLimit();
+ }
+}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java
new file mode 100644
index 00000000000..5721b33e9a3
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLock.java
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.Id;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.PostLoad;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Transient;
+import jakarta.persistence.Version;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.springframework.data.domain.Persistable;
+
+@Getter
+@MappedSuperclass
+@NoArgsConstructor
+public abstract class AccountLock implements Persistable, Serializable {
+
+ protected static final long serialVersionUID = 2272591907035824317L;
+
+ @Id
+ @Getter
+ @Column(name = "loan_id", nullable = false)
+ protected Long loanId;
+
+ @Version
+ @Getter
+ @Column(name = "version")
+ protected Long version;
+
+ @Enumerated(EnumType.STRING)
+ @Getter
+ @Column(name = "lock_owner", nullable = false)
+ protected LockOwner lockOwner;
+
+ @Column(name = "lock_placed_on", nullable = false)
+ @Getter
+ protected OffsetDateTime lockPlacedOn;
+
+ @Column(name = "error")
+ @Getter
+ protected String error;
+
+ @Column(name = "stacktrace")
+ @Getter
+ protected String stacktrace;
+
+ @Column(name = "lock_placed_on_cob_business_date")
+ @Getter
+ protected LocalDate lockPlacedOnCobBusinessDate;
+
+ @Transient
+ @Setter(value = AccessLevel.NONE)
+ @Getter
+ protected boolean isNew = true;
+
+ @PrePersist
+ @PostLoad
+ void markNotNew() {
+ this.isNew = false;
+ }
+
+ @Override
+ public Long getId() {
+ return getLoanId();
+ }
+
+ public AccountLock(Long loanId, LockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) {
+ this.loanId = loanId;
+ this.lockOwner = lockOwner;
+ this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
+ this.lockPlacedOnCobBusinessDate = lockPlacedOnCobBusinessDate;
+ }
+
+ public void setError(String errorMessage, String stacktrace) {
+ this.error = errorMessage;
+ this.stacktrace = stacktrace;
+ }
+
+ public void setNewLockOwner(LockOwner newLockOwner) {
+ this.lockOwner = newLockOwner;
+ this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
+ }
+}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java
new file mode 100644
index 00000000000..9d1a34dd7b4
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/AccountLockRepository.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.domain;
+
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.repository.NoRepositoryBean;
+
+@NoRepositoryBean
+public interface AccountLockRepository {
+
+ Optional findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+
+ void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
+
+ List findAllByLoanIdIn(List loanIds);
+
+ boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+
+ boolean existsByLoanIdAndLockOwnerAndErrorIsNotNull(Long loanId, LockOwner lockOwner);
+
+ List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
+
+ void removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(List lockOwners);
+
+ Page findAll(Pageable loanAccountLockPage);
+
+ T saveAndFlush(T entity);
+
+ Optional findById(Long id);
+}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java
index 765db8e28ca..ba5d8ed4f4a 100644
--- a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepository.java
@@ -18,7 +18,7 @@
*/
package org.apache.fineract.cob.domain;
-public interface CustomLoanAccountLockRepository {
+public interface CustomLoanAccountLockRepository {
void updateLoanFromAccountLocks();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java
similarity index 71%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java
index 00830ba4e5f..7b2e12ec066 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/domain/LockingService.java
@@ -16,23 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cob.loan;
+package org.apache.fineract.cob.domain;
import java.util.List;
-import org.apache.fineract.cob.domain.LoanAccountLock;
-import org.apache.fineract.cob.domain.LockOwner;
-public interface LoanLockingService {
+public interface LockingService {
void upgradeLock(List accountsToLock, LockOwner lockOwner);
void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
- List findAllByLoanIdIn(List loanIds);
+ List findAllByLoanIdIn(List loanIds);
- LoanAccountLock findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+ T findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
- List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
+ List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
void applyLock(List loanIds, LockOwner lockOwner);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java
similarity index 85%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java
index 5d5c6e1c89c..aac761ce49e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanLockCannotBeAppliedException.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockCannotBeAppliedException.java
@@ -18,9 +18,9 @@
*/
package org.apache.fineract.cob.exceptions;
-public class LoanLockCannotBeAppliedException extends Exception {
+public class LockCannotBeAppliedException extends Exception {
- public LoanLockCannotBeAppliedException(String message, Throwable cause) {
+ public LockCannotBeAppliedException(String message, Throwable cause) {
super(message, cause);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java
similarity index 90%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java
index 482f4f55b26..6a94b8c24a1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/exceptions/LockedReadException.java
@@ -18,11 +18,11 @@
*/
package org.apache.fineract.cob.exceptions;
-public class LoanReadException extends Exception {
+public class LockedReadException extends Exception {
private final Long id;
- public LoanReadException(Long id, Throwable t) {
+ public LockedReadException(Long id, Throwable t) {
super(String.format("Loan is in already locked state! loanId: %d", id), t);
this.id = id;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java b/fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java
similarity index 84%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java
index e42c9973ca9..34e44f9292d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/listener/AbstractLoanItemListener.java
@@ -23,13 +23,12 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.AccountLock;
import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.exceptions.LoanReadException;
-import org.apache.fineract.cob.loan.LoanLockingService;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.exceptions.LockedReadException;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.serialization.ThrowableSerialization;
-import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.batch.core.annotation.OnProcessError;
import org.springframework.batch.core.annotation.OnReadError;
import org.springframework.batch.core.annotation.OnSkipInProcess;
@@ -44,9 +43,9 @@
@Slf4j
@RequiredArgsConstructor
-public abstract class AbstractLoanItemListener {
+public abstract class AbstractLoanItemListener> {
- private final LoanLockingService loanLockingService;
+ private final LockingService loanLockingService;
private final TransactionTemplate transactionTemplate;
@@ -57,7 +56,7 @@ private void updateAccountLockWithError(List loanIds, String msg, Throwabl
@Override
protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
for (Long loanId : loanIds) {
- LoanAccountLock loanAccountLock = loanLockingService.findByLoanIdAndLockOwner(loanId, getLockOwner());
+ T loanAccountLock = loanLockingService.findByLoanIdAndLockOwner(loanId, getLockOwner());
if (loanAccountLock != null) {
loanAccountLock.setError(String.format(msg, loanId), ThrowableSerialization.serialize(e));
}
@@ -68,7 +67,7 @@ protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
@OnReadError
public void onReadError(Exception e) {
- if (e instanceof LoanReadException ee) {
+ if (e instanceof LockedReadException ee) {
log.warn("Error was triggered during reading of Loan (id={}) due to: {}", ee.getId(), ThrowableSerialization.serialize(e));
updateAccountLockWithError(List.of(ee.getId()), "Loan (id: %d) reading is failed", e);
} else {
@@ -77,13 +76,13 @@ public void onReadError(Exception e) {
}
@OnProcessError
- public void onProcessError(@NonNull Loan item, Exception e) {
+ public void onProcessError(@NonNull S item, Exception e) {
log.warn("Error was triggered during processing of Loan (id={}) due to: {}", item.getId(), ThrowableSerialization.serialize(e));
updateAccountLockWithError(List.of(item.getId()), "Loan (id: %d) processing is failed", e);
}
@OnWriteError
- public void onWriteError(Exception e, @NonNull Chunk extends Loan> items) {
+ public void onWriteError(Exception e, @NonNull Chunk extends S> items) {
List loanIds = items.getItems().stream().map(AbstractPersistableCustom::getId).toList();
log.warn("Error was triggered during writing of Loans (ids={}) due to: {}", loanIds, ThrowableSerialization.serialize(e));
@@ -96,12 +95,12 @@ public void onSkipInRead(@NonNull Throwable e) {
}
@OnSkipInProcess
- public void onSkipInProcess(@NonNull Loan item, @NonNull Throwable e) {
+ public void onSkipInProcess(@NonNull S item, @NonNull Throwable e) {
log.warn("Skipping was triggered during processing of Loan (id={})", item.getId());
}
@OnSkipInWrite
- public void onSkipInWrite(@NonNull Loan item, @NonNull Throwable e) {
+ public void onSkipInWrite(@NonNull S item, @NonNull Throwable e) {
log.warn("Skipping was triggered during writing of Loan (id={})", item.getId());
}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java b/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java
new file mode 100644
index 00000000000..9efdeb1d7e9
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/processor/AbstractItemProcessor.java
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.processor;
+
+import static org.apache.fineract.cob.data.BusinessStepNameAndOrder.getBusinessStepMap;
+
+import java.time.LocalDate;
+import java.util.Set;
+import java.util.TreeMap;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
+import org.apache.fineract.cob.resolver.BusinessDateResolver;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.annotation.AfterStep;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.lang.NonNull;
+
+@RequiredArgsConstructor
+public abstract class AbstractItemProcessor> implements ItemProcessor {
+
+ private final COBBusinessStepService cobBusinessStepService;
+
+ @Setter
+ private ExecutionContext executionContext;
+
+ @Getter
+ private LocalDate businessDate;
+
+ @SuppressWarnings({ "unchecked" })
+ @Override
+ public I process(@NonNull I item) throws Exception {
+ Set businessSteps = (Set) executionContext.get("businessSteps");
+ if (businessSteps == null) {
+ throw new IllegalStateException("No business steps found in the execution context");
+ }
+ TreeMap businessStepMap = getBusinessStepMap(businessSteps);
+
+ I alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, item);
+ setLastRun(alreadyProcessedLoan);
+ return alreadyProcessedLoan;
+ }
+
+ protected void setBusinessDate(StepExecution stepExecution) {
+ businessDate = BusinessDateResolver.resolve(stepExecution);
+ }
+
+ @AfterStep
+ public ExitStatus afterStep(@NonNull StepExecution stepExecution) {
+ return ExitStatus.COMPLETED;
+ }
+
+ public abstract void setLastRun(I processedLoan);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java
similarity index 93%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java
index e91dabbbc80..7b57f89407e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/BusinessDateResolver.java
@@ -19,7 +19,7 @@
package org.apache.fineract.cob.resolver;
import java.time.LocalDate;
-import org.apache.fineract.cob.loan.LoanCOBConstant;
+import org.apache.fineract.cob.COBConstant;
import org.springframework.batch.core.StepExecution;
public final class BusinessDateResolver {
@@ -27,7 +27,7 @@ public final class BusinessDateResolver {
private BusinessDateResolver() {}
public static LocalDate resolve(StepExecution stepExecution) {
- Object bd = stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME);
+ Object bd = stepExecution.getJobExecution().getExecutionContext().get(COBConstant.BUSINESS_DATE_PARAMETER_NAME);
return switch (bd) {
case null -> throw new IllegalStateException(
"Missing BusinessDate in JobExecutionContext for jobExecutionId=" + stepExecution.getJobExecution().getId());
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java
similarity index 92%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java
index 25afa79c45e..6ddf59df585 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/resolver/CatchUpFlagResolver.java
@@ -18,7 +18,7 @@
*/
package org.apache.fineract.cob.resolver;
-import org.apache.fineract.cob.loan.LoanCOBConstant;
+import org.apache.fineract.cob.COBConstant;
import org.springframework.batch.core.StepExecution;
public final class CatchUpFlagResolver {
@@ -26,7 +26,7 @@ public final class CatchUpFlagResolver {
private CatchUpFlagResolver() {}
public static boolean resolve(StepExecution stepExecution) {
- Object isCatchUp = stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME);
+ Object isCatchUp = stepExecution.getJobExecution().getExecutionContext().get(COBConstant.IS_CATCH_UP_PARAMETER_NAME);
return switch (isCatchUp) {
case null -> false;
case String isCatchUpStr -> Boolean.parseBoolean(isCatchUpStr);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java
similarity index 71%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java
index 1077c7afad0..29d5aa8a531 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AbstractAccountLockService.java
@@ -20,26 +20,26 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.cob.domain.LoanAccountLock;
-import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.AccountLock;
+import org.apache.fineract.cob.domain.AccountLockRepository;
+import org.apache.fineract.cob.domain.CustomLoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
-import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
-@Service
@RequiredArgsConstructor
-public class LoanAccountLockServiceImpl implements LoanAccountLockService {
+public abstract class AbstractAccountLockService implements AccountLockService {
- private final LoanAccountLockRepository loanAccountLockRepository;
+ private final AccountLockRepository loanAccountLockRepository;
+ private final CustomLoanAccountLockRepository customLoanAccountLockRepository;
@Override
- public List getLockedLoanAccountByPage(int page, int limit) {
+ public List getLockedLoanAccountByPage(int page, int limit) {
Pageable loanAccountLockPage = PageRequest.of(page, limit);
- Page loanAccountLocks = loanAccountLockRepository.findAll(loanAccountLockPage);
+ Page loanAccountLocks = loanAccountLockRepository.findAll(loanAccountLockPage);
return loanAccountLocks.getContent();
}
@@ -58,8 +58,9 @@ public boolean isLockOverrulable(Long loanId) {
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateCobAndRemoveLocks() {
- loanAccountLockRepository.updateLoanFromAccountLocks();
- loanAccountLockRepository.removeLockByOwner();
+ customLoanAccountLockRepository.updateLoanFromAccountLocks();
+ loanAccountLockRepository.removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(
+ List.of(LockOwner.LOAN_COB_CHUNK_PROCESSING, LockOwner.LOAN_INLINE_COB_PROCESSING));
}
}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java
new file mode 100644
index 00000000000..4f55670ac2f
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/AccountLockService.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import java.util.List;
+import org.apache.fineract.cob.domain.AccountLock;
+
+public interface AccountLockService {
+
+ List getLockedLoanAccountByPage(int page, int limit);
+
+ boolean isLoanHardLocked(Long loanId);
+
+ boolean isLockOverrulable(Long loanId);
+
+ void updateCobAndRemoveLocks();
+}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java
new file mode 100644
index 00000000000..c79d761829c
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/BeforeStepLockingItemReaderHelper.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.converter.COBParameterConverter;
+import org.apache.fineract.cob.data.COBParameter;
+import org.apache.fineract.cob.domain.AccountLock;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.lang.NonNull;
+
+@RequiredArgsConstructor
+public class BeforeStepLockingItemReaderHelper {
+
+ private final RetrieveIdService retrieveIdService;
+ private final LockingService loanLockingService;
+
+ @SuppressWarnings({ "unchecked" })
+ public LinkedBlockingQueue filterRemainingData(@NonNull StepExecution stepExecution) {
+ ExecutionContext executionContext = stepExecution.getExecutionContext();
+ COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(COBConstant.COB_PARAMETER));
+ List loanIds;
+ boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution);
+ if (Objects.isNull(loanCOBParameter)
+ || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId()))
+ || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) {
+ loanIds = Collections.emptyList();
+ } else {
+ loanIds = retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp);
+ if (!loanIds.isEmpty()) {
+ List lockedByCOBChunkProcessingAccountIds = getLoanIdsLockedWithChunkProcessingLock(loanIds);
+ loanIds.retainAll(lockedByCOBChunkProcessingAccountIds);
+ }
+ }
+ return new LinkedBlockingQueue<>(loanIds);
+ }
+
+ private List getLoanIdsLockedWithChunkProcessingLock(List loanIds) {
+ List accountLocks = new ArrayList<>(
+ loanLockingService.findAllByLoanIdInAndLockOwner(loanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING));
+ return accountLocks.stream().map(T::getLoanId).toList();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java b/fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java
similarity index 85%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
rename to fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java
index 590757fc741..a934a161c3e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/service/RetrieveIdService.java
@@ -16,8 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cob.loan;
+package org.apache.fineract.cob.service;
+import java.sql.ResultSet;
+import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
@@ -26,7 +28,7 @@
import org.apache.fineract.cob.data.COBPartition;
import org.springframework.data.repository.query.Param;
-public interface RetrieveLoanIdService {
+public interface RetrieveIdService {
List retrieveLoanCOBPartitions(Long numberOfDays, LocalDate businessDate, boolean isCatchUp, int partitionSize);
@@ -41,4 +43,8 @@ public interface RetrieveLoanIdService {
List findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate);
List retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List loanIds);
+
+ static COBPartition mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new COBPartition(rs.getLong("min"), rs.getLong("max"), rs.getLong("page"), rs.getLong("count"));
+ }
}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java b/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java
new file mode 100644
index 00000000000..b1fcd63d446
--- /dev/null
+++ b/fineract-cob/src/main/java/org/apache/fineract/cob/tasklet/ApplyCommonLockTasklet.java
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.tasklet;
+
+import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
+
+import com.google.common.collect.Lists;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.converter.COBParameterConverter;
+import org.apache.fineract.cob.data.COBParameter;
+import org.apache.fineract.cob.domain.AccountLock;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.exceptions.LockCannotBeAppliedException;
+import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.apache.fineract.cob.service.RetrieveIdService;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.step.tasklet.Tasklet;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.lang.NonNull;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class ApplyCommonLockTasklet implements Tasklet {
+
+ private static final long NUMBER_OF_RETRIES = 3;
+ private final FineractProperties fineractProperties;
+ private final LockingService loanLockingService;
+ private final RetrieveIdService retrieveIdService;
+ private final TransactionTemplate transactionTemplate;
+
+ public abstract String getCOBParameter();
+
+ public abstract LockOwner getLockOwner();
+
+ @Override
+ @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
+ public RepeatStatus execute(@NonNull StepContribution contribution, @NonNull ChunkContext chunkContext)
+ throws LockCannotBeAppliedException {
+ ExecutionContext executionContext = contribution.getStepExecution().getExecutionContext();
+ long numberOfExecutions = contribution.getStepExecution().getCommitCount();
+ COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(getCOBParameter()));
+ boolean isCatchUp = CatchUpFlagResolver.resolve(contribution.getStepExecution());
+ List loanIds;
+ if (Objects.isNull(loanCOBParameter)
+ || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId()))
+ || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) {
+ loanIds = Collections.emptyList();
+ } else {
+ loanIds = new ArrayList<>(
+ retrieveIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp));
+ }
+ List> loanIdPartitions = Lists.partition(loanIds, getInClauseParameterSizeLimit());
+ List accountLocks = new ArrayList<>();
+ loanIdPartitions.forEach(loanIdPartition -> accountLocks.addAll(loanLockingService.findAllByLoanIdIn(loanIdPartition)));
+
+ List toBeProcessedLoanIds = new ArrayList<>(loanIds);
+ List alreadyLockedAccountIds = accountLocks.stream().map(AccountLock::getLoanId).toList();
+
+ toBeProcessedLoanIds.removeAll(alreadyLockedAccountIds);
+ try {
+ applyLocks(toBeProcessedLoanIds);
+ } catch (Exception e) {
+ if (numberOfExecutions > NUMBER_OF_RETRIES) {
+ String message = "There was an error applying lock to loan accounts.";
+ log.error("{}", message, e);
+ throw new LockCannotBeAppliedException(message, e);
+ } else {
+ return RepeatStatus.CONTINUABLE;
+ }
+ }
+
+ return RepeatStatus.FINISHED;
+ }
+
+ private void applyLocks(List toBeProcessedLoanIds) {
+ transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+
+ @Override
+ protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
+ log.info("Apply locks for {} by owner {}", toBeProcessedLoanIds, getLockOwner());
+ loanLockingService.applyLock(toBeProcessedLoanIds, getLockOwner());
+ }
+ });
+ }
+
+ private int getInClauseParameterSizeLimit() {
+ return fineractProperties.getQuery().getInClauseParameterSizeLimit();
+ }
+}
diff --git a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
index 509a60dd916..af0d95e2a35 100644
--- a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
+++ b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml
@@ -65,7 +65,7 @@
org.apache.fineract.infrastructure.businessdate.domain.BusinessDate
org.apache.fineract.infrastructure.codes.domain.CodeValue
-
+
false
diff --git a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
index 110908f75ad..535623c9121 100644
--- a/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
+++ b/fineract-command/src/main/java/org/apache/fineract/command/starter/CommandPersistenceConfiguration.java
@@ -28,7 +28,7 @@
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
@Configuration
-@EnableJdbcRepositories(basePackages = { "org.apache.fineract.**.domain", "org.apache.fineract.**.persistence" })
+@EnableJdbcRepositories(basePackages = { "org.apache.fineract.**.domain" })
@ComponentScan("org.apache.fineract.command.persistence")
class CommandPersistenceConfiguration {
diff --git a/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java b/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
index 638a65ea6fa..79f266c5c0d 100644
--- a/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
+++ b/fineract-command/src/test/java/org/apache/fineract/command/CommandBaseTest.java
@@ -33,6 +33,7 @@
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.utility.DockerImageName;
@@ -45,12 +46,13 @@ abstract class CommandBaseTest {
protected static Network network = Network.newNetwork();
@Container
- private static final PostgreSQLContainer> POSTGRES_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16"))
+ private static final PostgreSQLContainer> POSTGRES_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:18.3"))
.withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
@Container
- private static final MariaDBContainer> MARIADB_CONTAINER = new MariaDBContainer<>(DockerImageName.parse("mariadb:11.4"))
- .withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test");
+ private static final MariaDBContainer> MARIADB_CONTAINER = new MariaDBContainer<>(DockerImageName.parse("mariadb:12.2"))
+ .withNetwork(network).withUsername("root").withPassword("mifos").withDatabaseName("fineract-test")
+ .withCommand("--innodb-snapshot-isolation=OFF").waitingFor(Wait.forListeningPort());
@Container
private static final MySQLContainer> MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8")).withNetwork(network)
diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
index 01e6b6d2513..914d2014f44 100644
--- a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
+++ b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
@@ -149,6 +149,8 @@ private static void init() {
commandStrategies.put(CommandContext
.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\/transactions\\/" + NUMBER_REGEX + OPTIONAL_COMMAND_PARAM_REGEX)
.method(POST).build(), "savingsAccountAdjustTransactionCommandStrategy");
+ commandStrategies.put(CommandContext.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\/charges").method(POST).build(),
+ "createSavingsAccountChargeCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/charges").method(POST).build(),
"createChargeCommandStrategy");
commandStrategies.put(
diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 61b339618b4..8493553fb56 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -426,13 +426,6 @@ public CommandWrapperBuilder deleteReport(final Long id) {
return this;
}
- public CommandWrapperBuilder updateCurrencies() {
- this.actionName = "UPDATE";
- this.entityName = "CURRENCY";
- this.href = "/currencies";
- return this;
- }
-
public CommandWrapperBuilder createSms() {
this.actionName = "CREATE";
this.entityName = "SMS";
@@ -553,6 +546,30 @@ public CommandWrapperBuilder updateLoanProduct(final Long productId) {
return this;
}
+ public CommandWrapperBuilder createWorkingCapitalLoanProduct() {
+ this.actionName = "CREATE";
+ this.entityName = "WORKINGCAPITALLOANPRODUCT";
+ this.entityId = null;
+ this.href = "/working-capital-loan-products/template";
+ return this;
+ }
+
+ public CommandWrapperBuilder updateWorkingCapitalLoanProduct(final Long productId) {
+ this.actionName = "UPDATE";
+ this.entityName = "WORKINGCAPITALLOANPRODUCT";
+ this.entityId = productId;
+ this.href = "/working-capital-loan-products/" + productId;
+ return this;
+ }
+
+ public CommandWrapperBuilder deleteWorkingCapitalLoanProduct(final Long productId) {
+ this.actionName = "DELETE";
+ this.entityName = "WORKINGCAPITALLOANPRODUCT";
+ this.entityId = productId;
+ this.href = "/working-capital-loan-products/" + productId;
+ return this;
+ }
+
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
this.actionName = "CREATE";
this.entityName = "CLIENTIDENTIFIER";
@@ -1862,55 +1879,6 @@ public CommandWrapperBuilder deleteCalendar(final String supportedEntityType, fi
return this;
}
- public CommandWrapperBuilder createNote(final CommandWrapper resourceDetails, final String resourceType, final Long resourceId) {
- this.actionName = "CREATE";
- this.entityName = resourceDetails.entityName();// Note supports multiple
- // resources. Note
- // Permissions are set
- // for each resource.
- this.clientId = resourceDetails.getClientId();
- this.loanId = resourceDetails.getLoanId();
- this.savingsId = resourceDetails.getSavingsId();
- this.groupId = resourceDetails.getGroupId();
- this.subentityId = resourceDetails.subresourceId();
- this.href = "/" + resourceType + "/" + resourceId + "/notes/template";
- return this;
- }
-
- public CommandWrapperBuilder updateNote(final CommandWrapper resourceDetails, final String resourceType, final Long resourceId,
- final Long noteId) {
- this.actionName = "UPDATE";
- this.entityName = resourceDetails.entityName();// Note supports multiple
- // resources. Note
- // Permissions are set
- // for each resource.
- this.entityId = noteId;
- this.clientId = resourceDetails.getClientId();
- this.loanId = resourceDetails.getLoanId();
- this.savingsId = resourceDetails.getSavingsId();
- this.groupId = resourceDetails.getGroupId();
- this.subentityId = resourceDetails.subresourceId();
- this.href = "/" + resourceType + "/" + resourceId + "/notes";
- return this;
- }
-
- public CommandWrapperBuilder deleteNote(final CommandWrapper resourceDetails, final String resourceType, final Long resourceId,
- final Long noteId) {
- this.actionName = "DELETE";
- this.entityName = resourceDetails.entityName();// Note supports multiple
- // resources. Note
- // Permissions are set
- // for each resource.
- this.entityId = noteId;
- this.clientId = resourceDetails.getClientId();
- this.loanId = resourceDetails.getLoanId();
- this.savingsId = resourceDetails.getSavingsId();
- this.groupId = resourceDetails.getGroupId();
- this.subentityId = resourceDetails.subresourceId();
- this.href = "/" + resourceType + "/" + resourceId + "/calendars/" + noteId;
- return this;
- }
-
public CommandWrapperBuilder createGroup() {
this.actionName = "CREATE";
this.entityName = "GROUP";
@@ -2193,14 +2161,6 @@ public CommandWrapperBuilder deleteAccountingRule(final Long accountingRuleId) {
return this;
}
- public CommandWrapperBuilder updateTaxonomyMapping(final Long mappingId) {
- this.actionName = "UPDATE";
- this.entityName = "XBRLMAPPING";
- this.entityId = mappingId;
- this.href = "/xbrlmapping";
- return this;
- }
-
public CommandWrapperBuilder createHoliday() {
this.actionName = "CREATE";
this.entityName = "HOLIDAY";
@@ -2351,6 +2311,14 @@ public CommandWrapperBuilder updateJobDetail(final Long jobId) {
return this;
}
+ public CommandWrapperBuilder executeSchedulerJob(final Long jobId) {
+ this.actionName = "EXECUTEJOB";
+ this.entityName = "SCHEDULER";
+ this.entityId = jobId;
+ this.href = "/jobs/" + jobId + "?command=executeJob";
+ return this;
+ }
+
public CommandWrapperBuilder createMeeting(final CommandWrapper resourceDetails, final String supportedEntityType,
final Long supportedEntityId) {
this.actionName = "CREATE";
@@ -2387,13 +2355,6 @@ public CommandWrapperBuilder saveOrUpdateAttendance(final Long entityId, final S
return this;
}
- public CommandWrapperBuilder updateCache() {
- this.actionName = "UPDATE";
- this.entityName = "CACHE";
- this.href = "/cache";
- return this;
- }
-
/**
* Deposit account mappings
*/
@@ -3582,13 +3543,6 @@ public CommandWrapperBuilder updateRate(final Long rateId) {
return this;
}
- public CommandWrapperBuilder updateBusinessDate() {
- this.actionName = "UPDATE";
- this.entityName = "BUSINESS_DATE";
- this.href = "/businessdate";
- return this;
- }
-
public CommandWrapperBuilder createDelinquencyRange() {
this.actionName = "CREATE";
this.entityName = "DELINQUENCY_RANGE";
@@ -3651,13 +3605,6 @@ public CommandWrapperBuilder executeInlineJob(String jobName) {
return this;
}
- public CommandWrapperBuilder updateExternalEventConfigurations() {
- this.actionName = "UPDATE";
- this.entityName = "EXTERNAL_EVENT_CONFIGURATION";
- this.href = "/externaleventconfiguration";
- return this;
- }
-
public CommandWrapperBuilder chargeOff(final Long loanId) {
this.actionName = "CHARGEOFF";
this.entityName = "LOAN";
@@ -3954,4 +3901,13 @@ public CommandWrapperBuilder detachLoanOriginator(final Long loanId, final Long
this.href = "/loans/" + loanId + "/originators/" + originatorId;
return this;
}
+
+ public CommandWrapperBuilder savingsAccountForceWithdrawal(final Long accountId) {
+ this.actionName = "FORCE_WITHDRAWAL";
+ this.entityName = "SAVINGSACCOUNT";
+ this.entityId = accountId;
+ this.savingsId = accountId;
+ this.href = "/savingsaccounts/" + accountId;
+ return this;
+ }
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
index 5406ac5b445..604a07a7a4d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
@@ -19,6 +19,7 @@
package org.apache.fineract.commands.service;
import com.google.gson.JsonElement;
+import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +33,7 @@
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.dataqueries.service.CleanupService;
import org.apache.fineract.infrastructure.jobs.service.SchedulerJobRunnerReadService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -49,6 +51,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
private final CommandProcessingService processAndLogCommandService;
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
private final ConfigurationDomainService configurationService;
+ private final List cleanupServices;
@Override
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
@@ -146,6 +149,11 @@ public Long rejectEntry(final Long makerCheckerId) {
final AppUser maker = this.context.authenticatedUser();
commandSourceInput.markAsRejected(maker);
this.commandSourceRepository.save(commandSourceInput);
+ if (cleanupServices != null) {
+ for (CleanupService cleanupService : cleanupServices) {
+ cleanupService.cleanup(commandSourceInput);
+ }
+ }
return makerCheckerId;
}
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
index 88ec4bc0dde..edaa38bc970 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
@@ -81,6 +81,9 @@ public final class GlobalConfigurationConstants {
public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application";
public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT = "password-reuse-check-history-count";
+ public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT = "allow-force-withdrawal-on-savings-account";
+ public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT = "force-withdrawal-on-savings-account-limit";
+ public static final String FORCE_PASSWORD_RESET_ON_FIRST_LOGIN = "force-password-reset-on-first-login";
private GlobalConfigurationConstants() {}
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
index 83c21afd2b1..2f88120cf2f 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
@@ -152,5 +152,11 @@ public interface ConfigurationDomainService {
String getAssetOwnerTransferOustandingInterestStrategy();
+ boolean isForceWithdrawalOnSavingsAccountEnabled();
+
+ Long retrieveForceWithdrawalOnSavingsAccountLimit();
+
Integer getPasswordReuseRestrictionCount();
+
+ boolean isForcePasswordResetOnFirstLoginEnabled();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
new file mode 100644
index 00000000000..175a3a18b27
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.dataqueries.service;
+
+import org.apache.fineract.commands.domain.CommandSource;
+
+public interface CleanupService {
+
+ void cleanup(CommandSource commandSource);
+
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
index 16f9ad9c29e..51a501599e9 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java
@@ -60,6 +60,7 @@ public enum JobName {
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"), //
JOURNAL_ENTRY_AGGREGATION("Journal Entry Aggregation"), //
+ WORKING_CAPITAL_LOAN_COB_JOB("Working Capital Loan COB"), //
; //
private final String name;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDays.java b/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDays.java
index 8fbe7125b58..b55b591c73c 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDays.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDays.java
@@ -21,16 +21,21 @@
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
-import java.util.LinkedHashMap;
-import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Getter;
+import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import lombok.experimental.FieldNameConstants;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
-import org.apache.fineract.organisation.workingdays.api.WorkingDaysApiConstants;
+@Builder
@Getter
+@Setter
@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@FieldNameConstants
@Table(name = "m_working_days")
public class WorkingDays extends AbstractPersistableCustom {
@@ -47,51 +52,4 @@ public class WorkingDays extends AbstractPersistableCustom {
@Column(name = "extend_term_holiday_repayment", nullable = false)
private Boolean extendTermForRepaymentsOnHolidays;
- protected WorkingDays() {
-
- }
-
- public WorkingDays(final String recurrence, final Integer repaymentReschedulingType, final Boolean extendTermForDailyRepayments,
- final Boolean extendTermForRepaymentsOnHolidays) {
- this.recurrence = recurrence;
- this.repaymentReschedulingType = repaymentReschedulingType;
- this.extendTermForDailyRepayments = extendTermForDailyRepayments;
- this.extendTermForRepaymentsOnHolidays = extendTermForRepaymentsOnHolidays;
- }
-
- public Map update(final JsonCommand command) {
- final Map actualChanges = new LinkedHashMap<>(7);
-
- final String recurrenceParamName = "recurrence";
- if (command.isChangeInStringParameterNamed(recurrenceParamName, this.recurrence)) {
- final String newValue = command.stringValueOfParameterNamed(recurrenceParamName);
- actualChanges.put(recurrenceParamName, newValue);
- this.recurrence = newValue;
- }
-
- final String repaymentRescheduleTypeParamName = "repaymentRescheduleType";
- if (command.isChangeInIntegerParameterNamed(repaymentRescheduleTypeParamName, this.repaymentReschedulingType)) {
- final Integer newValue = command.integerValueOfParameterNamed(repaymentRescheduleTypeParamName);
- actualChanges.put(repaymentRescheduleTypeParamName, WorkingDaysEnumerations.workingDaysStatusType(newValue));
- this.repaymentReschedulingType = RepaymentRescheduleType.fromInt(newValue).getValue();
- }
-
- if (command.isChangeInBooleanParameterNamed(WorkingDaysApiConstants.extendTermForDailyRepayments,
- this.extendTermForDailyRepayments)) {
- final Boolean newValue = command.booleanPrimitiveValueOfParameterNamed(WorkingDaysApiConstants.extendTermForDailyRepayments);
- actualChanges.put(WorkingDaysApiConstants.extendTermForDailyRepayments, newValue);
- this.extendTermForDailyRepayments = newValue;
- }
-
- if (command.isChangeInBooleanParameterNamed(WorkingDaysApiConstants.extendTermForRepaymentsOnHolidays,
- this.extendTermForRepaymentsOnHolidays)) {
- final Boolean newValue = command
- .booleanPrimitiveValueOfParameterNamed(WorkingDaysApiConstants.extendTermForRepaymentsOnHolidays);
- actualChanges.put(WorkingDaysApiConstants.extendTermForRepaymentsOnHolidays, newValue);
- this.extendTermForRepaymentsOnHolidays = newValue;
- }
-
- return actualChanges;
- }
-
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDaysEnumerations.java b/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDaysEnumerations.java
index e3f9606285a..c2caf644841 100644
--- a/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDaysEnumerations.java
+++ b/fineract-core/src/main/java/org/apache/fineract/organisation/workingdays/domain/WorkingDaysEnumerations.java
@@ -41,8 +41,8 @@ public static EnumOptionData repaymentRescheduleType(final RepaymentRescheduleTy
case SAME_DAY:
optionData = new EnumOptionData(RepaymentRescheduleType.SAME_DAY.getValue().longValue(),
RepaymentRescheduleType.SAME_DAY.getCode(), "same day");
-
break;
+
case MOVE_TO_NEXT_WORKING_DAY:
optionData = new EnumOptionData(RepaymentRescheduleType.MOVE_TO_NEXT_WORKING_DAY.getValue().longValue(),
RepaymentRescheduleType.MOVE_TO_NEXT_WORKING_DAY.getCode(), "move to next working day");
@@ -52,10 +52,12 @@ public static EnumOptionData repaymentRescheduleType(final RepaymentRescheduleTy
optionData = new EnumOptionData(RepaymentRescheduleType.MOVE_TO_NEXT_REPAYMENT_MEETING_DAY.getValue().longValue(),
RepaymentRescheduleType.MOVE_TO_NEXT_REPAYMENT_MEETING_DAY.getCode(), "move to next repayment meeting day");
break;
+
case MOVE_TO_PREVIOUS_WORKING_DAY:
optionData = new EnumOptionData(RepaymentRescheduleType.MOVE_TO_PREVIOUS_WORKING_DAY.getValue().longValue(),
RepaymentRescheduleType.MOVE_TO_PREVIOUS_WORKING_DAY.getCode(), "move to previous working day");
break;
+
case MOVE_TO_NEXT_MEETING_DAY:
optionData = new EnumOptionData(RepaymentRescheduleType.MOVE_TO_NEXT_MEETING_DAY.getValue().longValue(),
RepaymentRescheduleType.MOVE_TO_NEXT_MEETING_DAY.getCode(), "move to next meeting day");
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java
index 3a81a147c10..5b6f1a69120 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java
@@ -58,14 +58,4 @@ public static PortfolioAccountType fromInt(final Integer type) {
}
return enumType;
}
-
- // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
- public boolean isSavingsAccount() {
- return this.equals(SAVINGS);
- }
-
- // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
- public boolean isLoanAccount() {
- return this.equals(LOAN);
- }
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/Fund.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/Fund.java
index eb791ef3aeb..081a0081855 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/Fund.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/Fund.java
@@ -24,6 +24,7 @@
import jakarta.persistence.UniqueConstraint;
import java.util.LinkedHashMap;
import java.util.Map;
+import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@@ -33,6 +34,7 @@
@UniqueConstraint(columnNames = { "external_id" }, name = "fund_externalid_org") })
public class Fund extends AbstractPersistableCustom {
+ @Getter
@Column(name = "name")
private String name;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/domain/FundRepository.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/FundRepository.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/domain/FundRepository.java
rename to fineract-core/src/main/java/org/apache/fineract/portfolio/fund/domain/FundRepository.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/exception/FundNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/exception/FundNotFoundException.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/exception/FundNotFoundException.java
rename to fineract-core/src/main/java/org/apache/fineract/portfolio/fund/exception/FundNotFoundException.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundReadPlatformService.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/fund/service/FundReadPlatformService.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundReadPlatformService.java
rename to fineract-core/src/main/java/org/apache/fineract/portfolio/fund/service/FundReadPlatformService.java
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java
index 4be438b08ea..a0d800c8d8c 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java
@@ -112,7 +112,7 @@ public PaymentTypeUpdateResponse updatePaymentType(@PathParam("paymentTypeId") f
@Path("{paymentTypeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "Delete a Payment Type", description = "Deletes payment type")
+ @Operation(summary = "Delete a Payment Type", operationId = "deleteCodePaymentType", description = "Deletes payment type")
public PaymentTypeDeleteResponse deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) {
final var command = new PaymentTypeDeleteCommand();
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java
index e0391a3ae8c..0d31101e5a8 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsHelper.java
@@ -57,6 +57,9 @@ public List determineInterestPostingPeriods(final LocalDate s
return postingPeriods;
}
+ if (postInterestAsOn == null) {
+ postInterestAsOn = Collections.emptyList();
+ }
LocalDate periodStartDate = startInterestCalculationLocalDate;
LocalDate periodEndDate = periodStartDate;
LocalDate actualPeriodStartDate = periodStartDate;
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java
index 7f4cab97be5..abbf4f7196a 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java
@@ -202,6 +202,15 @@ public static SavingsAccountTransactionEnumData transactionType(final SavingsAcc
return optionData;
}
+ public static EnumOptionData status(final SavingsAccountStatusEnumData status) {
+
+ Long id = status.getId();
+ String code = status.getCode();
+ String value = status.getValue();
+
+ return new EnumOptionData(id, code, value);
+ }
+
public static SavingsAccountStatusEnumData status(final Integer statusEnum) {
return status(SavingsAccountStatusType.fromInt(statusEnum));
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-core/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
index 661564d52ef..5d7ab23054f 100644
--- a/fineract-core/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
+++ b/fineract-core/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java
@@ -131,6 +131,17 @@ public class AppUser extends AbstractPersistableCustom implements Platform
@Column(name = "cannot_change_password", nullable = true)
private Boolean cannotChangePassword;
+ @Column(name = "password_reset_required", nullable = false)
+ private boolean passwordResetRequired;
+
+ public boolean isPasswordResetRequired() {
+ return this.passwordResetRequired;
+ }
+
+ public void updatePasswordResetRequired(final boolean required) {
+ this.passwordResetRequired = required;
+ }
+
public static AppUser fromJson(final Office userOffice, final Staff linkedStaff, final Set allRoles,
final Collection clients, final JsonCommand command) {
diff --git a/fineract-doc/src/docs/en/chapters/architecture/api-backward-compatibility.adoc b/fineract-doc/src/docs/en/chapters/architecture/api-backward-compatibility.adoc
new file mode 100644
index 00000000000..59eb81c4f24
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/architecture/api-backward-compatibility.adoc
@@ -0,0 +1,190 @@
+= API Backward Compatibility
+
+== Overview
+
+Apache Fineract enforces API backward compatibility using https://github.com/docktape/swagger-brake[swagger-brake], an automated tool that compares OpenAPI specifications between the base branch and a pull request to detect breaking changes. This ensures that existing API consumers are not broken when new changes are deployed.
+
+The check runs automatically on every pull request via the `verify-api-backward-compatibility.yml` GitHub Actions workflow.
+
+== How It Works
+
+The workflow follows these steps:
+
+. **Generate baseline spec** — Checks out the base branch (e.g. `develop`) and runs `./gradlew :fineract-provider:resolve` to generate the current OpenAPI specification.
+. **Generate PR spec** — Checks out the PR branch and generates its OpenAPI specification.
+. **Sanitize specs** — Patches known issues in the generated specs (e.g. missing `schema` entries in `requestBody` content) to prevent false positives.
+. **Compare** — Runs `checkBreakingChanges` via the swagger-brake Gradle plugin to compare old vs new specs.
+. **Report** — If breaking changes are found:
+** A deduplicated summary table is written to the GitHub Actions Step Summary (visible on the workflow run page).
+** A comment is posted on the PR (when token permissions allow).
+** The full JSON report is archived as a build artifact.
+** The workflow **fails**, blocking the PR.
+
+== Breaking Change Rules
+
+swagger-brake detects the following categories of breaking changes:
+
+=== Endpoint Rules
+
+[cols="1,3", options="header"]
+|===
+| Rule | Description
+| R001 | A stable (non-beta) API was changed to beta
+| R002 | An API path was deleted
+|===
+
+=== Request Rules
+
+[cols="1,3", options="header"]
+|===
+| Rule | Description
+| R003 | A request media type (content type) was removed
+| R004 | A request parameter was deleted
+| R005 | An enum value was removed from a request parameter
+| R006 | A parameter location changed (e.g. `query` to `header`)
+| R007 | A parameter was made required
+| R008 | A parameter type was changed
+| R009 | An attribute was removed from a request body schema
+| R010 | A property type was changed in a request schema
+| R011 | An enum value was removed from a request body schema
+|===
+
+=== Response Rules
+
+[cols="1,3", options="header"]
+|===
+| Rule | Description
+| R012 | A response code was deleted
+| R013 | A response media type was removed
+| R014 | An attribute was removed from a response schema
+| R015 | A property type was changed in a response schema
+| R016 | An enum value was removed from a response schema
+|===
+
+=== Constraint Rules
+
+[cols="1,3", options="header"]
+|===
+| Rule | Description
+| R017 | A request parameter constraint was tightened (covers `maxLength`, `minLength`, `maximum`, `minimum`, `maxItems`, `minItems`, `uniqueItems`)
+|===
+
+== Gradle Configuration
+
+The swagger-brake plugin is configured in `fineract-provider/build.gradle`:
+
+[source,groovy]
+----
+apply plugin: 'com.docktape.swagger-brake'
+
+swaggerBrake {
+ newApi = "${project.buildDir}/resources/main/static/fineract.json"
+ oldApi = findProperty('apiBaseline') ?: "${projectDir}/config/swagger/fineract-baseline.json"
+ outputFormats = ['JSON']
+ outputFilePath = "${project.buildDir}/swagger-brake"
+ deprecatedApiDeletionAllowed = true
+ strictValidation = false
+}
+----
+
+=== Configuration Options
+
+[cols="2,1,4", options="header"]
+|===
+| Option | Default | Description
+| `newApi` | — | Path to the new (PR branch) OpenAPI spec. Generated by the `resolve` task.
+| `oldApi` | — | Path to the baseline OpenAPI spec. Provided via `-PapiBaseline` in CI, or falls back to a local file.
+| `outputFormats` | `['STDOUT', 'HTML']` | Report formats. We use `['JSON']` to avoid STDOUT spam and parse the report programmatically.
+| `outputFilePath` | `build/swagger-brake` | Directory for generated reports.
+| `deprecatedApiDeletionAllowed` | `true` | When `true`, removing a deprecated endpoint is NOT a breaking change.
+| `strictValidation` | `true` | When `false`, schemas without an explicit `type` field log a warning instead of failing. Set to `false` for Fineract because the generated spec has many type-less schemas.
+| `excludedPaths` | `[]` | List of path prefixes to skip (e.g. `['/v1/smscampaigns', '/v1/internal']`). Useful for excluding endpoints undergoing cleanup.
+| `ignoredBreakingChangeRules` | `[]` | List of rule codes to suppress entirely (e.g. `['R001']`).
+| `betaApiExtensionName` | `x-beta-api` | Vendor extension name for marking beta APIs. Beta endpoints can be freely modified without triggering violations.
+| `maxLogSerializationDepth` | `3` | Controls nested object serialization depth in logs (range 1-20). Increase if you see `StackOverflowError` from circular schema references.
+|===
+
+== Running Locally
+
+To run the check locally, you need a baseline spec to compare against:
+
+[source,bash]
+----
+# 1. Generate the baseline from develop
+git stash
+git checkout develop
+./gradlew :fineract-provider:resolve --no-daemon
+cp fineract-provider/build/resources/main/static/fineract.json /tmp/baseline.json
+git checkout -
+git stash pop
+
+# 2. Generate your current spec and compare
+./gradlew :fineract-provider:checkBreakingChanges \
+ -PapiBaseline="/tmp/baseline.json" \
+ --no-daemon
+----
+
+The JSON report is written to `fineract-provider/build/swagger-brake/`.
+
+== Handling Breaking Changes
+
+=== Intentional Breaking Changes
+
+If your PR intentionally introduces a breaking API change (e.g. removing a deprecated field):
+
+. The workflow will fail and report the violations.
+. Document the breaking change in the PR description with justification.
+. A committer will review and approve the PR with the understanding that the API contract is changing.
+
+=== Excluding Paths Under Cleanup
+
+If you need to fix incorrect API annotations on endpoints that are not yet stable, use `excludedPaths` to temporarily exclude them from checking:
+
+[source,groovy]
+----
+swaggerBrake {
+ excludedPaths = [
+ '/v1/smscampaigns',
+ '/v1/email',
+ ]
+}
+----
+
+Path exclusion is **prefix-based** — excluding `/v1/smscampaigns` will skip all paths starting with that prefix.
+
+Remove the exclusion once the cleanup is complete.
+
+=== Marking Endpoints as Beta
+
+For endpoints that are experimental or under active development, mark them as beta in the Java code:
+
+[source,java]
+----
+@Operation(
+ summary = "...",
+ extensions = @Extension(
+ properties = @ExtensionProperty(name = "x-beta-api", value = "true")
+ )
+)
+----
+
+Beta endpoints can be freely modified, created, or removed without triggering violations. Promoting a beta endpoint to stable (removing the extension) is also non-breaking. However, demoting a stable endpoint to beta **is** a breaking change (R001).
+
+== Report Format
+
+When breaking changes are detected, the workflow produces a deduplicated summary table:
+
+[cols="1,2,1,3,1", options="header"]
+|===
+| Rule | Description | Detail | Affected endpoints | Count
+| R014 | Response attribute removed | `totalOverpaid` | `GET /v1/loans`, `GET /v1/loans/{loanId}`, `GET /v1/loans/external-id/{loanExternalId}` | 3
+|===
+
+The deduplication groups violations by rule code and affected attribute, collapsing multiple endpoint occurrences into a single row. This is important because a single schema change (e.g. removing a field from a shared response type) can generate dozens of raw violations — one per endpoint that uses that schema.
+
+== Tool Reference
+
+* **Tool**: https://github.com/docktape/swagger-brake[swagger-brake] v2.7.0
+* **Gradle plugin**: `com.docktape.swagger-brake`
+* **Documentation**: https://docktape.github.io/swagger-brake/
+* **License**: Apache 2.0
diff --git a/fineract-doc/src/docs/en/chapters/architecture/index.adoc b/fineract-doc/src/docs/en/chapters/architecture/index.adoc
index 01a635c6f91..212f8b452f5 100644
--- a/fineract-doc/src/docs/en/chapters/architecture/index.adoc
+++ b/fineract-doc/src/docs/en/chapters/architecture/index.adoc
@@ -35,3 +35,5 @@ include::business-date.adoc[leveloffset=+1]
include::reliable-event-framework.adoc[leveloffset=+1]
include::advanced-payment-allocation.adoc[leveloffset=+1]
+
+include::api-backward-compatibility.adoc[leveloffset=+1]
diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc
index d718f19cbf4..5c386f1e1e7 100644
--- a/fineract-doc/src/docs/en/chapters/features/index.adoc
+++ b/fineract-doc/src/docs/en/chapters/features/index.adoc
@@ -15,3 +15,4 @@ include::pause-delinquency.adoc[leveloffset=+1]
include::re-amortization.adoc[leveloffset=+1]
include::re-ageing.adoc[leveloffset=+1]
include::delayed-schedule-captures.adoc[leveloffset=+1]
+include::loan-origination-details.adoc[leveloffset=+1]
diff --git a/fineract-doc/src/docs/en/chapters/features/loan-origination-details.adoc b/fineract-doc/src/docs/en/chapters/features/loan-origination-details.adoc
new file mode 100644
index 00000000000..81c0d310588
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/features/loan-origination-details.adoc
@@ -0,0 +1,494 @@
+= Loan Origination Details
+
+== Overview
+
+Tracks the originator of a loan (merchant, broker, affiliate, platform, or channel) for revenue sharing and reporting. Originator details are propagated through business events and reporting.
+
+== Configuration
+
+=== Enabling the Module
+
+[source,properties]
+----
+fineract.module.loan-origination.enabled=${FINERACT_MODULE_LOAN_ORIGINATION_ENABLED:true}
+----
+
+**Enabled by default.** When disabled, API endpoints become unavailable, event enrichment is skipped, and the loan creation flow continues to work without originator processing.
+
+=== Global Configuration
+
+[cols="1,1,1", options="header"]
+|===
+| Configuration Key | Default | Description
+
+| `enable-originator-creation-during-loan-application`
+| `false`
+| Allows automatic creation of new originator records when an unknown `externalId` is provided during loan creation
+|===
+
+== Data Model
+
+=== Originator Registry (`m_loan_originator`)
+
+[cols="1,1,1,1", options="header"]
+|===
+| Column | Type | Required | Description
+
+| `id`
+| BIGINT (PK)
+| Yes
+| Auto-generated primary key
+
+| `external_id`
+| VARCHAR(100)
+| Yes
+| Unique, immutable external identifier (a.k.a. Revenue Share ID)
+
+| `name`
+| VARCHAR(255)
+| No
+| Originator display name
+
+| `status`
+| VARCHAR(20)
+| Yes
+| `ACTIVE`, `PENDING`, or `INACTIVE`
+
+| `originator_type_cv_id`
+| INT (FK)
+| No
+| Code value reference to `LoanOriginatorType`
+
+| `channel_type_cv_id`
+| INT (FK)
+| No
+| Code value reference to `LoanOriginationChannelType`
+
+| `created_on_utc`
+| DATETIME
+| Yes
+| Record creation timestamp (UTC)
+
+| `created_by`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_appuser` -- user who created the record
+
+| `last_modified_on_utc`
+| DATETIME
+| Yes
+| Last modification timestamp (UTC)
+
+| `last_modified_by`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_appuser` -- user who last modified the record
+|===
+
+=== Loan-Originator Mapping (`m_loan_originator_mapping`)
+
+Associates loans with originators. Supports multiple originators per loan, though typical usage is one.
+
+[cols="1,1,1,1", options="header"]
+|===
+| Column | Type | Required | Description
+
+| `id`
+| BIGINT (PK)
+| Yes
+| Auto-generated primary key
+
+| `loan_id`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_loan`
+
+| `originator_id`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_loan_originator`
+
+| `created_on_utc`
+| DATETIME
+| Yes
+| Record creation timestamp (UTC)
+
+| `created_by`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_appuser` -- user who created the record
+
+| `last_modified_on_utc`
+| DATETIME
+| Yes
+| Last modification timestamp (UTC)
+
+| `last_modified_by`
+| BIGINT (FK)
+| Yes
+| Foreign key to `m_appuser` -- user who last modified the record
+|===
+
+A unique constraint on `(loan_id, originator_id)` prevents duplicate assignments.
+
+=== Code Values
+
+**`LoanOriginatorType`** (default values): `MERCHANT`, `BROKER`, `AFFILIATE`, `PLATFORM`
+
+**`LoanOriginationChannelType`** (default values): `ONLINE`, `IN_STORE`, `API`, `AGGREGATOR`
+
+Both code value sets are extensible -- additional values can be added via the standard Code Values API.
+
+== API Endpoints
+
+=== Originator Registry APIs
+
+==== Create a Loan Originator
+
+`POST /v1/loan-originators`
+
+**Permission**: `CREATE_LOAN_ORIGINATOR`
+
+[source,json]
+----
+{
+ "name": "Best Merchant in US",
+ "externalId": "best-merchant-us-east",
+ "status": "ACTIVE",
+ "originatorTypeId": 12,
+ "channelTypeId": 44
+}
+----
+
+* `externalId` -- **required**, unique, max 100 characters
+* `name` -- optional, max 255 characters
+* `status` -- optional, defaults to `ACTIVE`. Allowed values: `ACTIVE`, `PENDING`, `INACTIVE`
+* `originatorTypeId` -- optional, must reference a valid `LoanOriginatorType` code value
+* `channelTypeId` -- optional, must reference a valid `LoanOriginationChannelType` code value
+
+**Response:**
+
+[source,json]
+----
+{
+ "resourceId": 13,
+ "resourceExternalId": "best-merchant-us-east"
+}
+----
+
+|===
+| HTTP Code | Description
+
+| 200 | Created successfully
+| 400 | Required parameter missing or incorrect format
+| 403 | Duplicate external ID or insufficient permissions
+| 404 | Originator type or channel type does not exist
+|===
+
+==== List All Loan Originators
+
+`GET /v1/loan-originators`
+
+**Permission**: `READ_LOAN_ORIGINATOR`
+
+[source,json]
+----
+[
+ {
+ "id": 13,
+ "externalId": "best-merchant-us-east",
+ "name": "Best Merchant in US",
+ "status": "ACTIVE",
+ "originatorType": {
+ "id": 12,
+ "name": "MERCHANT",
+ "active": true,
+ "mandatory": false
+ },
+ "channelType": {
+ "id": 44,
+ "name": "ONLINE",
+ "active": true,
+ "mandatory": false
+ }
+ }
+]
+----
+
+==== Retrieve a Loan Originator
+
+`GET /v1/loan-originators/{originatorId}` +
+`GET /v1/loan-originators/external-id/{externalId}`
+
+**Permission**: `READ_LOAN_ORIGINATOR`
+
+==== Get Template Data
+
+`GET /v1/loan-originators/template`
+
+**Permission**: `READ_LOAN_ORIGINATOR`
+
+Returns a pre-generated `externalId`, available status values, and code value options for originator type and channel type.
+
+==== Update a Loan Originator
+
+`PUT /v1/loan-originators/{originatorId}` +
+`PUT /v1/loan-originators/external-id/{externalId}`
+
+**Permission**: `UPDATE_LOAN_ORIGINATOR`
+
+[source,json]
+----
+{
+ "status": "PENDING"
+}
+----
+
+Updatable fields: `name`, `status`, `originatorTypeId`, `channelTypeId`. Only changed fields need to be included.
+
+**Response:**
+
+[source,json]
+----
+{
+ "resourceId": 13,
+ "resourceExternalId": "best-merchant-us-east",
+ "changes": {
+ "status": "PENDING"
+ }
+}
+----
+
+|===
+| HTTP Code | Description
+
+| 200 | Updated successfully
+| 400 | Unsupported parameter (e.g. `externalId`) or incorrect format
+| 404 | Originator not found
+|===
+
+[IMPORTANT]
+====
+The `externalId` field **cannot be updated** after creation.
+====
+
+==== Delete a Loan Originator
+
+`DELETE /v1/loan-originators/{originatorId}` +
+`DELETE /v1/loan-originators/external-id/{externalId}`
+
+**Permission**: `DELETE_LOAN_ORIGINATOR`
+
+|===
+| HTTP Code | Description
+
+| 200 | Deleted successfully
+| 403 | Originator is currently mapped to one or more loans
+| 404 | Originator not found
+|===
+
+[IMPORTANT]
+====
+An originator cannot be deleted if it is currently mapped to any loans.
+====
+
+=== Loan-Originator Mapping APIs
+
+==== Attach Originator to Loan
+
+`POST /v1/loans/{loanId}/originators/{originatorId}` +
+`POST /v1/loans/{loanId}/originators/external-id/{originatorExternalId}` +
+`POST /v1/loans/external-id/{loanExternalId}/originators/{originatorId}` +
+`POST /v1/loans/external-id/{loanExternalId}/originators/external-id/{originatorExternalId}`
+
+**Permission**: `ATTACH_LOAN_ORIGINATOR`
+
+No request body.
+
+**Response:**
+
+[source,json]
+----
+{
+ "loanId": 45,
+ "loanExternalId": "11793428-12cb-42fe-ab9f-72b4ddf2453a",
+ "originatorId": 13,
+ "originatorExternalId": "best-merchant-us-east"
+}
+----
+
+|===
+| HTTP Code | Description
+
+| 200 | Attached successfully
+| 403 | Loan is not in Submitted and Pending Approval status, originator is not ACTIVE, or mapping already exists
+| 404 | Loan or originator not found
+|===
+
+==== Detach Originator from Loan
+
+`DELETE /v1/loans/{loanId}/originators/{originatorId}` +
+`DELETE /v1/loans/{loanId}/originators/external-id/{originatorExternalId}` +
+`DELETE /v1/loans/external-id/{loanExternalId}/originators/{originatorId}` +
+`DELETE /v1/loans/external-id/{loanExternalId}/originators/external-id/{originatorExternalId}`
+
+**Permission**: `DETACH_LOAN_ORIGINATOR`
+
+No request body. Response format is the same as Attach.
+
+|===
+| HTTP Code | Description
+
+| 200 | Detached successfully
+| 403 | Loan is not in Submitted and Pending Approval status
+| 404 | Loan, originator, or mapping not found
+|===
+
+==== Retrieve Originators for a Loan
+
+`GET /v1/loans/{loanId}/originators` +
+`GET /v1/loans/external-id/{loanExternalId}/originators`
+
+**Permission**: `READ_LOAN`
+
+[source,json]
+----
+{
+ "originators": [
+ {
+ "id": 13,
+ "externalId": "best-merchant-us-east",
+ "name": "Best Merchant in US",
+ "status": "ACTIVE",
+ "originatorType": {
+ "id": 12,
+ "name": "MERCHANT",
+ "active": true,
+ "mandatory": false
+ },
+ "channelType": {
+ "id": 44,
+ "name": "ONLINE",
+ "active": true,
+ "mandatory": false
+ }
+ }
+ ]
+}
+----
+
+==== Originators via Retrieve Loan API
+
+Originator details can also be fetched as part of the standard Retrieve Loan API using the `associations` query parameter:
+
+`GET /v1/loans/{loanId}?associations=originators` +
+`GET /v1/loans/external-id/{loanExternalId}?associations=originators`
+
+**Permission**: `READ_LOAN`
+
+The `originators` association is also included when `associations=all` is used. Since the `associations` parameter defaults to `all`, originator data is included in the Retrieve Loan response by default (even without an explicit `associations` parameter). The response adds an `originators` field to the loan object with the same structure as the dedicated endpoint above.
+
+==== Attach/Detach Validation Rules
+
+[IMPORTANT]
+====
+* Attach and detach are **only allowed while the loan is in Submitted and Pending Approval status**
+* The same originator **cannot be attached twice** to the same loan
+* The same originator **cannot be detached twice** (returns 404 if mapping does not exist)
+* Only originators with `ACTIVE` status can be **attached** (no status restriction for detach)
+====
+
+=== Inline Originator Creation During Loan Application
+
+Originators can be provided as part of the loan creation request (`POST /v1/loans`):
+
+[source,json]
+----
+{
+ "...": "...",
+ "originators": [
+ {
+ "id": 1,
+ "externalId": "XYZ",
+ "name": "PP Merchant",
+ "originatorTypeId": 1,
+ "channelTypeId": 2
+ }
+ ]
+}
+----
+
+* `id` or `externalId` is mandatory for each entry
+* If `id` is provided: attaches the existing originator (lookup by `id` takes priority over `externalId`)
+* If only `externalId` is provided:
+** Attaches the existing originator if found
+** Creates a new originator and attaches it (only when `enable-originator-creation-during-loan-application` is `true`)
+** Returns **403** if originator is not found and creation is not enabled
+* `name`, `originatorTypeId`, `channelTypeId` are optional, used only when creating a new entry
+* Newly created originators are automatically assigned `ACTIVE` status
+* Duplicate originators within the same request are silently skipped
+
+== Business Events Integration
+
+Originator details are automatically included in all loan and loan transaction external business events.
+
+An `OriginatorDetailsV1` Avro record is added as an optional `originators` field (list) to:
+
+* `LoanAccountDataV1.avsc` -- all loan-centric events
+* `LoanTransactionDataV1.avsc` -- all loan transaction-centric events
+
+[source,json]
+----
+{
+ "name": "OriginatorDetailsV1",
+ "fields": [
+ {"name": "id", "type": ["null", "long"]},
+ {"name": "externalId", "type": ["null", "string"]},
+ {"name": "name", "type": ["null", "string"]},
+ {"name": "status", "type": ["null", "string"]},
+ {"name": "originatorType", "type": ["null", "CodeValueDataV1"]},
+ {"name": "channelType", "type": ["null", "CodeValueDataV1"]}
+ ]
+}
+----
+
+The field is **optional with default `null`**, preserving backward compatibility for existing event consumers.
+
+On any loan or loan transaction event publication, the enricher fetches originator mappings for the loan, builds the `originators` list from the registry, and attaches it to the event payload.
+
+== Security and Permissions
+
+[cols="1,1", options="header"]
+|===
+| Permission | Description
+
+| `CREATE_LOAN_ORIGINATOR`
+| Create originator records
+
+| `READ_LOAN_ORIGINATOR`
+| View originator registry records and template data
+
+| `READ_LOAN`
+| View loan-originator associations (`GET /v1/loans/.../originators`)
+
+| `UPDATE_LOAN_ORIGINATOR`
+| Modify originator records
+
+| `DELETE_LOAN_ORIGINATOR`
+| Delete originator records (only if not mapped to loans)
+
+| `ATTACH_LOAN_ORIGINATOR`
+| Attach an originator to a loan
+
+| `DETACH_LOAN_ORIGINATOR`
+| Detach an originator from a loan
+|===
+
+== Reporting
+
+Originator external IDs are included in the following stretchy reports:
+
+* **Transaction Summary Report**
+* **Trial Balance Report**
+
+Report queries join `m_loan_originator_mapping` and `m_loan_originator` via a CTE. Multiple originators per loan are aggregated into a comma-separated string (`STRING_AGG` on PostgreSQL, `GROUP_CONCAT` on MySQL). The resulting column is `originator_external_ids`. Loans without originators have `NULL` in this column.
diff --git a/fineract-doc/src/docs/en/chapters/testing/cucumber.adoc b/fineract-doc/src/docs/en/chapters/testing/cucumber.adoc
index e9e876525c4..449219d91eb 100644
--- a/fineract-doc/src/docs/en/chapters/testing/cucumber.adoc
+++ b/fineract-doc/src/docs/en/chapters/testing/cucumber.adoc
@@ -24,7 +24,7 @@ Apache Fineract's E2E test suite provides comprehensive coverage of business fun
=== Required Software
* *Java 21*: Apache Fineract requires Java 21 (Azul Zulu JDK recommended)
-* *Database*: MariaDB 11.5.2, PostgreSQL 17.4, or MySQL 9.1
+* *Database*: MariaDB 12.2, PostgreSQL 17.4, or MySQL 9.1
* *Git*: For source code management
* *Gradle 8.14.3*: Included via wrapper
@@ -149,12 +149,15 @@ mysql -u root -pmysql fineract_default -e \
Method 2 - Via API (After Fineract is Running):
[source,bash]
----
-curl -X PUT https://localhost:8443/fineract-provider/api/v1/configurations/name/enable-business-date \
+curl -k -X PUT https://localhost:8443/fineract-provider/api/v1/configurations/name/enable-business-date \
-H "Authorization: Basic bWlmb3M6cGFzc3dvcmQ=" \
+ -H "Fineract-Platform-TenantId: default" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
----
+NOTE: The `Fineract-Platform-TenantId` header is required. Without it, the request can fail with HTTP 400 because tenant context is missing.
+
*Verification*:
[source,bash]
----
@@ -170,10 +173,12 @@ mysql -u root -pmysql fineract_default -e \
[source,bash]
----
-# Start Fineract in background
-./gradlew bootRun
+# Start Fineract with test profile enabled for E2E
+./gradlew bootRun -Dspring.profiles.active=test
----
+IMPORTANT: When running E2E tests that hit endpoints/APIs backed by beans annotated with `@Profile(FineractProfiles.TEST)`, the provider startup must include `-Dspring.profiles.active=test`. Without this, test-profile-only components are not loaded.
+
Wait for Fineract to be fully started. You can verify by checking:
[source,bash]
----
diff --git a/fineract-doc/src/docs/en/chapters/testing/integration.adoc b/fineract-doc/src/docs/en/chapters/testing/integration.adoc
index 2df8236d42a..15c1ae6de2e 100644
--- a/fineract-doc/src/docs/en/chapters/testing/integration.adoc
+++ b/fineract-doc/src/docs/en/chapters/testing/integration.adoc
@@ -26,7 +26,7 @@ Integration tests in Apache Fineract validate the complete API layer and busines
=== Required Software
* *Java 21*: Apache Fineract requires Java 21 (Azul Zulu JDK recommended)
-* *Database*: MariaDB 11.5.2, PostgreSQL 17.4, or MySQL 9.1
+* *Database*: MariaDB 12.2, PostgreSQL 17.4, or MySQL 9.1
* *Git*: For source code management
* *Gradle 8.14.3*: Included via wrapper
* *12GB RAM*: Recommended for test execution
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
index b29b993462b..f72b6350d87 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java
@@ -197,7 +197,8 @@ public ImageCreateResponse createImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE
@PUT
@Consumes(MediaType.MULTIPART_FORM_DATA)
- @RequestBody(description = "Update image", content = { @Content(mediaType = MediaType.MULTIPART_FORM_DATA) })
+ @RequestBody(description = "Update image", content = {
+ @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @io.swagger.v3.oas.annotations.media.Schema(type = "object")) })
public ImageCreateResponse updateImage(@PathParam(DOCUMENT_API_PARAM_ENTITY_TYPE) final String entityName,
@PathParam(DOCUMENT_API_PARAM_ENTITY_ID) final Long entityId, @HeaderParam(CONTENT_LENGTH) final Long fileSize,
@FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream inputStream,
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
index 578201f1d15..c05c7d5ef52 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/exception/DocumentNotFoundException.java
@@ -18,9 +18,9 @@
*/
package org.apache.fineract.infrastructure.documentmanagement.exception;
-import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
-public class DocumentNotFoundException extends AbstractPlatformDomainRuleException {
+public class DocumentNotFoundException extends AbstractPlatformResourceNotFoundException {
public DocumentNotFoundException(final Long id) {
super("error.msg.document.id.invalid", "Document with identifier " + id + " does not exist", id);
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
index 056a1112260..7c64d18a9dc 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java
@@ -28,7 +28,6 @@
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentContent;
import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
-import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentInvalidRequestException;
import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
import org.springframework.stereotype.Service;
@@ -45,25 +44,15 @@ public class ImageReadPlatformServiceImpl implements ImageReadPlatformService {
@Override
public DocumentContent retrieveImage(final String entityType, final Long entityId) {
- try {
- return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
- .flatMap(imageIdAdapter -> imageIdAdapter.get(entityId))
- .flatMap(
- imageIdResult -> imageRepository.findById(imageIdResult.getId())
- .map(image -> DocumentContent
- .builder().fileName(
- FilenameUtils.getName(image.getLocation()))
- .format(FilenameUtils.getExtension(image.getLocation()))
- .displayName(imageIdResult.getDisplayName())
- .contentType(
- contentDetectorManager
- .detect(ContentDetectorContext.builder()
- .fileName(FilenameUtils.getName(image.getLocation())).build())
- .getMimeType())
- .stream(storeService.download(image.getLocation())).build()))
- .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, -1L));
- } catch (final Exception e) {
- throw new DocumentInvalidRequestException(e);
- }
+ return imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(entityType)).findFirst()
+ .flatMap(imageIdAdapter -> imageIdAdapter.get(entityId))
+ .flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId()).map(image -> DocumentContent.builder()
+ .fileName(FilenameUtils.getName(image.getLocation())).format(FilenameUtils.getExtension(image.getLocation()))
+ .displayName(imageIdResult.getDisplayName())
+ .contentType(contentDetectorManager
+ .detect(ContentDetectorContext.builder().fileName(FilenameUtils.getName(image.getLocation())).build())
+ .getMimeType())
+ .stream(storeService.download(image.getLocation())).build()))
+ .orElseThrow(() -> new DocumentNotFoundException(entityType, entityId, -1L));
}
}
diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
index 1538788bee5..6d25a80bda6 100644
--- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
+++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImpl.java
@@ -65,6 +65,7 @@ public ImageCreateResponse createImage(final ImageCreateRequest request) {
// TODO: keeping the path segment always "clients" not consistent how this works with documents
request.setEntityType(DEFAULT_ENTITY_TYPE);
}
+ final var imageEntityType = normalizeImageEntityType(request.getEntityType());
if (StringUtils.isEmpty(request.getFileName())) {
// NOTE: defacto limiting the uploads to JPEG files, same behavior as before
@@ -83,12 +84,11 @@ public ImageCreateResponse createImage(final ImageCreateRequest request) {
}
// TODO: make "prefix" configurable?
- var path = getPath(STORE_PREFIX, request.getEntityType(), request.getEntityId(), request.getFileName(),
- storeService.getDelimiter());
+ var path = getPath(STORE_PREFIX, imageEntityType, request.getEntityId(), request.getFileName(), storeService.getDelimiter());
final var imagePath = storeService.upload(path, request.getStream(), request.getType());
- final var result = imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
+ final var result = imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(imageEntityType)).findFirst()
.flatMap(imageIdAdapter -> imageIdAdapter.get(request.getEntityId()))
.flatMap(imageIdResult -> imageRepository.findById(imageIdResult.getId())).map(image -> {
// delete old image
@@ -99,7 +99,7 @@ public ImageCreateResponse createImage(final ImageCreateRequest request) {
.map(image -> image.setLocation(imagePath).setStorageType(storeService.getType().getValue())).map(imageRepository::save)
.map(image -> ImageCreateResponse.builder().resourceId(image.getId()).build());
- imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(request.getEntityType())).findFirst()
+ imageIdAdapters.stream().filter(imageIdAdapter -> imageIdAdapter.accept(imageEntityType)).findFirst()
.ifPresent(imageIdAdapter -> result.ifPresent(
imageCreateResponse -> imageIdAdapter.set(request.getEntityId(), imageCreateResponse.getResourceId())));
@@ -135,6 +135,16 @@ private Optional delete(final String entityType, final Long
});
}
+ private String normalizeImageEntityType(final String entityType) {
+ if ("staff".equalsIgnoreCase(entityType)) {
+ return "staff";
+ }
+ if ("clients".equalsIgnoreCase(entityType)) {
+ return DEFAULT_ENTITY_TYPE;
+ }
+ return entityType;
+ }
+
private String getPath(final String prefix, final String entityType, final Long entityId, final String fileName, String delimiter) {
requireNonNull(prefix);
requireNonNull(entityType);
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImplTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImplTest.java
new file mode 100644
index 00000000000..dc214f6b497
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageReadPlatformServiceImplTest.java
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.documentmanagement.service;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
+import org.apache.fineract.infrastructure.documentmanagement.exception.DocumentNotFoundException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ImageReadPlatformServiceImplTest {
+
+ @Mock
+ private ImageRepository imageRepository;
+ @Mock
+ private ContentStoreService storeService;
+ @Mock
+ private ContentDetectorManager contentDetectorManager;
+ @Mock
+ private EntityImageIdAdapter imageIdAdapter;
+
+ private ImageReadPlatformServiceImpl imageReadPlatformService;
+
+ @BeforeEach
+ void setUp() {
+ List imageIdAdapters = Collections.singletonList(imageIdAdapter);
+ imageReadPlatformService = new ImageReadPlatformServiceImpl(imageIdAdapters, storeService, imageRepository, contentDetectorManager);
+ }
+
+ @Test
+ void testRetrieveImage_NotFound_ThrowsDocumentNotFoundException() {
+ // Arrange
+ String entityType = "clients";
+ Long entityId = 1L;
+
+ when(imageIdAdapter.accept(anyString())).thenReturn(true);
+ when(imageIdAdapter.get(entityId)).thenReturn(Optional.empty());
+
+ // Act & Assert
+ assertThrows(DocumentNotFoundException.class, () -> {
+ imageReadPlatformService.retrieveImage(entityType, entityId);
+ });
+ }
+}
diff --git a/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImplTest.java b/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImplTest.java
new file mode 100644
index 00000000000..a3c71c2c8a9
--- /dev/null
+++ b/fineract-document/src/test/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceImplTest.java
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.documentmanagement.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.infrastructure.contentstore.data.ContentStoreType;
+import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
+import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
+import org.apache.fineract.infrastructure.documentmanagement.adapter.EntityImageIdAdapter;
+import org.apache.fineract.infrastructure.documentmanagement.data.ImageCreateRequest;
+import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
+import org.apache.fineract.infrastructure.documentmanagement.domain.ImageRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ImageWritePlatformServiceImplTest {
+
+ @Mock
+ private EntityImageIdAdapter clientImageIdAdapter;
+ @Mock
+ private EntityImageIdAdapter staffImageIdAdapter;
+ @Mock
+ private ContentStoreService contentStoreService;
+ @Mock
+ private ImageRepository imageRepository;
+ @Mock
+ private ContentDetectorManager contentDetectorManager;
+
+ private ImageWritePlatformServiceImpl underTest;
+
+ @BeforeEach
+ void setUp() {
+ underTest = new ImageWritePlatformServiceImpl(List.of(clientImageIdAdapter, staffImageIdAdapter), contentStoreService,
+ imageRepository, contentDetectorManager);
+
+ when(contentStoreService.getDelimiter()).thenReturn("/");
+ when(contentStoreService.getType()).thenReturn(ContentStoreType.FILE_SYSTEM);
+ when(contentStoreService.upload(anyString(), any(InputStream.class), anyString()))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+ when(imageRepository.save(any(Image.class))).thenAnswer(invocation -> {
+ Image image = invocation.getArgument(0);
+ image.setId(99L);
+ return image;
+ });
+ }
+
+ @Test
+ void createImageShouldStoreStaffImageUnderStaffDirectory() {
+ Long entityId = 7L;
+ when(staffImageIdAdapter.accept("staff")).thenReturn(true);
+ when(staffImageIdAdapter.get(entityId)).thenReturn(Optional.empty());
+ when(staffImageIdAdapter.set(eq(entityId), anyLong())).thenReturn(Optional.empty());
+ when(clientImageIdAdapter.accept(anyString())).thenReturn(false);
+
+ ImageCreateRequest request = ImageCreateRequest.builder().entityType("STAFF").entityId(entityId).fileName("profile.png")
+ .type("image/png").stream(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).build();
+
+ var response = underTest.createImage(request);
+
+ ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(String.class);
+ verify(contentStoreService).upload(pathCaptor.capture(), any(InputStream.class), eq("image/png"));
+ assertEquals("images/staff/7/profile.png", pathCaptor.getValue());
+ assertEquals(99L, response.getResourceId());
+ }
+
+ @Test
+ void createImageShouldStoreClientImageUnderClientsDirectory() {
+ Long entityId = 7L;
+ when(clientImageIdAdapter.accept("clients")).thenReturn(true);
+ when(clientImageIdAdapter.get(entityId)).thenReturn(Optional.empty());
+ when(clientImageIdAdapter.set(eq(entityId), anyLong())).thenReturn(Optional.empty());
+
+ ImageCreateRequest request = ImageCreateRequest.builder().entityType("CLIENTS").entityId(entityId).fileName("profile.png")
+ .type("image/png").stream(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).build();
+
+ var response = underTest.createImage(request);
+
+ ArgumentCaptor pathCaptor = ArgumentCaptor.forClass(String.class);
+ verify(contentStoreService).upload(pathCaptor.capture(), any(InputStream.class), eq("image/png"));
+ assertEquals("images/clients/7/profile.png", pathCaptor.getValue());
+ assertEquals(99L, response.getResourceId());
+ }
+}
diff --git a/fineract-e2e-tests-core/build.gradle b/fineract-e2e-tests-core/build.gradle
index d4cb2af8f43..53c3ff7e6ff 100644
--- a/fineract-e2e-tests-core/build.gradle
+++ b/fineract-e2e-tests-core/build.gradle
@@ -99,6 +99,10 @@ dependencies {
testImplementation 'io.github.classgraph:classgraph:4.8.179'
testImplementation 'org.apache.commons:commons-collections4:4.4'
+
+ testImplementation 'org.springframework:spring-jdbc'
+ testImplementation 'org.postgresql:postgresql'
+ testImplementation 'org.mariadb.jdbc:mariadb-java-client'
}
tasks.withType(JavaCompile).configureEach {
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java
new file mode 100644
index 00000000000..909501b7f76
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/TestDatabaseConfiguration.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.config;
+
+import javax.sql.DataSource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+@Configuration
+@Slf4j
+public class TestDatabaseConfiguration {
+
+ @Value("${fineract-test.db.protocol}")
+ private String protocol;
+
+ @Value("${fineract-test.db.hostname}")
+ private String hostname;
+
+ @Value("${fineract-test.db.port}")
+ private String port;
+
+ @Value("${fineract-test.db.name}")
+ private String dbName;
+
+ @Value("${fineract-test.db.username}")
+ private String username;
+
+ @Value("${fineract-test.db.password}")
+ private String password;
+
+ @Bean
+ public DataSource testDataSource() {
+ // DriverManagerDataSource creates a new connection per call (no pooling).
+ // This is intentional for lightweight e2e test usage — no pool management overhead needed.
+ DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ String url = protocol + "://" + hostname + ":" + port + "/" + dbName;
+ log.debug("Test database URL: {}", url);
+ dataSource.setUrl(url);
+ dataSource.setUsername(username);
+ dataSource.setPassword(password);
+ return dataSource;
+ }
+
+ @Bean
+ public JdbcTemplate testJdbcTemplate(DataSource testDataSource) {
+ return new JdbcTemplate(testDataSource);
+ }
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/ChargeProductType.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/ChargeProductType.java
index b84f47ccc18..11e570e9f7e 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/ChargeProductType.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/ChargeProductType.java
@@ -36,7 +36,8 @@ public enum ChargeProductType {
CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT(14L), //
LOAN_INSTALLMENT_FEE_FLAT(15L), //
LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT(16L), //
- LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST(17L); //
+ LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST(17L), //
+ LOAN_DISBURSEMENT_PERCENTAGE_AMOUNT_PLUS_INTEREST_FEE(18L); //
public final Long value;
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java
index 5a02805d7f0..c3d2e8c9308 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanStatus.java
@@ -27,6 +27,8 @@ public enum LoanStatus {
SUBMITTED_AND_PENDING_APPROVAL(100), //
APPROVED(200), //
ACTIVE(300), //
+ TRANSFER_IN_PROGRESS(303), //
+ TRANSFER_ON_HOLD(304), //
WITHDRAWN(400), //
REJECTED(500), //
CLOSED_OBLIGATIONS_MET(600), //
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/CodeValueResolver.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/CodeValueResolver.java
index 0c8409d5b57..fdb90f93a05 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/CodeValueResolver.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/CodeValueResolver.java
@@ -53,7 +53,7 @@ public long resolve(Long codeId, CodeValue codeValue) {
public long resolve(String codeName, String codeValue) {
log.debug("Resolving code value by code id and name [{}]", codeValue);
List codeValuesResponses = ok(
- () -> fineractClient.codeValues().retrieveAllCodeValues1(codeName, Map.of()));
+ () -> fineractClient.codeValues().retrieveAllCodeValuesByCodeName(codeName, Map.of()));
GetCodeValuesDataResponse foundPtr = codeValuesResponses.stream().filter(ptr -> codeValue.equals(ptr.getName())).findAny()
.orElseThrow(() -> new IllegalArgumentException("Code Value [%s] not found for Code [%s]".formatted(codeValue, codeName)));
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
index f9f97f312c4..347f3fca3a0 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
@@ -28,7 +28,8 @@ public enum DefaultJob implements Job {
ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting", "ACC_ACPO"), //
ADD_ACCRUAL_TRANSACTIONS_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions", "LA_AATR"), //
- RECALCULATE_INTEREST_FOR_LOANS("Recalculate Interest For Loans", "LA_RINT"); //
+ RECALCULATE_INTEREST_FOR_LOANS("Recalculate Interest For Loans", "LA_RINT"), //
+ WORKING_CAPITAL_LOAN_COB("Working Capital Loan COB", "WC_COB"); //
private final String customName;
private final String shortName;
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index 031e8cc5c46..6bbe66d4fba 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -190,6 +190,8 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_FULL_TERM_TRANCHE_CHARGEBACK, //
LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_FULL_TERM_TRANCHE_ZERO_INT_CHARGE_OFF, //
LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_FULL_TERM_TRANCHE_ACCELERATE_MATURITY, //
+ LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES_FEE_INCOME, //
+ LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL_INTEREST_RECALC, //
;
@Override
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java
new file mode 100644
index 00000000000..bb35365fc0b
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.data.workingcapitalproduct;
+
+public enum DefaultWorkingCapitalLoanProduct implements WorkingCapitalLoanProduct {
+
+ WCLP, //
+ WCLP_FOR_UPDATE; //
+
+ @Override
+ public String getName() {
+ return name();
+ }
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/WorkingCapitalLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/WorkingCapitalLoanProduct.java
new file mode 100644
index 00000000000..048139d0edf
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/WorkingCapitalLoanProduct.java
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.data.workingcapitalproduct;
+
+public interface WorkingCapitalLoanProduct {
+
+ String getName();
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java
index 9759148adec..ba11f3a75b0 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java
@@ -1770,6 +1770,11 @@ public PostLoanProductsRequest defaultLoanProductsRequestLP2BuyDownFees() {
.incomeFromBuyDownAccountId(accountTypeResolver.resolve(DefaultAccountType.INCOME_FROM_BUY_DOWN));//
}
+ public PostLoanProductsRequest defaultLoanProductsRequestLP2BuyDownFeesFeeIncome() {
+ return defaultLoanProductsRequestLP2BuyDownFees()//
+ .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE);//
+ }
+
public PostLoanProductsRequest defaultLoanProductsRequestLP2ChargeOffReasonToExpenseAccountMappingsWithBuyDownFee() {
Long chargeOffReasonId = codeHelper.retrieveCodeByName(CHARGE_OFF_REASONS).getId();
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanRequestFactory.java
index 77c3089fe1b..f3c2d4385e8 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanRequestFactory.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanRequestFactory.java
@@ -120,7 +120,7 @@ public PostLoansRequest defaultLoansRequest(Long clientId) {
.transactionProcessingStrategyCode(DEFAULT_TRANSACTION_PROCESSING_STRATEGY_CODE)//
.dateFormat(DATE_FORMAT)//
.graceOnArrearsAgeing(3)//
- .maxOutstandingLoanBalance(new BigDecimal(10000));
+ ;
}
public PostLoansRequest defaultProgressiveLoansRequest(final Long clientId) {
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java
new file mode 100644
index 00000000000..22817033afd
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.factory;
+
+import static org.apache.fineract.test.data.DaysInYearType.DAYS365;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.CURRENCY_CODE;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.CURRENCY_CODE_USD;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DATE_FORMAT;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DAYS_IN_MONTH_TYPE_30;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DAYS_IN_YEAR_TYPE_360;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DELINQUENCY_BUCKET_ID;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.FUND_ID;
+import static org.apache.fineract.test.factory.LoanProductsRequestFactory.LOCALE_EN;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.client.models.PaymentAllocationOrder;
+import org.apache.fineract.client.models.PostAllowAttributeOverrides;
+import org.apache.fineract.client.models.PostPaymentAllocation;
+import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest;
+import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest;
+import org.apache.fineract.test.helper.Utils;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class WorkingCapitalRequestFactory {
+
+ private final LoanProductsRequestFactory loanProductsRequestFactory;
+
+ public static final String WCLP_NAME_PREFIX = "WCLP-";
+ public static final String WCLP_DESCRIPTION = "Working Capital Loan Product";
+ public static final String PENALTY = "PENALTY";
+ public static final String FEE = "FEE";
+ public static final String PRINCIPAL = "PRINCIPAL";
+
+ public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductRequest() {
+ String name = Utils.randomStringGenerator(WCLP_NAME_PREFIX, 10);
+ String shortName = loanProductsRequestFactory.generateShortNameSafely();
+
+ return new PostWorkingCapitalLoanProductsRequest()//
+ .name(name)//
+ .shortName(shortName)//
+ .description(WCLP_DESCRIPTION)//
+ .fundId(FUND_ID)//
+ .periodPaymentRate(new BigDecimal(1))//
+ .repaymentFrequencyType(PostWorkingCapitalLoanProductsRequest.RepaymentFrequencyTypeEnum.DAYS)//
+ .repaymentEvery(DAYS_IN_MONTH_TYPE_30)//
+ .startDate(null)//
+ .closeDate(null)//
+ .currencyCode(CURRENCY_CODE)//
+ .digitsAfterDecimal(2)//
+ .inMultiplesOf(1)//
+ .principal(new BigDecimal(100))//
+ .minPrincipal(new BigDecimal(10))//
+ .maxPrincipal(new BigDecimal(100000))//
+ .amortizationType(PostWorkingCapitalLoanProductsRequest.AmortizationTypeEnum.EIR)//
+ .npvDayCount(DAYS_IN_YEAR_TYPE_360)//
+ .delinquencyBucketId(DELINQUENCY_BUCKET_ID.longValue())//
+ .dateFormat(DATE_FORMAT)//
+ .locale(LOCALE_EN)//
+ .paymentAllocation(List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(),
+ List.of(PENALTY, FEE, PRINCIPAL))));//
+ }
+
+ public PutWorkingCapitalLoanProductsProductIdRequest defaultWorkingCapitalLoanProductRequestUpdate() {
+ String name = Utils.randomStringGenerator(WCLP_NAME_PREFIX, 10);
+ String shortName = loanProductsRequestFactory.generateShortNameSafely();
+
+ PostAllowAttributeOverrides allowAttributeOverrides = new PostAllowAttributeOverrides().delinquencyBucketClassification(true)
+ .discountDefault(false).flatPercentageAmount(true).periodPaymentFrequencyType(false).periodPaymentFrequency(true);
+
+ return new PutWorkingCapitalLoanProductsProductIdRequest()//
+ .name(name)//
+ .shortName(shortName)//
+ .description(WCLP_DESCRIPTION)//
+ .fundId(FUND_ID)//
+ .periodPaymentRate(new BigDecimal(1))//
+ .repaymentFrequencyType(PutWorkingCapitalLoanProductsProductIdRequest.RepaymentFrequencyTypeEnum.MONTHS)//
+ .repaymentEvery(1)//
+ .startDate(null)//
+ .closeDate(null)//
+ .currencyCode(CURRENCY_CODE_USD)//
+ .digitsAfterDecimal(2)//
+ .inMultiplesOf(1)//
+ .principal(new BigDecimal(200))//
+ .minPrincipal(new BigDecimal(15))//
+ .maxPrincipal(new BigDecimal(300000))//
+ .amortizationType(PutWorkingCapitalLoanProductsProductIdRequest.AmortizationTypeEnum.EIR)//
+ .npvDayCount(DAYS365.value)//
+ .delinquencyBucketId(null)//
+ .dateFormat(DATE_FORMAT)//
+ .locale(LOCALE_EN)//
+ .allowAttributeOverrides(allowAttributeOverrides).paymentAllocation(List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), //
+ List.of(FEE, PRINCIPAL, PENALTY))));//
+ }
+
+ public List invalidNumberOfPaymentAllocationRulesForWorkingCapitalLoanProductCreateRequest() {
+ return List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), //
+ List.of(FEE, PRINCIPAL, PENALTY, "INTEREST")));//
+ }
+
+ public List invalidPaymentAllocationRulesForWorkingCapitalLoanProductCreateRequest() {
+ return List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), //
+ List.of(FEE, PRINCIPAL, "INTEREST")));//
+ }
+
+ public List invalidNumberOfPaymentAllocationRulesForWorkingCapitalLoanProductUpdateRequest() {
+ return List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), //
+ List.of(FEE, PRINCIPAL, PENALTY, "INTEREST")));//
+ }
+
+ public List invalidPaymentAllocationRulesForWorkingCapitalLoanProductUpdateRequest() {
+ return List.of(//
+ createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), //
+ List.of(FEE, PRINCIPAL, "INTEREST")));//
+ }
+
+ public static PostPaymentAllocation createPaymentAllocation(String transactionType, List paymentAllocationRules) {
+ PostPaymentAllocation.TransactionTypeEnum transactionTypeName = PostPaymentAllocation.TransactionTypeEnum.valueOf(transactionType);
+ PostPaymentAllocation paymentAllocationData = new PostPaymentAllocation();
+ paymentAllocationData.setTransactionType(transactionTypeName);
+
+ List paymentAllocationOrders = new ArrayList<>();
+ for (int i = 0; i < paymentAllocationRules.size(); i++) {
+ PaymentAllocationOrder e = new PaymentAllocationOrder();
+ e.setOrder(i + 1);
+ e.setPaymentAllocationRule(paymentAllocationRules.get(i));
+ paymentAllocationOrders.add(e);
+ }
+
+ paymentAllocationData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return paymentAllocationData;
+ }
+
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java
index cc84aa03fc7..387d57bcff3 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java
@@ -77,6 +77,19 @@ public static String setCurrencyNullValueMandatoryFailure() {
return "The parameter 'currencies' is mandatory.";
}
+ public static String currencyNotFound(String currencyCode) {
+ return String.format("Currency with code '%s' not found in currency options", currencyCode);
+ }
+
+ public static String wrongCurrencyField(String currencyCode, String fieldName, Object actual, Object expected) {
+ return String.format("Wrong %s for currency '%s'. Actual value is: %s - But expected value is: %s", fieldName, currencyCode, actual,
+ expected);
+ }
+
+ public static String wrongSelectedCurrencies(List actual, List expected) {
+ return String.format("Wrong selected currencies. Actual value is: %s - But expected value is: %s", actual, expected);
+ }
+
public static String disburseDateFailure(Integer loanId) {
String loanIdStr = parseLoanIdToString(loanId);
return String.format("The date on which a loan with identifier : %s is disbursed cannot be in the future.", loanIdStr);
@@ -115,6 +128,10 @@ public static String disburseIsNotAllowedFailure() {
return "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state.";
}
+ public static String disburseIsNotAllowedExceedApprovedAmountFailure() {
+ return "Loan can't be disbursed, disburse amount is exceeding approved principal.";
+ }
+
public static String loanSubmitDateInFutureFailureMsg() {
return "The date on which a loan is submitted cannot be in the future.";
}
@@ -998,4 +1015,33 @@ public static String reAmortizeClosedLoanFailure() {
public static String reAmortizeSameDateFailure() {
return "Validation errors: [id] Loan reamortization can only be done once a day. There has already been a reamortization done for today";
}
+
+ public static String incorrectExpectedValueInResponse() {
+ return "The parameter is not matching to expected.";
+ }
+
+ public static String fieldValueNullOrEmptyMandatoryFailure(String fieldName) {
+ return String.format("The parameter `%s` is mandatory.", fieldName);
+ }
+
+ public static String fieldValueMoreMaxLengthAllowedFailure(String fieldName, int maxAllowedLength) {
+ return String.format("The parameter `%s` exceeds max length of %d.", fieldName, maxAllowedLength);
+ }
+
+ public static String fieldValueZeroValueFailure(String fieldName) {
+ return String.format("The parameter `%s` must be greater than 0.", fieldName);
+ }
+
+ public static String paymentAllocationRulesInvalidNumberFailure(int actualNumberOfPaymentAllocationRules) {
+ return String.format("Each provided payment allocation must contain exactly 3 allocation rules, but %d were provided",
+ actualNumberOfPaymentAllocationRules);
+ }
+
+ public static String paymentAllocationRulesInvalidValueFailure() {
+ return "One or more payment allocation types are invalid or not recognized";
+ }
+
+ public static String workingCapitalLoanProductIdentifiedDoesNotExistFailure(String identifierId) {
+ return String.format("Working Capital Loan Product with identifier %s does not exist", identifierId);
+ }
}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java
index 7a2d6a64641..2ca73cfc3c5 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java
@@ -50,7 +50,7 @@ private void switchAndSetGlobalConfiguration(String configKey, boolean enabled,
PutGlobalConfigurationsRequest updateRequest = new PutGlobalConfigurationsRequest().enabled(enabled).value(value);
- ok(() -> fineractClient.globalConfiguration().updateConfiguration1(configId, updateRequest, Map.of()));
+ ok(() -> fineractClient.globalConfiguration().updateGlobalConfiguration(configId, updateRequest, Map.of()));
GlobalConfigurationPropertyData updatedConfiguration = ok(
() -> fineractClient.globalConfiguration().retrieveOneByName(configKey, Map.of()));
boolean isEnabled = BooleanUtils.toBoolean(updatedConfiguration.getEnabled());
@@ -64,7 +64,7 @@ public void setGlobalConfigValueString(String configKey, String value) {
PutGlobalConfigurationsRequest updateRequest = new PutGlobalConfigurationsRequest().enabled(true).stringValue(value);
- ok(() -> fineractClient.globalConfiguration().updateConfiguration1(configId, updateRequest, Map.of()));
+ ok(() -> fineractClient.globalConfiguration().updateGlobalConfiguration(configId, updateRequest, Map.of()));
GlobalConfigurationPropertyData updatedConfiguration = ok(
() -> fineractClient.globalConfiguration().retrieveOneByName(configKey, Map.of()));
boolean isEnabled = BooleanUtils.toBoolean(updatedConfiguration.getEnabled());
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/Utils.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/Utils.java
index 8427be5b021..5da2b2e0732 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/Utils.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/Utils.java
@@ -59,6 +59,10 @@ public static String randomStringGenerator(final String prefix, final int len) {
return randomStringGenerator(prefix, len, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
+ public static String randomStringGenerator(final int len) {
+ return randomStringGenerator("", len);
+ }
+
public static String randomFirstNameGenerator() {
return firstNames.get(random.nextInt(firstNames.size()));
}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
index c1c5621a864..94c27c39fc2 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
@@ -30,7 +30,9 @@
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.BusinessStep;
import org.apache.fineract.client.models.BusinessStepRequest;
+import org.apache.fineract.client.models.ConfiguredJobNamesDTO;
import org.apache.fineract.client.models.JobBusinessStepConfigData;
+import org.apache.fineract.client.models.JobBusinessStepDetail;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@@ -62,6 +64,46 @@ public void setWorkflowJobs() {
logChanges();
}
+ /**
+ * Returns all job names that have business step configuration registered.
+ */
+ public ConfiguredJobNamesDTO getConfiguredBusinessJobs() {
+ return ok(() -> fineractClient.businessStepConfiguration().retrieveAllConfiguredBusinessJobs(Map.of()));
+ }
+
+ /**
+ * Returns the currently configured business steps for the given job.
+ *
+ * @param jobName
+ * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS}
+ */
+ public JobBusinessStepConfigData getConfiguredWorkflowSteps(String jobName) {
+ return ok(() -> fineractClient.businessStepConfiguration().retrieveAllConfiguredBusinessStep(jobName, Map.of()));
+ }
+
+ /**
+ * Returns all available (registered) business steps for the given job.
+ *
+ * @param jobName
+ * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS}
+ */
+ public JobBusinessStepDetail getAvailableWorkflowSteps(String jobName) {
+ return ok(() -> fineractClient.businessStepConfiguration().retrieveAllAvailableBusinessStep(jobName, Map.of()));
+ }
+
+ /**
+ * Replaces the configured business steps for the given job.
+ *
+ * @param jobName
+ * the job name, e.g. {@code LOAN_CLOSE_OF_BUSINESS}
+ * @param steps
+ * the ordered list of business steps to configure
+ */
+ public void updateWorkflowSteps(String jobName, List steps) {
+ BusinessStepRequest request = new BusinessStepRequest().businessSteps(steps);
+ executeVoid(() -> fineractClient.businessStepConfiguration().updateJobBusinessStepConfig(jobName, request, Map.of()));
+ }
+
private void logChanges() {
JobBusinessStepConfigData changesResponse = ok(() -> fineractClient.businessStepConfiguration()
.retrieveAllConfiguredBusinessStep(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, Map.of()));
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java
new file mode 100644
index 00000000000..f210d01c562
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkingCapitalLoanTestHelper.java
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.helper;
+
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Objects;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.test.data.LoanStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class WorkingCapitalLoanTestHelper {
+
+ private static final String TABLE_WC_LOAN = "m_wc_loan";
+ private static final String TABLE_WC_LOAN_ACCOUNT_LOCKS = "m_wc_loan_account_locks";
+ private static final int INITIAL_VERSION = 0;
+ private static final long ADMIN_USER_ID = 1L;
+
+ private final JdbcTemplate testJdbcTemplate;
+ private final SimpleJdbcInsert wcLoanInsert;
+
+ public WorkingCapitalLoanTestHelper(JdbcTemplate testJdbcTemplate) {
+ this.testJdbcTemplate = testJdbcTemplate;
+ this.wcLoanInsert = new SimpleJdbcInsert(testJdbcTemplate)//
+ .withTableName(TABLE_WC_LOAN)//
+ .usingGeneratedKeyColumns("id");
+ }
+
+ public Long insertActiveLoan() {
+ return insertLoan(LoanStatus.ACTIVE.getValue(), null);
+ }
+
+ public String generateUniqueExternalId() {
+ return "EXT-" + UUID.randomUUID().toString().substring(0, 8);
+ }
+
+ public String generateAccountNumber() {
+ return String.valueOf(System.currentTimeMillis());
+ }
+
+ public Long insertLoan(int loanStatusId, LocalDate lastClosedBusinessDate) {
+ Timestamp now = Timestamp.from(OffsetDateTime.now(ZoneOffset.UTC).toInstant());
+ MapSqlParameterSource params = new MapSqlParameterSource()//
+ .addValue("account_no", generateAccountNumber()).addValue("external_id", generateUniqueExternalId())
+ .addValue("version", INITIAL_VERSION)//
+ .addValue("created_by", ADMIN_USER_ID)//
+ .addValue("last_modified_by", ADMIN_USER_ID)//
+ .addValue("created_on_utc", now)//
+ .addValue("last_modified_on_utc", now)//
+ .addValue("loan_status_id", loanStatusId)//
+ .addValue("last_closed_business_date", lastClosedBusinessDate);
+ Number key = wcLoanInsert.executeAndReturnKey(params);
+ return Objects.requireNonNull(key, "Generated key must not be null").longValue();
+ }
+
+ public LocalDate getLastClosedBusinessDate(Long loanId) {
+ return testJdbcTemplate.queryForObject("SELECT last_closed_business_date FROM " + TABLE_WC_LOAN + " WHERE id = ?", LocalDate.class,
+ loanId);
+ }
+
+ public int getVersion(Long loanId) {
+ return testJdbcTemplate.queryForObject("SELECT version FROM " + TABLE_WC_LOAN + " WHERE id = ?", Integer.class, loanId);
+ }
+
+ public int countLocksByLoanId(Long loanId) {
+ return testJdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + TABLE_WC_LOAN_ACCOUNT_LOCKS + " WHERE loan_id = ?", Integer.class,
+ loanId);
+ }
+
+ public void deleteById(Long loanId) {
+ testJdbcTemplate.update("DELETE FROM " + TABLE_WC_LOAN_ACCOUNT_LOCKS + " WHERE loan_id = ?", loanId);
+ testJdbcTemplate.update("DELETE FROM " + TABLE_WC_LOAN + " WHERE id = ?", loanId);
+ }
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java
index 269d3c9de07..e97fd26afc5 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java
@@ -20,6 +20,7 @@
import java.util.List;
import org.apache.fineract.test.config.CacheConfiguration;
+import org.apache.fineract.test.config.TestDatabaseConfiguration;
import org.apache.fineract.test.initializer.global.FineractGlobalInitializerStep;
import org.apache.fineract.test.initializer.scenario.FineractScenarioInitializerStep;
import org.apache.fineract.test.initializer.suite.FineractSuiteInitializerStep;
@@ -32,7 +33,7 @@
@Configuration
@ComponentScan({ "org.apache.fineract.test.api", "org.apache.fineract.test.helper" })
@PropertySource("classpath:fineract-test-application.properties")
-@Import({ CacheConfiguration.class })
+@Import({ CacheConfiguration.class, TestDatabaseConfiguration.class })
public class BaseFineractInitializerConfiguration {
@Bean
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
index a3c6762a2ae..ede9c2cdbbf 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
@@ -113,7 +113,7 @@ private void waitForTransactionCommit() {
public void clientEventCheck(PostClientsResponse clientCreationResponse) {
waitForTransactionCommit();
- GetClientsClientIdResponse body = ok(() -> fineractClient.clients().retrieveOne11(clientCreationResponse.getClientId(),
+ GetClientsClientIdResponse body = ok(() -> fineractClient.clients().retrieveOneClient(clientCreationResponse.getClientId(),
Map.of("staffInSelectedOfficeOnly", false)));
Long clientId = Long.valueOf(body.getId());
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/service/JobService.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/service/JobService.java
index e18f6600988..33a68d3eb87 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/service/JobService.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/service/JobService.java
@@ -71,7 +71,7 @@ private void waitUntilJobIsFinished(Job job) {
.until(() -> {
log.debug("Waiting for job {} to finish", jobName);
Long jobId = jobResolver.resolve(job);
- GetJobsResponse getJobsResponse = ok(() -> fineractClient.schedulerJob().retrieveOne5(jobId));
+ GetJobsResponse getJobsResponse = ok(() -> fineractClient.schedulerJob().retrieveOneSchedulerJob(jobId));
Boolean currentlyRunning = getJobsResponse.getCurrentlyRunning();
return BooleanUtils.isFalse(currentlyRunning);
});
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java
index e64c3496664..39ae500f533 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java
@@ -804,7 +804,7 @@ public void adminTransactionCommandTheWithType(String command, String type) thro
String transferExternalId = testContext()
.get(TestContextKey.ASSET_EXTERNALIZATION_TRANSFER_EXTERNAL_ID_USER_GENERATED + "_" + type);
- externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command));
+ externalAssetOwnersApi().transferRequestWithIdByExternalId(transferExternalId, Map.of("command", command));
}
@When("Admin send {string} command to the transaction type {string} will throw error")
@@ -813,7 +813,7 @@ public void adminTransactionCommandTheWithTypeThrowError(String command, String
.get(TestContextKey.ASSET_EXTERNALIZATION_TRANSFER_EXTERNAL_ID_USER_GENERATED + "_" + type);
CallFailedRuntimeException exception = fail(
- () -> externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command)));
+ () -> externalAssetOwnersApi().transferRequestWithIdByExternalId(transferExternalId, Map.of("command", command)));
assertThat(exception.getStatus()).as("Expected status code: 403").isEqualTo(403);
}
@@ -875,7 +875,7 @@ public void adminSendCommandAndItWillThrowError(String command, String transacti
}
CallFailedRuntimeException exception = fail(
- () -> externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command)));
+ () -> externalAssetOwnersApi().transferRequestWithIdByExternalId(transferExternalId, Map.of("command", command)));
assertThat(exception.getStatus()).as("Expected status code: 403").isEqualTo(403);
}
@@ -889,7 +889,7 @@ public void adminSendCommand(String command, String transactionType) throws IOEx
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
}
- externalAssetOwnersApi().transferRequestWithId1(transferExternalId, Map.of("command", command));
+ externalAssetOwnersApi().transferRequestWithIdByExternalId(transferExternalId, Map.of("command", command));
}
@When("Admin set external asset owner loan product attribute {string} value {string} for loan product {string}")
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java
index b389dd1909c..c29839cf167 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BatchApiStepDef.java
@@ -552,7 +552,7 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobErr
// Create new user which cannot bypass loan COB execution
PostUsersResponse createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.getResourceId();
- GetUsersUserIdResponse user = fineractFeignClient.users().retrieveOne32(createdUserId);
+ GetUsersUserIdResponse user = fineractFeignClient.users().retrieveOneUser(createdUserId);
String authorizationString = user.getUsername() + ":" + PWD_USER_WITH_ROLE;
Base64 base64 = new Base64();
headerMap.put("Authorization",
@@ -849,7 +849,7 @@ public void givenClientCreated(int nr) throws IOException {
Map clientQueryParams = new HashMap<>();
clientQueryParams.put("staffInSelectedOfficeOnly", false);
- GetClientsClientIdResponse response = clientApi().retrieveOne12(clientExternalId, clientQueryParams);
+ GetClientsClientIdResponse response = clientApi().retrieveOneClientByExternalId(clientExternalId, clientQueryParams);
assertThat(response.getId()).as(ErrorMessageHelper.idNull()).isNotNull();
}
@@ -866,7 +866,7 @@ public void givenLoanCreated(int nr) throws IOException {
Map loanQueryParams = new HashMap<>();
loanQueryParams.put("staffInSelectedOfficeOnly", false);
- GetLoansLoanIdResponse response = loansApi().retrieveLoan1(loanExternalId, loanQueryParams);
+ GetLoansLoanIdResponse response = loansApi().retrieveLoanByExternalId(loanExternalId, loanQueryParams);
assertThat(response.getId()).as(ErrorMessageHelper.idNull()).isNotNull();
}
@@ -883,7 +883,7 @@ public void givenLoanApproved(int nr) throws IOException {
Map loanQueryParams = new HashMap<>();
loanQueryParams.put("staffInSelectedOfficeOnly", false);
- GetLoansLoanIdResponse response = loansApi().retrieveLoan1(loanExternalId, loanQueryParams);
+ GetLoansLoanIdResponse response = loansApi().retrieveLoanByExternalId(loanExternalId, loanQueryParams);
GetLoansLoanIdStatus status = response.getStatus();
Integer statusIdActual = status.getId();
Integer statusIdExpected = LoanStatus.APPROVED.value;
@@ -909,7 +909,7 @@ public void clientNotCreated(int nr) throws IOException {
try {
Map clientQueryParams = new HashMap<>();
clientQueryParams.put("staffInSelectedOfficeOnly", false);
- clientApi().retrieveOne12(clientExternalId, clientQueryParams);
+ clientApi().retrieveOneClientByExternalId(clientExternalId, clientQueryParams);
throw new IllegalStateException("Expected Feign exception but call succeeded");
} catch (org.apache.fineract.client.feign.FeignException e) {
errorResponse = fromJson(e.responseBodyAsString(), ErrorResponse.class);
@@ -948,7 +948,7 @@ public void loanNotCreated(int nr) throws IOException {
// Feign throws exceptions on errors instead of returning error in response body
ErrorResponse errorResponse = null;
try {
- loansApi().retrieveLoan1(loanExternalId, loanQueryParams);
+ loansApi().retrieveLoanByExternalId(loanExternalId, loanQueryParams);
throw new IllegalStateException("Expected Feign exception but call succeeded");
} catch (org.apache.fineract.client.feign.FeignException e) {
errorResponse = fromJson(e.responseBodyAsString(), ErrorResponse.class);
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java
new file mode 100644
index 00000000000..365a55409b1
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepConfigurationStepDef.java
@@ -0,0 +1,131 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.stepdef.common;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.cucumber.datatable.DataTable;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
+import org.apache.fineract.client.models.BusinessStep;
+import org.apache.fineract.client.models.BusinessStepDetail;
+import org.apache.fineract.client.models.ConfiguredJobNamesDTO;
+import org.apache.fineract.client.models.JobBusinessStepConfigData;
+import org.apache.fineract.client.models.JobBusinessStepDetail;
+import org.apache.fineract.test.helper.WorkFlowJobHelper;
+import org.apache.fineract.test.stepdef.AbstractStepDef;
+import org.springframework.beans.factory.annotation.Autowired;
+
+@Slf4j
+public class BusinessStepConfigurationStepDef extends AbstractStepDef {
+
+ @Autowired
+ private WorkFlowJobHelper workFlowJobHelper;
+
+ @Then("Admin checks that configured business jobs contain {string}")
+ public void checkConfiguredBusinessJobsContain(String jobName) {
+ ConfiguredJobNamesDTO response = workFlowJobHelper.getConfiguredBusinessJobs();
+ List businessJobs = response.getBusinessJobs();
+ log.debug("Configured business jobs: {}", businessJobs);
+ assertThat(businessJobs)//
+ .as("Configured business jobs should contain '%s' but got: %s", jobName, businessJobs)//
+ .contains(jobName);
+ }
+
+ @Then("Admin verifies configured business steps for {string} match:")
+ public void verifyConfiguredBusinessStepsMatch(String jobName, DataTable dataTable) {
+ JobBusinessStepConfigData response = workFlowJobHelper.getConfiguredWorkflowSteps(jobName);
+ List actualSteps = response.getBusinessSteps();
+ log.debug("Configured business steps for '{}': {}", jobName, actualSteps);
+
+ List
*/
+ @Getter
private LocalDate interestChargedFromDate;
private Money inArrearsTolerance;
@@ -140,20 +159,29 @@ public final class LoanApplicationTerms {
// added
private LocalDate loanEndDate;
+ @Getter
private List disbursementDatas;
private boolean multiDisburseLoan;
+ @Setter
private BigDecimal fixedEmiAmount;
+ @Setter
private BigDecimal fixedPrincipalAmount;
+ @Getter
+ @Setter
private BigDecimal currentPeriodFixedEmiAmount;
+ @Getter
+ @Setter
private BigDecimal currentPeriodFixedPrincipalAmount;
+ @Getter
private BigDecimal actualFixedEmiAmount;
+ @Getter
private BigDecimal maxOutstandingBalance;
private Money totalInterestDue;
@@ -162,34 +190,47 @@ public final class LoanApplicationTerms {
private DaysInYearType daysInYearType;
+ @Getter
private boolean interestRecalculationEnabled;
+ @Getter
private LoanRescheduleStrategyMethod rescheduleStrategyMethod;
+ @Getter
private InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod;
+ @Getter
private CalendarInstance restCalendarInstance;
+ @Getter
private RecalculationFrequencyType recalculationFrequencyType;
+ @Getter
private CalendarInstance compoundingCalendarInstance;
+ @Getter
private RecalculationFrequencyType compoundingFrequencyType;
private boolean allowCompoundingOnEod;
private BigDecimal principalThresholdForLastInstalment;
+ @Getter
private Integer installmentAmountInMultiplesOf;
+ @Getter
private LoanPreCloseInterestCalculationStrategy preClosureInterestCalculationStrategy;
+ @Getter
private Money approvedPrincipal;
private LoanTermVariationsDataWrapper variationsDataWrapper;
private Money adjustPrincipalForFlatLoans;
+ @Getter
+ @Setter
private LocalDate seedDate;
+ @Getter
private CalendarHistoryDataWrapper calendarHistoryDataWrapper;
private Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled;
@@ -220,18 +261,27 @@ public final class LoanApplicationTerms {
private int periodsCompleted = 0;
private int extraPeriods = 0;
private boolean isEqualAmortization;
+ @Setter
private Money interestTobeApproppriated;
private BigDecimal fixedPrincipalPercentagePerInstallment;
+ @Getter
+ @Setter
private LocalDate newScheduledDueDateStart;
private boolean isDownPaymentEnabled;
+ @Getter
private BigDecimal disbursedAmountPercentageForDownPayment;
+ @Getter
private Money downPaymentAmount;
private boolean isAutoRepaymentForDownPaymentEnabled;
+ @Getter
private RepaymentStartDateType repaymentStartDateType;
+ @Getter
private LocalDate submittedOnDate;
+ @Setter
private Money disbursedPrincipal;
+ @Getter
private LoanScheduleType loanScheduleType;
private LoanScheduleProcessingType loanScheduleProcessingType;
private boolean enableAccrualActivityPosting;
@@ -1504,19 +1554,11 @@ private Integer calculateLastInterestGracePeriod(int periodNumber) {
}
public boolean isPrincipalGraceApplicableForThisPeriod(final int periodNumber) {
- boolean isPrincipalGraceApplicableForThisPeriod = false;
- if (this.periodNumbersApplicableForPrincipalGrace.contains(periodNumber)) {
- isPrincipalGraceApplicableForThisPeriod = true;
- }
- return isPrincipalGraceApplicableForThisPeriod;
+ return this.periodNumbersApplicableForPrincipalGrace.contains(periodNumber);
}
public boolean isInterestPaymentGraceApplicableForThisPeriod(final int periodNumber) {
- boolean isInterestPaymentGraceApplicableForThisPeriod = false;
- if (this.periodNumbersApplicableForInterestGrace.contains(periodNumber)) {
- isInterestPaymentGraceApplicableForThisPeriod = true;
- }
- return isInterestPaymentGraceApplicableForThisPeriod;
+ return this.periodNumbersApplicableForInterestGrace.contains(periodNumber);
}
private boolean isFirstPeriodAfterInterestPaymentGracePeriod(final int periodNumber) {
@@ -1662,12 +1704,7 @@ private Integer calculateNumberOfRemainingPrincipalPaymentPeriods(final Integer
principalFeePeriods++;
}
}
- Integer periodsRemaining = totalNumberOfRepaymentPeriods - periodsElapsed - principalFeePeriods;
- return periodsRemaining;
- }
-
- public void setFixedPrincipalAmount(BigDecimal fixedPrincipalAmount) {
- this.fixedPrincipalAmount = fixedPrincipalAmount;
+ return totalNumberOfRepaymentPeriods - periodsElapsed - principalFeePeriods;
}
private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment,
@@ -1715,65 +1752,17 @@ public ILoanConfigurationDetails toLoanConfigurationDetails() {
repaymentEvery, numberOfRepayments,
isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled,
daysInYearCustomStrategy, allowPartialPeriodInterestCalculation, interestRecalculationEnabled, recalculationFrequencyType,
- preClosureInterestCalculationStrategy, allowFullTermForTranche);
- }
-
- public Integer getLoanTermFrequency() {
- return this.loanTermFrequency;
- }
-
- public PeriodFrequencyType getLoanTermPeriodFrequencyType() {
- return this.loanTermPeriodFrequencyType;
- }
-
- public Integer getRepaymentEvery() {
- return this.repaymentEvery;
- }
-
- public PeriodFrequencyType getRepaymentPeriodFrequencyType() {
- return this.repaymentPeriodFrequencyType;
+ preClosureInterestCalculationStrategy, allowFullTermForTranche, loanScheduleProcessingType);
}
public LocalDate getRepaymentStartFromDate() {
return this.repaymentsStartingFromDate;
}
- public LocalDate getInterestChargedFromDate() {
- return this.interestChargedFromDate;
- }
-
- public void setPrincipal(Money principal) {
- this.principal = principal;
- }
-
- public void setDisbursedPrincipal(Money disbursedPrincipal) {
- this.disbursedPrincipal = disbursedPrincipal;
- }
-
public LocalDate getInterestChargedFromLocalDate() {
return this.interestChargedFromDate;
}
- public InterestMethod getInterestMethod() {
- return this.interestMethod;
- }
-
- public AmortizationMethod getAmortizationMethod() {
- return this.amortizationMethod;
- }
-
- public CurrencyData getCurrency() {
- return currency;
- }
-
- public Integer getNumberOfRepayments() {
- return this.numberOfRepayments;
- }
-
- public LocalDate getExpectedDisbursementDate() {
- return this.expectedDisbursementDate;
- }
-
public LocalDate getRepaymentsStartingFromLocalDate() {
return this.repaymentsStartingFromDate;
}
@@ -1782,18 +1771,6 @@ public LocalDate getCalculatedRepaymentsStartingFromLocalDate() {
return this.calculatedRepaymentsStartingFromDate;
}
- public Money getPrincipal() {
- return this.principal;
- }
-
- public Money getApprovedPrincipal() {
- return this.approvedPrincipal;
- }
-
- public List getDisbursementDatas() {
- return this.disbursementDatas;
- }
-
public boolean isMultiDisburseLoan() {
return this.multiDisburseLoan;
}
@@ -1803,10 +1780,6 @@ public Money getMaxOutstandingBalanceMoney() {
return Money.of(getCurrency(), this.maxOutstandingBalance);
}
- public BigDecimal getMaxOutstandingBalance() {
- return maxOutstandingBalance;
- }
-
public BigDecimal getFixedEmiAmount() {
BigDecimal fixedEmiAmount = this.fixedEmiAmount;
if (getCurrentPeriodFixedEmiAmount() != null) {
@@ -1815,18 +1788,6 @@ public BigDecimal getFixedEmiAmount() {
return fixedEmiAmount;
}
- public Integer getNthDay() {
- return this.nthDay;
- }
-
- public DayOfWeekType getWeekDayType() {
- return this.weekDayType;
- }
-
- public void setFixedEmiAmount(BigDecimal fixedEmiAmount) {
- this.fixedEmiAmount = fixedEmiAmount;
- }
-
public void resetFixedEmiAmount() {
this.fixedEmiAmount = this.actualFixedEmiAmount;
}
@@ -1843,22 +1804,6 @@ public boolean isInterestBearingAndInterestRecalculationEnabled() {
return isInterestBearing() && isInterestRecalculationEnabled();
}
- public boolean isInterestRecalculationEnabled() {
- return this.interestRecalculationEnabled;
- }
-
- public LoanRescheduleStrategyMethod getRescheduleStrategyMethod() {
- return this.rescheduleStrategyMethod;
- }
-
- public InterestRecalculationCompoundingMethod getInterestRecalculationCompoundingMethod() {
- return this.interestRecalculationCompoundingMethod;
- }
-
- public CalendarInstance getRestCalendarInstance() {
- return this.restCalendarInstance;
- }
-
private boolean isFallingInRepaymentPeriod(LocalDate fromDate, LocalDate toDate) {
boolean isSameAsRepaymentPeriod = false;
if (this.interestCalculationPeriodMethod.getValue().equals(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD.getValue())) {
@@ -1868,14 +1813,8 @@ private boolean isFallingInRepaymentPeriod(LocalDate fromDate, LocalDate toDate)
isSameAsRepaymentPeriod = (days % 7) == 0;
break;
case MONTHS:
- boolean isFromDateOnEndDate = false;
- if (fromDate.getDayOfMonth() > fromDate.plusDays(1).getDayOfMonth()) {
- isFromDateOnEndDate = true;
- }
- boolean isToDateOnEndDate = false;
- if (toDate.getDayOfMonth() > toDate.plusDays(1).getDayOfMonth()) {
- isToDateOnEndDate = true;
- }
+ boolean isFromDateOnEndDate = fromDate.getDayOfMonth() > fromDate.plusDays(1).getDayOfMonth();
+ boolean isToDateOnEndDate = toDate.getDayOfMonth() > toDate.plusDays(1).getDayOfMonth();
if (isFromDateOnEndDate && isToDateOnEndDate) {
isSameAsRepaymentPeriod = true;
@@ -1915,10 +1854,6 @@ private Integer getPeriodsBetween(LocalDate fromDate, LocalDate toDate) {
return numberOfPeriods;
}
- public RecalculationFrequencyType getRecalculationFrequencyType() {
- return this.recalculationFrequencyType;
- }
-
public void updateNumberOfRepayments(final Integer numberOfRepayments) {
this.numberOfRepayments = numberOfRepayments;
this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments();
@@ -1941,14 +1876,13 @@ public void updateInterestRatePerPeriod(BigDecimal interestRatePerPeriod) {
public void updateAnnualNominalInterestRate(BigDecimal annualNominalInterestRate) {
if (annualNominalInterestRate != null) {
+ if (this.annualNominalInterestRate == null || annualNominalInterestRate.compareTo(this.annualNominalInterestRate) != 0) {
+ this.fixedEmiAmount = null;
+ }
this.annualNominalInterestRate = annualNominalInterestRate;
}
}
- public BigDecimal getAnnualNominalInterestRate() {
- return this.annualNominalInterestRate;
- }
-
public void updateInterestChargedFromDate(LocalDate interestChargedFromDate) {
if (interestChargedFromDate != null) {
this.interestChargedFromDate = interestChargedFromDate;
@@ -1965,30 +1899,6 @@ public void updateTotalInterestDue(Money totalInterestDue) {
this.totalInterestDue = totalInterestDue;
}
- public InterestCalculationPeriodMethod getInterestCalculationPeriodMethod() {
- return this.interestCalculationPeriodMethod;
- }
-
- public LoanPreCloseInterestCalculationStrategy getPreClosureInterestCalculationStrategy() {
- return this.preClosureInterestCalculationStrategy;
- }
-
- public CalendarInstance getCompoundingCalendarInstance() {
- return this.compoundingCalendarInstance;
- }
-
- public RecalculationFrequencyType getCompoundingFrequencyType() {
- return this.compoundingFrequencyType;
- }
-
- public BigDecimal getActualFixedEmiAmount() {
- return this.actualFixedEmiAmount;
- }
-
- public Calendar getLoanCalendar() {
- return loanCalendar;
- }
-
public BigDecimal getFixedPrincipalAmount() {
BigDecimal fixedPrincipalAmount = this.fixedPrincipalAmount;
if (getCurrentPeriodFixedPrincipalAmount() != null) {
@@ -2001,34 +1911,10 @@ public LoanTermVariationsDataWrapper getLoanTermVariations() {
return this.variationsDataWrapper;
}
- public BigDecimal getCurrentPeriodFixedEmiAmount() {
- return this.currentPeriodFixedEmiAmount;
- }
-
- public void setCurrentPeriodFixedEmiAmount(BigDecimal currentPeriodFixedEmiAmount) {
- this.currentPeriodFixedEmiAmount = currentPeriodFixedEmiAmount;
- }
-
- public BigDecimal getCurrentPeriodFixedPrincipalAmount() {
- return this.currentPeriodFixedPrincipalAmount;
- }
-
- public void setCurrentPeriodFixedPrincipalAmount(BigDecimal currentPeriodFixedPrincipalAmount) {
- this.currentPeriodFixedPrincipalAmount = currentPeriodFixedPrincipalAmount;
- }
-
public Integer fetchNumberOfRepaymentsAfterExceptions() {
return this.actualNumberOfRepayments;
}
- public LocalDate getSeedDate() {
- return this.seedDate;
- }
-
- public CalendarHistoryDataWrapper getCalendarHistoryDataWrapper() {
- return this.calendarHistoryDataWrapper;
- }
-
public Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled() {
return this.isInterestChargedFromDateSameAsDisbursalDateEnabled;
}
@@ -2150,10 +2036,6 @@ public void updateTotalInterestAccounted(Money totalInterestAccounted) {
this.totalInterestAccounted = totalInterestAccounted;
}
- public void setSeedDate(LocalDate seedDate) {
- this.seedDate = seedDate;
- }
-
public boolean isEqualAmortization() {
return isEqualAmortization;
}
@@ -2170,10 +2052,6 @@ public Money getInterestTobeApproppriated() {
return interestTobeApproppriated == null ? this.principal.zero() : interestTobeApproppriated;
}
- public void setInterestTobeApproppriated(Money interestTobeApproppriated) {
- this.interestTobeApproppriated = interestTobeApproppriated;
- }
-
public Boolean isInterestTobeApproppriated() {
return interestTobeApproppriated != null && interestTobeApproppriated.isGreaterThanZero();
}
@@ -2186,46 +2064,10 @@ public boolean isPrincipalCompoundingDisabledForOverdueLoans() {
return isPrincipalCompoundingDisabledForOverdueLoans;
}
- public LocalDate getNewScheduledDueDateStart() {
- return newScheduledDueDateStart;
- }
-
- public void setNewScheduledDueDateStart(LocalDate newScheduledDueDateStart) {
- this.newScheduledDueDateStart = newScheduledDueDateStart;
- }
-
public boolean isDownPaymentEnabled() {
return isDownPaymentEnabled;
}
- public BigDecimal getDisbursedAmountPercentageForDownPayment() {
- return disbursedAmountPercentageForDownPayment;
- }
-
- public RepaymentStartDateType getRepaymentStartDateType() {
- return repaymentStartDateType;
- }
-
- public LocalDate getSubmittedOnDate() {
- return submittedOnDate;
- }
-
- public Integer getInstallmentAmountInMultiplesOf() {
- return installmentAmountInMultiplesOf;
- }
-
- public LoanScheduleType getLoanScheduleType() {
- return loanScheduleType;
- }
-
- public Money getDownPaymentAmount() {
- return downPaymentAmount;
- }
-
- public Integer getFixedLength() {
- return fixedLength;
- }
-
public LocalDate calculateMaxDateForFixedLength() {
final LocalDate startDate = getRepaymentStartDate();
LocalDate maxDateForFixedLength = null;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java
index 017abc198ac..1312be79dab 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java
@@ -20,12 +20,14 @@
import java.time.LocalDate;
import java.util.Collection;
+import lombok.Getter;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
/**
* Immutable data object representing loan reschedule request data.
**/
+@Getter
public final class LoanRescheduleRequestData {
private final Long id;
@@ -126,41 +128,6 @@ public static LoanRescheduleRequestData instance(Long id, Long loanId, LoanResch
rescheduleReasonCodeValue);
}
- /**
- * @return the id
- */
- public Long getId() {
- return id;
- }
-
- /**
- * @return the loanId
- */
- public Long getLoanId() {
- return loanId;
- }
-
- /**
- * @return the statusEnum
- */
- public LoanRescheduleRequestStatusEnumData getStatusEnum() {
- return statusEnum;
- }
-
- /**
- * @return the reschedule from installment number
- */
- public Integer getRescheduleFromInstallment() {
- return rescheduleFromInstallment;
- }
-
- /**
- * @return the reschedule from date
- */
- public LocalDate getRescheduleFromDate() {
- return rescheduleFromDate;
- }
-
/**
* @return the rescheduleReasonCodeValueId
*/
@@ -168,41 +135,6 @@ public CodeValueData getRescheduleReasonCodeValueId() {
return rescheduleReasonCodeValue;
}
- /**
- * @return the rescheduleReasonText
- */
- public String getRescheduleReasonComment() {
- return rescheduleReasonComment;
- }
-
- /**
- * @return the timeline
- **/
- public LoanRescheduleRequestTimelineData getTimeline() {
- return this.timeline;
- }
-
- /**
- * @return the clientName
- */
- public String getClientName() {
- return clientName;
- }
-
- /**
- * @return the loanAccountNumber
- */
- public String getLoanAccountNumber() {
- return loanAccountNumber;
- }
-
- /**
- * @return the clientId
- */
- public Long getClientId() {
- return clientId;
- }
-
/**
* @return the recalculateInterest
*/
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestStatusEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestStatusEnumData.java
index 087cdd6c464..6685101639c 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestStatusEnumData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestStatusEnumData.java
@@ -18,11 +18,15 @@
*/
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.data;
+import lombok.Getter;
+import lombok.experimental.Accessors;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
/**
* Immutable data object represent loan reschedule request status enumerations.
**/
+@Getter
+@Accessors(fluent = true)
public class LoanRescheduleRequestStatusEnumData {
private final Long id;
@@ -46,18 +50,6 @@ public LoanRescheduleRequestStatusEnumData(Long id, String code, String value) {
this.rejected = Long.valueOf(LoanStatus.REJECTED.getValue()).equals(this.id);
}
- public Long id() {
- return this.id;
- }
-
- public String code() {
- return this.code;
- }
-
- public String value() {
- return this.value;
- }
-
public boolean isPendingApproval() {
return this.pendingApproval;
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java
index 8715f016435..d9b70f18fac 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java
@@ -19,11 +19,13 @@
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.data;
import java.time.LocalDate;
+import lombok.Data;
/**
* Immutable data object represent the timeline events of a loan reschedule request
**/
@SuppressWarnings("unused")
+@Data
public class LoanRescheduleRequestTimelineData {
private final LocalDate submittedOnDate;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModelRepaymentPeriod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModelRepaymentPeriod.java
index 06a5428ac46..b487c862c40 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModelRepaymentPeriod.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleModelRepaymentPeriod.java
@@ -20,9 +20,11 @@
import java.math.BigDecimal;
import java.time.LocalDate;
+import lombok.Setter;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
+@Setter
public final class LoanRescheduleModelRepaymentPeriod implements LoanRescheduleModalPeriod {
private int periodNumber;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java
index b8211af7e41..ef743f0038e 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequest.java
@@ -30,6 +30,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import lombok.Getter;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -40,6 +41,7 @@
@Entity
@Table(name = "m_loan_reschedule_request")
+@Getter
public class LoanRescheduleRequest extends AbstractPersistableCustom {
@ManyToOne
@@ -129,83 +131,6 @@ public static LoanRescheduleRequest instance(final Loan loan, final Integer stat
rejectedOnDate, rejectedByUser);
}
- /**
- * @return the reschedule request loan object
- **/
- public Loan getLoan() {
- return this.loan;
- }
-
- /**
- * @return the status enum
- **/
- public Integer getStatusEnum() {
- return this.statusEnum;
- }
-
- /**
- * @return installment number of the rescheduling start point
- **/
- public Integer getRescheduleFromInstallment() {
- return this.rescheduleFromInstallment;
- }
-
- /**
- * @return due date of the rescheduling start point
- **/
- public LocalDate getRescheduleFromDate() {
- return this.rescheduleFromDate;
- }
-
- /**
- * @return the reschedule reason code value object
- **/
- public CodeValue getRescheduleReasonCodeValue() {
- return this.rescheduleReasonCodeValue;
- }
-
- /**
- * @return the reschedule reason comment added by the "submittedByUser"
- **/
- public String getRescheduleReasonComment() {
- return this.rescheduleReasonComment;
- }
-
- /**
- * @return the date the request was submitted
- **/
- public LocalDate getSubmittedOnDate() {
- return this.submittedOnDate;
- }
-
- /**
- * @return the user that submitted the request
- **/
- public AppUser getSubmittedByUser() {
- return this.submittedByUser;
- }
-
- /**
- * @return the date the request was approved
- **/
- public LocalDate getApprovedOnDate() {
- return this.approvedOnDate;
- }
-
- /**
- * @return the user that approved the request
- **/
- public AppUser getApprovedByUser() {
- return this.approvedByUser;
- }
-
- /**
- * @return the date the request was rejected
- **/
- public LocalDate getRejectedOnDate() {
- return this.rejectedOnDate;
- }
-
/**
* @return the recalculate interest option (true/false)
**/
@@ -219,13 +144,6 @@ public Boolean getRecalculateInterest() {
return recalculateInterest;
}
- /**
- * @return the user that rejected the request
- **/
- public AppUser getRejectedByUser() {
- return this.rejectedByUser;
- }
-
/**
* change the status of the loan reschedule request to approved, also updating the approvedByUser and approvedOnDate
* properties
@@ -268,10 +186,6 @@ public void updateLoanRescheduleRequestToTermVariationMappings(final List getLoanRescheduleRequestToTermVariationMappings() {
- return this.loanRescheduleRequestToTermVariationMappings;
- }
-
public LoanTermVariations getInterestRateFromInstallmentTermVariationIfExists() {
return this.loanRescheduleRequestToTermVariationMappings.stream()
.map(LoanRescheduleRequestToTermVariationMapping::getLoanTermVariations)
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java
index c15689549df..04fed6458f8 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java
@@ -23,6 +23,7 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -35,6 +36,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -121,9 +123,34 @@ public void refreshSummaryAndBalancesForDisbursedLoan(final Loan loan) {
final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan);
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(),
capitalizedIncome, capitalizedIncomeAdjustment);
+ reconcileChargeStatusWithSummary(loan);
updateLoanOutstandingBalances(loan);
}
+ private void reconcileChargeStatusWithSummary(final Loan loan) {
+ final LoanSummary summary = loan.getSummary();
+ final Set charges = loan.getLoanCharges();
+ if (summary == null || charges == null) {
+ return;
+ }
+ if (MathUtil.isZero(summary.getTotalFeeChargesOutstanding())) {
+ for (final LoanCharge charge : charges) {
+ if (charge.isActive() && charge.isFeeCharge() && !charge.isPaid() && !charge.isWaived()
+ && MathUtil.isGreaterThanZero(charge.getAmount())) {
+ charge.reconcileFullyPaid();
+ }
+ }
+ }
+ if (MathUtil.isZero(summary.getTotalPenaltyChargesOutstanding())) {
+ for (final LoanCharge charge : charges) {
+ if (charge.isActive() && charge.isPenaltyCharge() && !charge.isPaid() && !charge.isWaived()
+ && MathUtil.isGreaterThanZero(charge.getAmount())) {
+ charge.reconcileFullyPaid();
+ }
+ }
+ }
+ }
+
private Money calculateTotalRecoveredPayments(Loan loan) {
// in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments
final BigDecimal totalRecoveryAmount = loanTransactionRepository.calculateTotalRecoveryPaymentAmount(loan);
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsDetailsApiResource.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsDetailsApiResource.java
new file mode 100644
index 00000000000..1225006b99a
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsDetailsApiResource.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.UriInfo;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.portfolio.loanproduct.data.LoanProductBasicDetailsData;
+import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadBasicDetailsService;
+import org.springframework.stereotype.Component;
+
+@Path("/v1/loanproducts")
+@Component
+@Tag(name = "Loan Products Details", description = "Loan product basic details to be listed")
+@RequiredArgsConstructor
+public class LoanProductsDetailsApiResource {
+
+ private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOANPRODUCT";
+ private final PlatformSecurityContext context;
+ private final List loanProductReadBasicDetailsServices;
+
+ @GET
+ @Path("basic-details")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "List Loan Products with basic details", description = "Lists Loan Products with basic details to be listed")
+ public Collection fetchProducts(@Context final UriInfo uriInfo) {
+ this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
+
+ Collection products = new ArrayList<>();
+ loanProductReadBasicDetailsServices.forEach(service -> products.addAll(service.retrieveProducts()));
+ return products;
+ }
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
index 977e9a513cb..3fdede017f0 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanConfigurationDetails.java
@@ -25,6 +25,7 @@
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.ILoanConfigurationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
@@ -60,6 +61,8 @@ public class LoanConfigurationDetails implements ILoanConfigurationDetails {
private final LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy;
@Getter
private final boolean allowFullTermForTranche;
+ @Getter
+ private final LoanScheduleProcessingType loanScheduleProcessingType;
public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate,
Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace,
@@ -69,7 +72,8 @@ public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePe
Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate,
DaysInYearCustomStrategyType daysInYearCustomStrategy, boolean allowPartialPeriodInterestCalculation,
boolean isInterestRecalculationEnabled, RecalculationFrequencyType restFrequencyType,
- LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy, boolean allowFullTermForTranche) {
+ LoanPreCloseInterestCalculationStrategy preCloseInterestCalculationStrategy, boolean allowFullTermForTranche,
+ LoanScheduleProcessingType loanScheduleProcessingType) {
this.currency = currency;
this.interestRatePerPeriod = interestRatePerPeriod;
this.annualNominalInterestRate = annualNominalInterestRate;
@@ -92,6 +96,7 @@ public LoanConfigurationDetails(CurrencyData currency, BigDecimal interestRatePe
this.restFrequencyType = restFrequencyType;
this.preCloseInterestCalculationStrategy = preCloseInterestCalculationStrategy;
this.allowFullTermForTranche = allowFullTermForTranche;
+ this.loanScheduleProcessingType = loanScheduleProcessingType;
}
private Integer defaultToNullIfZero(final Integer value) {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductBasicDetailsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductBasicDetailsData.java
new file mode 100644
index 00000000000..b5b7c83aaa1
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductBasicDetailsData.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.data;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.organisation.monetary.data.CurrencyData;
+
+@Data
+@RequiredArgsConstructor
+public class LoanProductBasicDetailsData {
+
+ private final String productType;
+ private final Long id;
+ private final String name;
+ private final String shortName;
+ private final String description;
+ private final CurrencyData currency;
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
index 4ec89f0fd0c..79fe75bc9e5 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/ILoanConfigurationDetails.java
@@ -22,6 +22,7 @@
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
/**
* Represents the bare minimum repayment details needed for activities related to generating repayment schedules.
@@ -75,4 +76,6 @@ public interface ILoanConfigurationDetails {
LoanPreCloseInterestCalculationStrategy getPreCloseInterestCalculationStrategy();
boolean isAllowFullTermForTranche();
+
+ LoanScheduleProcessingType getLoanScheduleProcessingType();
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java
index 78314991b2c..6a35428a311 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
+import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
@@ -41,4 +42,7 @@ public interface LoanProductRepository extends JpaRepository,
@Override
@Query("SELECT CASE WHEN COUNT(loanProduct)>0 THEN TRUE ELSE FALSE END FROM LoanProduct loanProduct WHERE loanProduct.id = :loanProductId")
boolean existsById(@NonNull @Param("loanProductId") Long loanProductId);
+
+ @Query("select loanProduct from LoanProduct loanProduct where loanProduct.closeDate is null or loanProduct.closeDate >= :businessDate")
+ List fetchActiveLoanProducts(LocalDate businessDate);
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/LoanProductBasicDetailsMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/LoanProductBasicDetailsMapper.java
new file mode 100644
index 00000000000..26f1d4a6bed
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/LoanProductBasicDetailsMapper.java
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.mapper;
+
+import java.util.List;
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.organisation.monetary.data.CurrencyData;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.portfolio.loanproduct.data.LoanProductBasicDetailsData;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface LoanProductBasicDetailsMapper {
+
+ @Mapping(target = "productType", constant = "loan")
+ @Mapping(target = "currency", source = "loanProductRelatedDetail.currency", qualifiedByName = "currencyData")
+ LoanProductBasicDetailsData map(LoanProduct source);
+
+ List map(List source);
+
+ @Named("currencyData")
+ default CurrencyData currencyData(final MonetaryCurrency currency) {
+ return currency.toData();
+ }
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsService.java
new file mode 100644
index 00000000000..b790823e811
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsService.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.service;
+
+import java.util.Collection;
+import org.apache.fineract.portfolio.loanproduct.data.LoanProductBasicDetailsData;
+
+public interface LoanProductReadBasicDetailsService {
+
+ Collection retrieveProducts();
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsServiceImpl.java
new file mode 100644
index 00000000000..b966dc6b597
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadBasicDetailsServiceImpl.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.service;
+
+import java.util.Collection;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.portfolio.loanproduct.data.LoanProductBasicDetailsData;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
+import org.apache.fineract.portfolio.loanproduct.mapper.LoanProductBasicDetailsMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class LoanProductReadBasicDetailsServiceImpl implements LoanProductReadBasicDetailsService {
+
+ private final LoanProductBasicDetailsMapper loanProductBasicDetailsMapper;
+ private final LoanProductRepository loanProductRepository;
+
+ @Override
+ public Collection retrieveProducts() {
+ return loanProductBasicDetailsMapper.map(loanProductRepository.fetchActiveLoanProducts(DateUtils.getBusinessLocalDate()));
+ }
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java
index d6ddef51a81..5536b45b33d 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java
@@ -57,4 +57,5 @@ public interface LoanProductReadPlatformService {
Collection retrieveCreditAllocationData(Long loanProductId);
LoanProductData retrieveLoanProductFloatingDetails(Long loanProductId);
+
}
diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AprCalculatorTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AprCalculatorTest.java
new file mode 100644
index 00000000000..f11129deee6
--- /dev/null
+++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AprCalculatorTest.java
@@ -0,0 +1,334 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.common.domain.DaysInYearType;
+import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class AprCalculatorTest {
+
+ private static final int PRECISION = 19;
+ private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class);
+ private static final MathContext MATH_CONTEXT = new MathContext(PRECISION, RoundingMode.HALF_EVEN);
+
+ @Mock
+ private PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
+
+ private AprCalculator aprCalculator;
+
+ @BeforeAll
+ static void init() {
+ MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
+ MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(MATH_CONTEXT);
+ }
+
+ @BeforeEach
+ void setUp() {
+ aprCalculator = new AprCalculator(paymentPeriodsInOneYearCalculator);
+ }
+
+ @AfterAll
+ static void tearDown() {
+ MONEY_HELPER.close();
+ }
+
+ /**
+ * Test DAYS frequency with DaysInYearType.ACTUAL
+ *
+ * This test verifies the fix for FINERACT-2492 where ACTUAL was incorrectly using value 1
+ * instead of delegating to PaymentPeriodsInOneYearCalculator which returns 365.
+ */
+ @Test
+ void testCalculateFrom_DaysFrequency_WithActualDaysInYear() {
+ // Given
+ when(paymentPeriodsInOneYearCalculator.calculate(PeriodFrequencyType.DAYS)).thenReturn(365);
+
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0); // 10% per day
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1,
+ PeriodFrequencyType.DAYS, daysInYearType);
+
+ // Then
+ // Annual rate should be 10% * 365 = 3650%
+ assertEquals(0, BigDecimal.valueOf(3650.0).compareTo(annualRate),
+ "Annual rate should be interestRatePerPeriod * 365 for ACTUAL");
+ verify(paymentPeriodsInOneYearCalculator).calculate(PeriodFrequencyType.DAYS);
+ }
+
+ /**
+ * Test WHOLE_TERM frequency with DAYS repayment and DaysInYearType.ACTUAL
+ *
+ * This is the exact scenario from FINERACT-2492 bug report.
+ */
+ @Test
+ void testCalculateFrom_WholeTermFrequency_WithDaysRepaymentAndActualDaysInYear() {
+ // Given
+ when(paymentPeriodsInOneYearCalculator.calculate(PeriodFrequencyType.DAYS)).thenReturn(365);
+
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0); // 10% whole term
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.WHOLE_TERM;
+ Integer numberOfRepayments = 3;
+ Integer repaymentEvery = 1;
+ PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, numberOfRepayments,
+ repaymentEvery, repaymentFrequencyType, daysInYearType);
+
+ // Then
+ // ratePerPeriod = 10% / (3 * 1) = 3.33333333%
+ // annualRate = 3.33333333% * 365 = 1216.66666667%
+ BigDecimal expectedAnnualRate = BigDecimal.valueOf(10.0).divide(BigDecimal.valueOf(3), 8, java.math.RoundingMode.HALF_EVEN)
+ .multiply(BigDecimal.valueOf(365));
+
+ assertEquals(0, expectedAnnualRate.compareTo(annualRate), "Annual rate calculation should use 365 days for ACTUAL");
+ verify(paymentPeriodsInOneYearCalculator).calculate(PeriodFrequencyType.DAYS);
+ }
+
+ /**
+ * Test DAYS frequency with DaysInYearType.DAYS_360
+ *
+ * Verify that DAYS_360 works correctly and uses value 360 directly.
+ */
+ @Test
+ void testCalculateFrom_DaysFrequency_WithDays360() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.DAYS_360;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.DAYS,
+ daysInYearType);
+
+ // Then
+ // Annual rate should be 10% * 360 = 3600%
+ assertEquals(0, BigDecimal.valueOf(3600.0).compareTo(annualRate), "Annual rate should use 360 days for DAYS_360");
+ }
+
+ /**
+ * Test DAYS frequency with DaysInYearType.DAYS_364
+ *
+ * Verify that DAYS_364 works correctly and uses value 364 directly.
+ */
+ @Test
+ void testCalculateFrom_DaysFrequency_WithDays364() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.DAYS_364;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.DAYS,
+ daysInYearType);
+
+ // Then
+ // Annual rate should be 10% * 364 = 3640%
+ assertEquals(0, BigDecimal.valueOf(3640.0).compareTo(annualRate), "Annual rate should use 364 days for DAYS_364");
+ }
+
+ /**
+ * Test DAYS frequency with DaysInYearType.DAYS_365
+ *
+ * Verify that DAYS_365 works correctly and uses value 365 directly.
+ */
+ @Test
+ void testCalculateFrom_DaysFrequency_WithDays365() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.DAYS,
+ daysInYearType);
+
+ // Then
+ // Annual rate should be 10% * 365 = 3650%
+ assertEquals(0, BigDecimal.valueOf(3650.0).compareTo(annualRate), "Annual rate should use 365 days for DAYS_365");
+ }
+
+ /**
+ * Test WHOLE_TERM frequency with WEEKS repayment and DaysInYearType.ACTUAL
+ *
+ * Verify that ACTUAL doesn't affect non-DAYS repayment frequencies.
+ */
+ @Test
+ void testCalculateFrom_WholeTermFrequency_WithWeeksRepaymentAndActualDaysInYear() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.WHOLE_TERM;
+ Integer numberOfRepayments = 4;
+ Integer repaymentEvery = 1;
+ PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.WEEKS;
+ DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, numberOfRepayments,
+ repaymentEvery, repaymentFrequencyType, daysInYearType);
+
+ // Then
+ // ratePerPeriod = 10% / (4 * 1) = 2.5%
+ // annualRate = 2.5% * 52 = 130%
+ BigDecimal expectedAnnualRate = BigDecimal.valueOf(10.0).divide(BigDecimal.valueOf(4), 8, java.math.RoundingMode.HALF_EVEN)
+ .multiply(BigDecimal.valueOf(52));
+
+ assertEquals(0, expectedAnnualRate.compareTo(annualRate), "Annual rate for WEEKS should use 52 weeks");
+ }
+
+ /**
+ * Test WHOLE_TERM frequency with MONTHS repayment and DaysInYearType.ACTUAL
+ *
+ * Verify that ACTUAL doesn't affect non-DAYS repayment frequencies.
+ */
+ @Test
+ void testCalculateFrom_WholeTermFrequency_WithMonthsRepaymentAndActualDaysInYear() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(12.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.WHOLE_TERM;
+ Integer numberOfRepayments = 6;
+ Integer repaymentEvery = 1;
+ PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.MONTHS;
+ DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, numberOfRepayments,
+ repaymentEvery, repaymentFrequencyType, daysInYearType);
+
+ // Then
+ // ratePerPeriod = 12% / (6 * 1) = 2%
+ // annualRate = 2% * 12 = 24%
+ BigDecimal expectedAnnualRate = BigDecimal.valueOf(12.0).divide(BigDecimal.valueOf(6), 8, java.math.RoundingMode.HALF_EVEN)
+ .multiply(BigDecimal.valueOf(12));
+
+ assertEquals(0, expectedAnnualRate.compareTo(annualRate), "Annual rate for MONTHS should use 12 months");
+ }
+
+ /**
+ * Test WEEKS frequency
+ */
+ @Test
+ void testCalculateFrom_WeeksFrequency() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(2.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.WEEKS;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.WEEKS,
+ DaysInYearType.ACTUAL);
+
+ // Then
+ // Annual rate should be 2% * 52 = 104%
+ assertEquals(0, BigDecimal.valueOf(104.0).compareTo(annualRate), "Annual rate for WEEKS should multiply by 52");
+ }
+
+ /**
+ * Test MONTHS frequency
+ */
+ @Test
+ void testCalculateFrom_MonthsFrequency() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(2.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.MONTHS;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.MONTHS,
+ DaysInYearType.ACTUAL);
+
+ // Then
+ // Annual rate should be 2% * 12 = 24%
+ assertEquals(0, BigDecimal.valueOf(24.0).compareTo(annualRate), "Annual rate for MONTHS should multiply by 12");
+ }
+
+ /**
+ * Test YEARS frequency
+ */
+ @Test
+ void testCalculateFrom_YearsFrequency() {
+ // Given
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(5.0);
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.YEARS;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, 1, 1, PeriodFrequencyType.YEARS,
+ DaysInYearType.ACTUAL);
+
+ // Then
+ // Annual rate should be 5% * 1 = 5%
+ assertEquals(0, BigDecimal.valueOf(5.0).compareTo(annualRate), "Annual rate for YEARS should multiply by 1");
+ }
+
+ /**
+ * Test bug scenario with realistic values from FINERACT-2492
+ *
+ * Principal: 3,400
+ * Interest rate: 10% WHOLE_TERM
+ * Repayments: 3 daily
+ * Expected interest per installment: 113.33
+ */
+ @Test
+ void testCalculateFrom_BugReproductionScenario() {
+ // Given
+ when(paymentPeriodsInOneYearCalculator.calculate(PeriodFrequencyType.DAYS)).thenReturn(365);
+
+ BigDecimal interestRatePerPeriod = BigDecimal.valueOf(10.0); // 10% whole term
+ PeriodFrequencyType interestFrequencyType = PeriodFrequencyType.WHOLE_TERM;
+ Integer numberOfRepayments = 3;
+ Integer repaymentEvery = 1;
+ PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.DAYS;
+ DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+
+ // When
+ BigDecimal annualRate = aprCalculator.calculateFrom(interestFrequencyType, interestRatePerPeriod, numberOfRepayments,
+ repaymentEvery, repaymentFrequencyType, daysInYearType);
+
+ // Then
+ // The bug was causing annual rate to be 3.333% instead of 1216.667%
+ // Verify it's much greater than 100 (definitely not 3.333)
+ assertEquals(true, annualRate.compareTo(BigDecimal.valueOf(1000)) > 0,
+ "Annual rate should be > 1000% (bug was producing 3.333%)");
+
+ // Verify exact expected value: 10/3 * 365 = 1216.66666667
+ BigDecimal expectedRate = BigDecimal.valueOf(10.0).divide(BigDecimal.valueOf(3), 8, java.math.RoundingMode.HALF_EVEN)
+ .multiply(BigDecimal.valueOf(365));
+ assertEquals(0, expectedRate.compareTo(annualRate), "Annual rate should be exactly 1216.67%");
+ }
+}
diff --git a/fineract-mix/build.gradle b/fineract-mix/build.gradle
new file mode 100644
index 00000000000..bbde3766997
--- /dev/null
+++ b/fineract-mix/build.gradle
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+description = 'Fineract Mix'
+
+apply plugin: 'java'
+apply plugin: 'eclipse'
+
+compileJava {
+ dependsOn ':fineract-avro-schemas:buildJavaSdk'
+}
+
+configurations {
+ providedRuntime // needed for Spring Boot executable WAR
+ providedCompile
+ compile() {
+ exclude module: 'hibernate-entitymanager'
+ exclude module: 'hibernate-validator'
+ exclude module: 'activation'
+ exclude module: 'bcmail-jdk14'
+ exclude module: 'bcprov-jdk14'
+ exclude module: 'bctsp-jdk14'
+ exclude module: 'c3p0'
+ exclude module: 'stax-api'
+ exclude module: 'jaxb-api'
+ exclude module: 'jaxb-impl'
+ exclude module: 'jboss-logging'
+ exclude module: 'itext-rtf'
+ exclude module: 'classworlds'
+ }
+ runtime
+}
+
+apply from: 'dependencies.gradle'
+
+// Configuration for the modernizer plugin
+// https://github.com/andygoossens/gradle-modernizer-plugin
+modernizer {
+ ignoreClassNamePatterns = [
+ '.*AbstractPersistableCustom',
+ '.*EntityTables',
+ '.*domain.*'
+ ]
+}
+
+// If we are running Gradle within Eclipse to enhance classes with OpenJPA,
+// set the classes directory to point to Eclipse's default build directory
+if (project.hasProperty('env') && project.getProperty('env') == 'eclipse') {
+ sourceSets.main.java.outputDir = new File(rootProject.projectDir, "fineract-core/bin/main")
+}
+
+if (!(project.hasProperty('env') && project.getProperty('env') == 'dev')) {
+ sourceSets {
+ test {
+ java {
+ exclude '**/core/boot/tests/**'
+ }
+ }
+ }
+}
diff --git a/fineract-mix/dependencies.gradle b/fineract-mix/dependencies.gradle
new file mode 100644
index 00000000000..cf823616690
--- /dev/null
+++ b/fineract-mix/dependencies.gradle
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dependencies {
+ // Never use "compile" scope, but make all dependencies either 'implementation', 'runtimeOnly' or 'testCompile'.
+ // Note that we never use 'api', because Fineract at least currently is a simple monolithic application ("WAR"), not a library.
+ // We also (normally should have) no need to ever use 'compileOnly'.
+
+ // implementation dependencies are directly used (compiled against) in src/main (and src/test)
+ //
+ implementation(
+ project(path: ':fineract-core'),
+ project(path: ':fineract-command'),
+ )
+
+ implementation(
+ 'org.springframework.boot:spring-boot-starter-web',
+ 'org.springframework.boot:spring-boot-starter-validation',
+ 'org.springframework.boot:spring-boot-starter-security',
+ 'org.springframework.boot:spring-boot-starter-data-jdbc',
+ 'jakarta.ws.rs:jakarta.ws.rs-api',
+
+ 'org.apache.commons:commons-lang3',
+
+ 'com.google.guava:guava',
+ // TODO: try to get rid of this as soon as we get hands on the MIX XSD file from 2009; then we can use JAXB
+ 'com.google.code.gson:gson',
+
+ 'com.github.spotbugs:spotbugs-annotations',
+ 'io.swagger.core.v3:swagger-annotations-jakarta',
+
+ 'org.mapstruct:mapstruct',
+
+ 'io.github.resilience4j:resilience4j-spring-boot3',
+ )
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ annotationProcessor 'org.mapstruct:mapstruct-processor'
+ implementation('org.dom4j:dom4j') {
+ exclude group: 'javax.xml.bind'
+ }
+ // testCompile dependencies are ONLY used in src/test, not src/main.
+ // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
+ //
+ testImplementation( 'io.github.classgraph:classgraph' )
+ testImplementation ('org.springframework.boot:spring-boot-starter-test') {
+ exclude group: 'com.jayway.jsonpath', module: 'json-path'
+ exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
+ exclude group: 'jakarta.activation'
+ exclude group: 'javax.activation'
+ exclude group: 'org.skyscreamer'
+ }
+ testImplementation ('org.mockito:mockito-inline')
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java
similarity index 78%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java
index bbdccc2b481..b381b587049 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java
@@ -26,27 +26,28 @@
import jakarta.ws.rs.core.MediaType;
import java.sql.Date;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.mix.data.XBRLData;
-import org.apache.fineract.mix.service.XBRLBuilder;
-import org.apache.fineract.mix.service.XBRLResultService;
+import org.apache.fineract.mix.service.MixReportXBRLBuilder;
+import org.apache.fineract.mix.service.MixReportXBRLResultService;
import org.springframework.stereotype.Component;
@Path("/v1/mixreport")
@Component
-@Tag(name = "Mix Report", description = "")
+@Tag(name = "Mix Report", description = """
+ """)
@RequiredArgsConstructor
public class MixReportApiResource {
- private final XBRLResultService xbrlResultService;
- private final XBRLBuilder xbrlBuilder;
+ private final MixReportXBRLResultService xbrlResultService;
+ private final MixReportXBRLBuilder xbrlBuilder;
@GET
@Produces({ MediaType.APPLICATION_XML })
public String retrieveXBRLReport(@QueryParam("startDate") final Date startDate, @QueryParam("endDate") final Date endDate,
@QueryParam("currency") final String currency) {
- final XBRLData data = this.xbrlResultService.getXBRLResult(startDate, endDate, currency);
+ final var data = xbrlResultService.getXBRLResult(startDate, endDate, currency);
+ // TODO: make this type safe?
return this.xbrlBuilder.build(data);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java
similarity index 79%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java
index f53b3171958..bb036cd8f50 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyApiResource.java
@@ -26,9 +26,8 @@
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.mix.data.MixTaxonomyData;
-import org.apache.fineract.mix.service.MixTaxonomyReadPlatformService;
+import org.apache.fineract.mix.service.MixTaxonomyReadService;
import org.springframework.stereotype.Component;
@Path("/v1/mixtaxonomy")
@@ -37,17 +36,12 @@
@RequiredArgsConstructor
public class MixTaxonomyApiResource {
- private final PlatformSecurityContext context;
- private final MixTaxonomyReadPlatformService readTaxonomyService;
+ private final MixTaxonomyReadService readTaxonomyService;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public List retrieveAll() {
-
- // FIXME - KW - no check for permission to read mix taxonomy data.
- this.context.authenticatedUser();
-
return readTaxonomyService.retrieveAll();
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java
similarity index 53%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java
index 1b259780e64..7ad31ccbada 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixTaxonomyMappingApiResource.java
@@ -25,16 +25,14 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
+import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.commands.domain.CommandWrapper;
-import org.apache.fineract.commands.service.CommandWrapperBuilder;
-import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
-import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.command.core.CommandPipeline;
+import org.apache.fineract.mix.command.MixTaxonomyMappingUpdateCommand;
import org.apache.fineract.mix.data.MixTaxonomyMappingData;
-import org.apache.fineract.mix.data.MixTaxonomyRequest;
-import org.apache.fineract.mix.service.MixTaxonomyMappingReadPlatformService;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateResponse;
+import org.apache.fineract.mix.service.MixTaxonomyMappingReadService;
import org.springframework.stereotype.Component;
@Path("/v1/mixmapping")
@@ -43,31 +41,31 @@
@RequiredArgsConstructor
public class MixTaxonomyMappingApiResource {
- private final PlatformSecurityContext context;
- private final ToApiJsonSerializer toApiJsonSerializer;
- private final MixTaxonomyMappingReadPlatformService readTaxonomyMappingService;
- private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
+ private final MixTaxonomyMappingReadService readTaxonomyMappingService;
+ private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public MixTaxonomyMappingData retrieveTaxonomyMapping() {
- this.context.authenticatedUser();
return this.readTaxonomyMappingService.retrieveTaxonomyMapping();
}
@PUT
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- public String updateTaxonomyMapping(final MixTaxonomyRequest mixTaxonomyRequest) {
- // TODO support multiple configuration file loading
- final Long mappingId = (long) 1;
- final CommandWrapper commandRequest = new CommandWrapperBuilder().updateTaxonomyMapping(mappingId)
- .withJson(toApiJsonSerializer.serialize(mixTaxonomyRequest)).build();
+ public MixTaxonomyMappingUpdateResponse updateTaxonomyMapping(final MixTaxonomyMappingUpdateRequest request) {
+ // TODO support multiple configuration file loading; this is the legacy behavior
+ if (request.getId() == null) {
+ request.setId(1L);
+ }
- final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+ final var command = new MixTaxonomyMappingUpdateCommand();
- return this.toApiJsonSerializer.serialize(result);
- }
+ command.setPayload(request);
+
+ final Supplier response = commandPipeline.send(command);
+ return response.get();
+ }
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/command/MixTaxonomyMappingUpdateCommand.java b/fineract-mix/src/main/java/org/apache/fineract/mix/command/MixTaxonomyMappingUpdateCommand.java
new file mode 100644
index 00000000000..fa59fe6a50f
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/command/MixTaxonomyMappingUpdateCommand.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.command;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MixTaxonomyMappingUpdateCommand extends Command {}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLContextData.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLContextData.java
new file mode 100644
index 00000000000..d70d6abe5ff
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLContextData.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class MixReportXBRLContextData implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String dimensionType;
+ private String dimension;
+ private Integer periodType;
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/data/XBRLData.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLData.java
similarity index 76%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/data/XBRLData.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLData.java
index f53c1785c7b..a6f60929ac9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/data/XBRLData.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLData.java
@@ -18,19 +18,28 @@
*/
package org.apache.fineract.mix.data;
+import java.io.Serial;
+import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Date;
-import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
+@Builder
@Data
@NoArgsConstructor
+@AllArgsConstructor
@Accessors(chain = true)
-public class XBRLData {
+public class MixReportXBRLData implements Serializable {
- private HashMap resultMap;
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Map resultMap;
private Date startDate;
private Date endDate;
private String currency;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/data/NamespaceData.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLNamespaceData.java
similarity index 79%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/data/NamespaceData.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLNamespaceData.java
index 954301a560d..6e256bc663d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/data/NamespaceData.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLNamespaceData.java
@@ -18,18 +18,25 @@
*/
package org.apache.fineract.mix.data;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
+@Builder
@Data
@NoArgsConstructor
+@AllArgsConstructor
@Accessors(chain = true)
-public class NamespaceData {
+public class MixReportXBRLNamespaceData implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
- @SuppressWarnings("unused")
private Long id;
- @SuppressWarnings("unused")
private String prefix;
private String url;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java
similarity index 78%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java
index 57fc937da4f..857df912a33 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyData.java
@@ -19,29 +19,37 @@
package org.apache.fineract.mix.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
+@Builder
@Data
@NoArgsConstructor
+@AllArgsConstructor
@Accessors(chain = true)
-public class MixTaxonomyData {
+public class MixTaxonomyData implements Serializable {
public static final Integer PORTFOLIO = 0;
- public static final Integer BALANCESHEET = 1;
+ public static final Integer BALANCE_SHEET = 1;
public static final Integer INCOME = 2;
public static final Integer EXPENSE = 3;
- @SuppressWarnings("unused")
+ @Serial
+ private static final long serialVersionUID = 1L;
+
private Long id;
private String name;
private String namespace;
private String dimension;
private Integer type;
- @SuppressWarnings("unused")
private String description;
+ // TODO: why is this different from the PORTFOLIO constant? This doesn't seem right!
@JsonIgnore
public boolean isPortfolio() {
return this.type == 5;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java
similarity index 90%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java
index 7d52bba70b0..26e71b3a581 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingData.java
@@ -18,17 +18,20 @@
*/
package org.apache.fineract.mix.data;
-import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
+@Builder
@Data
@NoArgsConstructor
+@AllArgsConstructor
@Accessors(chain = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
public class MixTaxonomyMappingData {
private String identifier;
private String config;
+ private String currency;
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateRequest.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateRequest.java
new file mode 100644
index 00000000000..eeea823cd52
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateRequest.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MixTaxonomyMappingUpdateRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long id;
+ private String identifier;
+ private String config;
+ private String currency;
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyRequest.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateResponse.java
similarity index 89%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyRequest.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateResponse.java
index 4a37e7262d9..059f15bee42 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/data/MixTaxonomyRequest.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixTaxonomyMappingUpdateResponse.java
@@ -21,17 +21,18 @@
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
+@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
-public class MixTaxonomyRequest implements Serializable {
+public class MixTaxonomyMappingUpdateResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- private String identifier;
- private String config;
+ private Long entityId;
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespace.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespace.java
new file mode 100644
index 00000000000..d73813aeb3b
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespace.java
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.domain;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+@Table("mix_xbrl_namespace")
+@Getter
+@Setter
+@NoArgsConstructor
+@Accessors(chain = true)
+public class MixReportXBRLNamespace implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("id")
+ private Long id;
+ @Column("prefix")
+ private String prefix;
+ @Column("url")
+ private String url;
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespaceRepository.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespaceRepository.java
new file mode 100644
index 00000000000..a8806fa0464
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixReportXBRLNamespaceRepository.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.domain;
+
+import java.util.Optional;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+
+public interface MixReportXBRLNamespaceRepository
+ extends ListCrudRepository, QueryByExampleExecutor {
+
+ Optional findOneByPrefix(String prefix);
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomy.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomy.java
new file mode 100644
index 00000000000..79b3462a3f4
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomy.java
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.domain;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+@Table("mix_taxonomy")
+@Getter
+@Setter
+@NoArgsConstructor
+@Accessors(chain = true)
+public final class MixTaxonomy implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("id")
+ private Long id;
+
+ @Column("name")
+ private String name;
+
+ @Column("namespace_id")
+ private Long namespaceId;
+
+ @Column("dimension")
+ private String dimension;
+
+ @Column("type")
+ private Integer type;
+
+ @Column("description")
+ private String description;
+
+ // TODO: this is never used, but creates an error on MySQL (tinyint vs boolean mapping)
+ // @Column("need_mapping")
+ // private Boolean needMapping;
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMapping.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMapping.java
new file mode 100644
index 00000000000..30e4abc7cf5
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMapping.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.domain;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+@Table("mix_taxonomy_mapping")
+@Getter
+@Setter
+@NoArgsConstructor
+@Accessors(chain = true)
+public final class MixTaxonomyMapping implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column("id")
+ private Long id;
+
+ @Column("identifier")
+ private String identifier;
+
+ @Column("config")
+ private String config;
+
+ @Column("currency")
+ private String currency;
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java
similarity index 79%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java
index 44df787b4ae..35baecaab26 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyMappingRepository.java
@@ -18,10 +18,8 @@
*/
package org.apache.fineract.mix.domain;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
public interface MixTaxonomyMappingRepository
- extends JpaRepository, JpaSpecificationExecutor {
-
-}
+ extends ListCrudRepository, QueryByExampleExecutor {}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyRepository.java b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyRepository.java
new file mode 100644
index 00000000000..8b1fa29ad01
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/domain/MixTaxonomyRepository.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.domain;
+
+import java.util.List;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+
+public interface MixTaxonomyRepository extends ListCrudRepository, QueryByExampleExecutor {
+
+ List findAllByOrderByIdAsc();
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/exception/XBRLMappingInvalidException.java b/fineract-mix/src/main/java/org/apache/fineract/mix/exception/MixReportXBRLMappingInvalidException.java
similarity index 86%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/exception/XBRLMappingInvalidException.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/exception/MixReportXBRLMappingInvalidException.java
index 45390617db7..cf78409424f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/exception/XBRLMappingInvalidException.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/exception/MixReportXBRLMappingInvalidException.java
@@ -20,9 +20,9 @@
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
-public class XBRLMappingInvalidException extends AbstractPlatformDomainRuleException {
+public class MixReportXBRLMappingInvalidException extends AbstractPlatformDomainRuleException {
- public XBRLMappingInvalidException(final String msg) {
+ public MixReportXBRLMappingInvalidException(final String msg) {
super("error.msg.xbrl.report.mapping.invalid.id", "Mapping does not exist", msg);
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/handler/MixTaxonomyMappingUpdateCommandHandler.java b/fineract-mix/src/main/java/org/apache/fineract/mix/handler/MixTaxonomyMappingUpdateCommandHandler.java
new file mode 100644
index 00000000000..af59efdd713
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/handler/MixTaxonomyMappingUpdateCommandHandler.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.handler;
+
+import io.github.resilience4j.retry.annotation.Retry;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.command.core.Command;
+import org.apache.fineract.command.core.CommandHandler;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateResponse;
+import org.apache.fineract.mix.service.MixTaxonomyMappingWriteService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MixTaxonomyMappingUpdateCommandHandler
+ implements CommandHandler {
+
+ private final MixTaxonomyMappingWriteService writeTaxonomyService;
+
+ @Retry(name = "commandMixTaxonomyMappingUpdate", fallbackMethod = "fallback")
+ @Transactional
+ @Override
+ public MixTaxonomyMappingUpdateResponse handle(Command command) {
+ return writeTaxonomyService.updateMapping(command.getPayload());
+ }
+
+ @Override
+ public MixTaxonomyMappingUpdateResponse fallback(Command command, Throwable t) {
+ return CommandHandler.super.fallback(command, t);
+ }
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixReportXBRLNamespaceMapper.java b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixReportXBRLNamespaceMapper.java
new file mode 100644
index 00000000000..bbd88503e78
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixReportXBRLNamespaceMapper.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.mapping;
+
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.mix.data.MixReportXBRLNamespaceData;
+import org.apache.fineract.mix.domain.MixReportXBRLNamespace;
+import org.mapstruct.Mapper;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface MixReportXBRLNamespaceMapper {
+
+ MixReportXBRLNamespaceData map(MixReportXBRLNamespace source);
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMapper.java b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMapper.java
new file mode 100644
index 00000000000..ad23c6dd93d
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMapper.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.mapping;
+
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.mix.data.MixTaxonomyData;
+import org.apache.fineract.mix.domain.MixTaxonomy;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface MixTaxonomyMapper {
+
+ @Mapping(ignore = true, target = "namespace")
+ MixTaxonomyData map(MixTaxonomy source);
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingMapper.java b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingMapper.java
new file mode 100644
index 00000000000..ac61c5eb492
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingMapper.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.mapping;
+
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.mix.data.MixTaxonomyMappingData;
+import org.apache.fineract.mix.domain.MixTaxonomyMapping;
+import org.mapstruct.Mapper;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface MixTaxonomyMappingMapper {
+
+ MixTaxonomyMappingData map(MixTaxonomyMapping source);
+}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingUpdateRequestMapper.java b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingUpdateRequestMapper.java
new file mode 100644
index 00000000000..a985c30a625
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/mapping/MixTaxonomyMappingUpdateRequestMapper.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.mapping;
+
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+import org.apache.fineract.mix.domain.MixTaxonomyMapping;
+import org.mapstruct.Mapper;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface MixTaxonomyMappingUpdateRequestMapper {
+
+ MixTaxonomyMapping map(MixTaxonomyMappingUpdateRequest source);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLBuilder.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java
similarity index 80%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLBuilder.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java
index 6af912acbc5..1d97f3210aa 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLBuilder.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java
@@ -25,36 +25,40 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.apache.fineract.mix.data.ContextData;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixReportXBRLContextData;
+import org.apache.fineract.mix.data.MixReportXBRLData;
+import org.apache.fineract.mix.data.MixReportXBRLNamespaceData;
import org.apache.fineract.mix.data.MixTaxonomyData;
-import org.apache.fineract.mix.data.NamespaceData;
-import org.apache.fineract.mix.data.XBRLData;
-import org.apache.fineract.mix.exception.XBRLMappingInvalidException;
+import org.apache.fineract.mix.exception.MixReportXBRLMappingInvalidException;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+@Slf4j
+@RequiredArgsConstructor
@Component
-public class XBRLBuilder {
+public class MixReportXBRLBuilder {
+ // NOTE: see https://www.xbrl.org/taxonomyrecognition/mx_2009-06-19_summary-page.htm
private static final String SCHEME_URL = "http://www.themix.org";
private static final String IDENTIFIER = "000000";
private static final String UNITID_PURE = "Unit1";
private static final String UNITID_CUR = "Unit2";
- @Autowired
- private NamespaceReadPlatformService readNamespaceService;
+ private final MixReportXBRLNamespaceReadService readNamespaceService;
- public String build(final XBRLData xbrlData) {
+ // TODO: we should do this with JAXB
+ public String build(final MixReportXBRLData xbrlData) {
return this.build(xbrlData.getResultMap(), xbrlData.getStartDate(), xbrlData.getEndDate(), xbrlData.getCurrency());
}
public String build(final Map map, final Date startDate, final Date endDate, final String currency) {
Integer instantScenarioCounter = 0;
Integer durationScenarioCounter = 0;
- Map contextMap = new HashMap<>();
+ Map contextMap = new HashMap<>();
final Document doc = DocumentHelper.createDocument();
Element root = doc.addElement("xbrl");
@@ -79,17 +83,17 @@ public String build(final Map map, final Date start
private Element addTaxonomy(final Element rootElement, final MixTaxonomyData taxonomy, final BigDecimal value, final Date startDate,
final Date endDate, Integer instantScenarioCounter, Integer durationScenarioCounter,
- final Map contextMap) {
+ final Map contextMap) {
// throw an error is start / endate is null
if (startDate == null || endDate == null) {
- throw new XBRLMappingInvalidException("start date and end date should not be null");
+ throw new MixReportXBRLMappingInvalidException("start date and end date should not be null");
}
final String prefix = taxonomy.getNamespace();
String qname = taxonomy.getName();
if (prefix != null && !prefix.isEmpty()) {
- final NamespaceData ns = this.readNamespaceService.retrieveNamespaceByPrefix(prefix);
+ final MixReportXBRLNamespaceData ns = this.readNamespaceService.retrieveNamespaceByPrefix(prefix);
if (ns != null) {
rootElement.addNamespace(prefix, ns.getUrl());
@@ -102,20 +106,20 @@ private Element addTaxonomy(final Element rootElement, final MixTaxonomyData tax
final String dimension = taxonomy.getDimension();
final SimpleDateFormat timeFormat = new SimpleDateFormat("MM_dd_yyyy");
- ContextData context = null;
+ MixReportXBRLContextData context = null;
if (dimension != null) {
final List dims = Splitter.on(':').splitToList(dimension);
if (dims.size() == 2) {
- context = new ContextData().setDimensionType(dims.get(0)).setDimension(dims.get(1)).setPeriodType(
- taxonomy.getType().equals(MixTaxonomyData.BALANCESHEET) || taxonomy.getType().equals(MixTaxonomyData.PORTFOLIO) ? 0
+ context = new MixReportXBRLContextData().setDimensionType(dims.get(0)).setDimension(dims.get(1)).setPeriodType(
+ taxonomy.getType().equals(MixTaxonomyData.BALANCE_SHEET) || taxonomy.getType().equals(MixTaxonomyData.PORTFOLIO) ? 0
: 1);
}
}
if (context == null) {
- context = new ContextData().setPeriodType(
- taxonomy.getType().equals(MixTaxonomyData.BALANCESHEET) || taxonomy.getType().equals(MixTaxonomyData.PORTFOLIO) ? 0
+ context = new MixReportXBRLContextData().setPeriodType(
+ taxonomy.getType().equals(MixTaxonomyData.BALANCE_SHEET) || taxonomy.getType().equals(MixTaxonomyData.PORTFOLIO) ? 0
: 1);
}
@@ -169,10 +173,11 @@ private void addCurrencyUnit(final Element root, final String currencyCode) {
}
- private void addContexts(final Element root, final Date startDate, final Date endDate, final Map contextMap) {
+ private void addContexts(final Element root, final Date startDate, final Date endDate,
+ final Map contextMap) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
- for (final Map.Entry entry : contextMap.entrySet()) {
- final ContextData context = entry.getKey();
+ for (final Map.Entry entry : contextMap.entrySet()) {
+ final MixReportXBRLContextData context = entry.getKey();
final Element contextElement = root.addElement("context");
contextElement.addAttribute("id", entry.getValue());
contextElement.addElement("entity").addElement("identifier").addAttribute("scheme", SCHEME_URL).addText(IDENTIFIER);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/NamespaceReadPlatformService.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadService.java
similarity index 80%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/NamespaceReadPlatformService.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadService.java
index aa5175406e8..8296fc6c164 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/NamespaceReadPlatformService.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadService.java
@@ -18,11 +18,9 @@
*/
package org.apache.fineract.mix.service;
-import org.apache.fineract.mix.data.NamespaceData;
+import org.apache.fineract.mix.data.MixReportXBRLNamespaceData;
-public interface NamespaceReadPlatformService {
+public interface MixReportXBRLNamespaceReadService {
- NamespaceData retrieveNamespaceById(Long id);
-
- NamespaceData retrieveNamespaceByPrefix(String prefix);
+ MixReportXBRLNamespaceData retrieveNamespaceByPrefix(String prefix);
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadServiceImpl.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadServiceImpl.java
new file mode 100644
index 00000000000..7a4c1f998e8
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLNamespaceReadServiceImpl.java
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixReportXBRLNamespaceData;
+import org.apache.fineract.mix.domain.MixReportXBRLNamespaceRepository;
+import org.apache.fineract.mix.mapping.MixReportXBRLNamespaceMapper;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class MixReportXBRLNamespaceReadServiceImpl implements MixReportXBRLNamespaceReadService {
+
+ private final MixReportXBRLNamespaceRepository repository;
+ private final MixReportXBRLNamespaceMapper mapper;
+
+ @Override
+ public MixReportXBRLNamespaceData retrieveNamespaceByPrefix(final String prefix) {
+ return repository.findOneByPrefix(prefix).map(mapper::map).orElse(null);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultService.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultService.java
similarity index 82%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultService.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultService.java
index 585b3e46c47..9e4fb0f4624 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultService.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultService.java
@@ -19,10 +19,10 @@
package org.apache.fineract.mix.service;
import java.sql.Date;
-import org.apache.fineract.mix.data.XBRLData;
+import org.apache.fineract.mix.data.MixReportXBRLData;
-public interface XBRLResultService {
+public interface MixReportXBRLResultService {
- XBRLData getXBRLResult(Date startDate, Date endDate, String currency);
+ MixReportXBRLData getXBRLResult(Date startDate, Date endDate, String currency);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultServiceImpl.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultServiceImpl.java
similarity index 56%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultServiceImpl.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultServiceImpl.java
index 0628a4f6a89..17eb95cc868 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/XBRLResultServiceImpl.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLResultServiceImpl.java
@@ -31,45 +31,47 @@
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixReportXBRLData;
import org.apache.fineract.mix.data.MixTaxonomyData;
import org.apache.fineract.mix.data.MixTaxonomyMappingData;
-import org.apache.fineract.mix.data.XBRLData;
-import org.apache.fineract.mix.exception.XBRLMappingInvalidException;
+import org.apache.fineract.mix.exception.MixReportXBRLMappingInvalidException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.stereotype.Component;
-@Component
@Slf4j
-public class XBRLResultServiceImpl implements XBRLResultService {
+@Component
+public class MixReportXBRLResultServiceImpl implements MixReportXBRLResultService {
private static final ScriptEngine SCRIPT_ENGINE = new ScriptEngineManager().getEngineByName("JavaScript");
- private final MixTaxonomyMappingReadPlatformService readTaxonomyMappingService;
- private final MixTaxonomyReadPlatformService readTaxonomyService;
+ private final MixTaxonomyMappingReadService readTaxonomyMappingService;
+ private final MixTaxonomyReadService readTaxonomyService;
private final JdbcTemplate jdbcTemplate;
@Autowired
- public XBRLResultServiceImpl(final JdbcTemplate jdbcTemplate, final MixTaxonomyMappingReadPlatformService readTaxonomyMappingService,
- final MixTaxonomyReadPlatformService readTaxonomyService) {
+ public MixReportXBRLResultServiceImpl(final JdbcTemplate jdbcTemplate, final MixTaxonomyMappingReadService readTaxonomyMappingService,
+ final MixTaxonomyReadService readTaxonomyService) {
this.readTaxonomyMappingService = readTaxonomyMappingService;
this.readTaxonomyService = readTaxonomyService;
this.jdbcTemplate = jdbcTemplate;
}
@Override
- public XBRLData getXBRLResult(final Date startDate, final Date endDate, final String currency) {
+ public MixReportXBRLData getXBRLResult(final Date startDate, final Date endDate, final String currency) {
- final HashMap config = retrieveTaxonomyConfig(startDate, endDate);
- if (config == null || config.size() == 0) {
- throw new XBRLMappingInvalidException("Mapping is empty");
+ final Map config = retrieveTaxonomyConfig(startDate, endDate);
+
+ if (config == null || config.isEmpty()) {
+ throw new MixReportXBRLMappingInvalidException("Mapping is empty");
}
- return new XBRLData().setResultMap(config).setStartDate(startDate).setEndDate(endDate).setCurrency(currency);
+
+ return new MixReportXBRLData().setResultMap(config).setStartDate(startDate).setEndDate(endDate).setCurrency(currency);
}
@SuppressWarnings("unchecked")
- private HashMap retrieveTaxonomyConfig(final Date startDate, final Date endDate) {
+ private Map retrieveTaxonomyConfig(final Date startDate, final Date endDate) {
final MixTaxonomyMappingData taxonomyMapping = this.readTaxonomyMappingService.retrieveTaxonomyMapping();
if (taxonomyMapping == null) {
return null;
@@ -77,7 +79,7 @@ private HashMap retrieveTaxonomyConfig(final Date s
final String config = taxonomyMapping.getConfig();
if (config != null) {
//
- HashMap configMap = new HashMap<>();
+ Map configMap = new HashMap<>();
configMap = new Gson().fromJson(config, configMap.getClass());
if (configMap == null) {
return null;
@@ -96,41 +98,34 @@ private HashMap retrieveTaxonomyConfig(final Date s
return null;
}
+ // TODO: this should at least use prepared statements and not just string concatenate the date objects!
private String getAccountSql(final Date startDate, final Date endDate) {
- return "select debits.glcode as 'glcode', debits.name as 'name', coalesce(debits.debitamount,0)-coalesce(credits.creditamount,0)) as 'balance' "
- + "from (select acc_gl_account.gl_code as 'glcode',name,sum(amount) as 'debitamount' "
- + "from acc_gl_journal_entry,acc_gl_account " + "where acc_gl_account.id = acc_gl_journal_entry.account_id "
- + "and acc_gl_journal_entry.type_enum=2 " + "and acc_gl_journal_entry.entry_date <= " + endDate
- + " and acc_gl_journal_entry.entry_date > " + startDate
- // "and (acc_gl_journal_entry.office_id=${branch} or
- // ${branch}=1) "
- // +
- + " group by glcode " + "order by glcode) debits " + "LEFT OUTER JOIN "
- + "(select acc_gl_account.gl_code as 'glcode',name,sum(amount) as 'creditamount' "
- + "from acc_gl_journal_entry,acc_gl_account " + "where acc_gl_account.id = acc_gl_journal_entry.account_id "
- + "and acc_gl_journal_entry.type_enum=1 " + "and acc_gl_journal_entry.entry_date <= " + endDate
- + " and acc_gl_journal_entry.entry_date > " + startDate
- // "and (acc_gl_journal_entry.office_id=${branch} or
- // ${branch}=1) "
- // +
- + " group by glcode " + "order by glcode) credits " + "on debits.glcode=credits.glcode " + "union "
- + "select credits.glcode as 'glcode', credits.name as 'name', coalesce(debits.debitamount,0)-coalesce(credits.creditamount,0)) as 'balance' "
- + "from (select acc_gl_account.gl_code as 'glcode',name,sum(amount) as 'debitamount' "
- + "from acc_gl_journal_entry,acc_gl_account " + "where acc_gl_account.id = acc_gl_journal_entry.account_id "
- + "and acc_gl_journal_entry.type_enum=2 " + "and acc_gl_journal_entry.entry_date <= " + endDate
- + " and acc_gl_journal_entry.entry_date > " + startDate
- // "and (acc_gl_journal_entry.office_id=${branch} or
- // ${branch}=1) "
- // +
- + " group by glcode " + "order by glcode) debits " + "RIGHT OUTER JOIN "
- + "(select acc_gl_account.gl_code as 'glcode',name,sum(amount) as 'creditamount' "
- + "from acc_gl_journal_entry,acc_gl_account " + "where acc_gl_account.id = acc_gl_journal_entry.account_id "
- + "and acc_gl_journal_entry.type_enum=1 " + "and acc_gl_journal_entry.entry_date <= " + endDate
- + " and acc_gl_journal_entry.entry_date > " + startDate
- // "and (acc_gl_journal_entry.office_id=${branch} or
- // ${branch}=1) "
- // +
- + " group by name, glcode " + "order by glcode) credits " + "on debits.glcode=credits.glcode;";
+ return "SELECT debits.glcode AS 'glcode', debits.name AS 'name', COALESCE(debits.debitamount,0)-COALESCE(credits.creditamount,0)) AS 'balance' "
+ + "FROM (SELECT acc_gl_account.gl_code AS 'glcode',name,SUM(amount) AS 'debitamount' "
+ + "FROM acc_gl_journal_entry,acc_gl_account WHERE acc_gl_account.id = acc_gl_journal_entry.account_id "
+ + "AND acc_gl_journal_entry.type_enum=2 AND acc_gl_journal_entry.entry_date <= " + endDate
+ + " AND acc_gl_journal_entry.entry_date > " + startDate
+ //
+ + " GROUP BY glcode ORDER BY glcode) debits LEFT OUTER JOIN "
+ + "(SELECT acc_gl_account.gl_code AS 'glcode',name,SUM(amount) AS 'creditamount' "
+ + "FROM acc_gl_journal_entry,acc_gl_account WHERE acc_gl_account.id = acc_gl_journal_entry.account_id "
+ + "AND acc_gl_journal_entry.type_enum=1 AND acc_gl_journal_entry.entry_date <= " + endDate
+ + " AND acc_gl_journal_entry.entry_date > " + startDate
+ //
+ + " GROUP BY glcode ORDER BY glcode) credits ON debits.glcode=credits.glcode UNION "
+ + "SELECT credits.glcode AS 'glcode', credits.name AS 'name', COALESCE(debits.debitamount,0)-COALESCE(credits.creditamount,0)) AS 'balance' "
+ + "FROM (SELECT acc_gl_account.gl_code AS 'glcode',name,SUM(amount) AS 'debitamount' "
+ + "FROM acc_gl_journal_entry,acc_gl_account WHERE acc_gl_account.id = acc_gl_journal_entry.account_id "
+ + "AND acc_gl_journal_entry.type_enum=2 AND acc_gl_journal_entry.entry_date <= " + endDate
+ + " AND acc_gl_journal_entry.entry_date > " + startDate
+ //
+ + " GROUP BY glcode ORDER BY glcode) debits RIGHT OUTER JOIN "
+ + "(SELECT acc_gl_account.gl_code AS 'glcode',name,SUM(amount) AS 'creditamount' "
+ + "FROM acc_gl_journal_entry,acc_gl_account WHERE acc_gl_account.id = acc_gl_journal_entry.account_id "
+ + "AND acc_gl_journal_entry.type_enum=1 AND acc_gl_journal_entry.entry_date <= " + endDate
+ + " AND acc_gl_journal_entry.entry_date > " + startDate
+ //
+ + " GROUP BY name, glcode ORDER BY glcode) credits ON debits.glcode=credits.glcode;";
}
private Map setupBalanceMap(final String sql) {
@@ -156,6 +151,7 @@ private BigDecimal processMappingString(Map accountBalanceMa
// evaluate the expression
float eval = 0f;
try {
+ // TODO: this doesn't work anymore in modern JVMs!!!!
final Number value = (Number) SCRIPT_ENGINE.eval(mappingString);
if (value != null) {
eval = value.floatValue();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadPlatformService.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadService.java
similarity index 94%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadPlatformService.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadService.java
index 38bbf8f5b1a..32c0f6c8fb5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadPlatformService.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadService.java
@@ -20,7 +20,7 @@
import org.apache.fineract.mix.data.MixTaxonomyMappingData;
-public interface MixTaxonomyMappingReadPlatformService {
+public interface MixTaxonomyMappingReadService {
MixTaxonomyMappingData retrieveTaxonomyMapping();
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadServiceImpl.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadServiceImpl.java
new file mode 100644
index 00000000000..4047f1d09ef
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingReadServiceImpl.java
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixTaxonomyMappingData;
+import org.apache.fineract.mix.domain.MixTaxonomyMappingRepository;
+import org.apache.fineract.mix.mapping.MixTaxonomyMappingMapper;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class MixTaxonomyMappingReadServiceImpl implements MixTaxonomyMappingReadService {
+
+ private final MixTaxonomyMappingRepository repository;
+ private final MixTaxonomyMappingMapper mapper;
+
+ @Override
+ public MixTaxonomyMappingData retrieveTaxonomyMapping() {
+ return repository.findAll().stream().findFirst().map(mapper::map).orElse(null);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWritePlatformService.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteService.java
similarity index 75%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWritePlatformService.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteService.java
index 77fa2929a83..1c91559b6bd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWritePlatformService.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteService.java
@@ -18,10 +18,10 @@
*/
package org.apache.fineract.mix.service;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateResponse;
-public interface MixTaxonomyMappingWritePlatformService {
+public interface MixTaxonomyMappingWriteService {
- CommandProcessingResult updateMapping(Long mappingId, JsonCommand command);
+ MixTaxonomyMappingUpdateResponse updateMapping(MixTaxonomyMappingUpdateRequest request);
}
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteServiceImpl.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteServiceImpl.java
new file mode 100644
index 00000000000..4876476fa62
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyMappingWriteServiceImpl.java
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.service;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateRequest;
+import org.apache.fineract.mix.data.MixTaxonomyMappingUpdateResponse;
+import org.apache.fineract.mix.domain.MixTaxonomyMappingRepository;
+import org.apache.fineract.mix.mapping.MixTaxonomyMappingUpdateRequestMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class MixTaxonomyMappingWriteServiceImpl implements MixTaxonomyMappingWriteService {
+
+ private final MixTaxonomyMappingRepository repository;
+ private final MixTaxonomyMappingUpdateRequestMapper mapper;
+
+ @Transactional
+ @Override
+ public MixTaxonomyMappingUpdateResponse updateMapping(@Valid final MixTaxonomyMappingUpdateRequest request) {
+ final var taxonomyMapping = mapper.map(request);
+
+ repository.save(taxonomyMapping);
+
+ return MixTaxonomyMappingUpdateResponse.builder().entityId(taxonomyMapping.getId()).build();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadPlatformService.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadService.java
similarity index 95%
rename from fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadPlatformService.java
rename to fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadService.java
index dd4525e57ec..e541366c3a7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadPlatformService.java
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadService.java
@@ -21,7 +21,7 @@
import java.util.List;
import org.apache.fineract.mix.data.MixTaxonomyData;
-public interface MixTaxonomyReadPlatformService {
+public interface MixTaxonomyReadService {
List retrieveAll();
diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadServiceImpl.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadServiceImpl.java
new file mode 100644
index 00000000000..152055967fb
--- /dev/null
+++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixTaxonomyReadServiceImpl.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.mix.service;
+
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.mix.data.MixTaxonomyData;
+import org.apache.fineract.mix.domain.MixTaxonomyRepository;
+import org.apache.fineract.mix.mapping.MixTaxonomyMapper;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class MixTaxonomyReadServiceImpl implements MixTaxonomyReadService {
+
+ private final MixTaxonomyRepository repository;
+ private final MixTaxonomyMapper mapper;
+
+ @Override
+ public List retrieveAll() {
+ return repository.findAllByOrderByIdAsc().stream().map(mapper::map).toList();
+ }
+
+ @Override
+ public MixTaxonomyData retrieveOne(final Long id) {
+ return repository.findById(id).map(mapper::map).orElse(null);
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index ea0aeedb42f..3bbb4b43abc 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -63,6 +63,7 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@@ -2917,6 +2918,7 @@ private Set getLoanChargesOfInstallment(final Set charge
private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings,
Balances balances) {
+ Loan loan = loanTransaction.getLoan();
VerticalPaymentAllocationContext paymentAllocationContext = new VerticalPaymentAllocationContext(ctx, loanTransaction,
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances);
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
@@ -2924,6 +2926,18 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
paymentAllocationContext.setAllocatedAmount(Money.zero(ctx.getCurrency()));
paymentAllocationContext.setInstallment(null);
paymentAllocationContext.setPaymentAllocationType(paymentAllocationType);
+ if (isInterestRecalculationSupported(ctx, loanTransaction.getLoan())) {
+ // Clear any previously skipped installments before re-evaluating
+ ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) ctx;
+ progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().clear();
+ paymentAllocationContext
+ .setInAdvanceInstallmentsFilteringRules(installment -> loanTransaction.isBefore(installment.getDueDate())
+ && installment.isOutstandingBalanceNotZero(paymentAllocationType.getAllocationType(), ctx.getCurrency())
+ && !progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().contains(installment));
+ } else {
+ paymentAllocationContext.setInAdvanceInstallmentsFilteringRules(getFilterPredicate(
+ paymentAllocationContext.getPaymentAllocationType(), paymentAllocationContext.getCtx().getCurrency()));
+ }
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
paymentAllocationContext, //
(VerticalPaymentAllocationContext context) -> context.getInstallment() != null
@@ -2944,11 +2958,20 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
context.getCtx().getCurrency());
- context.setAllocatedAmount(
- processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
- context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
- loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
- context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+
+ if (isInterestRecalculationSupported(context.getCtx(), loan)) {
+ context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
+ context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+ context.getBalances(), paymentAllocationType, context.getInstallment(),
+ (ProgressiveTransactionCtx) context.getCtx(), loanTransactionToRepaymentScheduleMapping,
+ oldestPastDueInstallmentCharges));
+ } else {
+ context.setAllocatedAmount(
+ processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
+ context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+ loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
+ context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+ }
context.setTransactionAmountUnprocessed(
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
}
@@ -2963,11 +2986,19 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
context.getCtx().getCurrency());
- context.setAllocatedAmount(
- processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
- context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
- loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
- LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+ if (isInterestRecalculationSupported(context.getCtx(), loan)) {
+ context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
+ context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+ context.getBalances(), paymentAllocationType, context.getInstallment(),
+ (ProgressiveTransactionCtx) context.getCtx(), loanTransactionToRepaymentScheduleMapping,
+ dueInstallmentCharges));
+ } else {
+ context.setAllocatedAmount(
+ processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
+ context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+ loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
+ context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+ }
context.setTransactionAmountUnprocessed(
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
}
@@ -2979,17 +3010,20 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
// element.
List currentInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule())) {
- currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
+ currentInstallments = context.getCtx().getInstallments().stream()
+ .filter(paymentAllocationContext.inAdvanceInstallmentsFilteringRules)
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())).toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT
.equals(context.getFutureInstallmentAllocationRule())) {
- currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
+ currentInstallments = context.getCtx().getInstallments().stream()
+ .filter(paymentAllocationContext.inAdvanceInstallmentsFilteringRules)
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
.toList();
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT
.equals(context.getFutureInstallmentAllocationRule())) {
- currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
+ currentInstallments = context.getCtx().getInstallments().stream()
+ .filter(paymentAllocationContext.inAdvanceInstallmentsFilteringRules)
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
.toList();
@@ -2998,14 +3032,16 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
// get current installment where from date < transaction date < to date OR
// transaction date
// is on first installment's first day ( from day )
- currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
+ currentInstallments = context.getCtx().getInstallments().stream()
+ .filter(paymentAllocationContext.inAdvanceInstallmentsFilteringRules)
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
.filter(f -> context.getLoanTransaction().isAfter(f.getFromDate())
|| context.getLoanTransaction().isOn(f.getFromDate()))
.toList();
// if there is no current in advance installment resolve similar to LAST_INSTALLMENT
if (currentInstallments.isEmpty()) {
- currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
+ currentInstallments = context.getCtx().getInstallments().stream()
+ .filter(paymentAllocationContext.inAdvanceInstallmentsFilteringRules)
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
.toList();
@@ -3034,18 +3070,37 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
context.getCtx().getCurrency());
- Money internalPaidPortion = processPaymentAllocation(context.getPaymentAllocationType(),
- context.getInstallment(), context.getLoanTransaction(), evenPortion,
- loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
- context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY);
- // Some extra logic to allocate as much as possible across the installments if
- // the
- // outstanding balances are different
- if (internalPaidPortion.isGreaterThanZero()) {
- context.setAllocatedAmount(internalPaidPortion);
+ if (isInterestRecalculationSupported(context.getCtx(), loan)) {
+ Money internalPaidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(
+ context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+ context.getBalances(), paymentAllocationType, context.getInstallment(),
+ (ProgressiveTransactionCtx) context.getCtx(), loanTransactionToRepaymentScheduleMapping,
+ inAdvanceInstallmentCharges);
+ // Some extra logic to allocate as much as possible across the installments
+ // if
+ // the
+ // outstanding balances are different
+ if (internalPaidPortion.isGreaterThanZero()) {
+ context.setAllocatedAmount(internalPaidPortion);
+ }
+ context.setTransactionAmountUnprocessed(
+ context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
+ } else {
+ Money internalPaidPortion = processPaymentAllocation(context.getPaymentAllocationType(),
+ context.getInstallment(), context.getLoanTransaction(), evenPortion,
+ loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
+ context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY);
+
+ // Some extra logic to allocate as much as possible across the installments
+ // if
+ // the
+ // outstanding balances are different
+ if (internalPaidPortion.isGreaterThanZero()) {
+ context.setAllocatedAmount(internalPaidPortion);
+ }
+ context.setTransactionAmountUnprocessed(
+ context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
}
- context.setTransactionAmountUnprocessed(
- context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
}
} else {
context.setInstallment(null);
@@ -3207,7 +3262,14 @@ private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
}
}
} else {
- handleReAgeWithCommonStrategy(loanTransaction, new CommonReAgeSettings(), ctx);
+ CommonReAgeSettings settings = switch (loanReAgeParameter.getInterestHandlingType()) {
+ case LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST -> new CommonReAgeSettings(false, true, true, true);
+ case LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST -> new CommonReAgeSettings(true, true, true, true);
+ case LoanReAgeInterestHandlingType.DEFAULT -> new CommonReAgeSettings();
+ case null -> new CommonReAgeSettings();
+ default -> throw new NotImplementedException();
+ };
+ handleReAgeWithCommonStrategy(loanTransaction, settings, ctx);
}
if (loanTransaction.getAmount().compareTo(ZERO) == 0) {
loanTransaction.reverse();
@@ -3569,6 +3631,7 @@ private static class VerticalPaymentAllocationContext implements LoopContext {
private Money transactionAmountUnprocessed;
private Money allocatedAmount;
private PaymentAllocationType paymentAllocationType;
+ private Predicate inAdvanceInstallmentsFilteringRules;
VerticalPaymentAllocationContext(TransactionCtx ctx, LoanTransaction loanTransaction,
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
index 487c7069ed1..0254a6a51a7 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
@@ -57,7 +57,7 @@ public static ILoanConfigurationDetails map(Loan loan) {
loanProductRelatedDetail.getNumberOfRepayments(), loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(),
loanProductRelatedDetail.getDaysInYearCustomStrategy(), loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation(),
loan.isInterestRecalculationEnabled(), getRestFrequencyType(loan), getPreCloseInterestCalculationStrategy(loan),
- loan.isAllowFullTermForTranche());
+ loan.isAllowFullTermForTranche(), loan.getLoanProductRelatedDetail().getLoanScheduleProcessingType());
}
private static RecalculationFrequencyType getRestFrequencyType(Loan loan) {
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
index a39748da338..77c9a48a5d1 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -54,6 +54,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
@@ -368,6 +369,18 @@ public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleMo
});
}
+ public void addOverdueBalanceCorrection(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate correctionDate,
+ final Money overdueAmount) {
+ scheduleModel
+ .changeOutstandingBalanceAndUpdateInterestPeriods(correctionDate, scheduleModel.zero(), overdueAmount, scheduleModel.zero())
+ .ifPresent(repaymentPeriod -> {
+ scheduleModel.recordOverdueCorrection(correctionDate, overdueAmount, repaymentPeriod.getDueDate());
+ calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
+ calculateOutstandingBalance(scheduleModel);
+ calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, correctionDate);
+ });
+ }
+
@Override
public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodFromDate,
LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money interestAmount) {
@@ -512,10 +525,15 @@ public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleMo
false)); //
}
}
-
+ Money duePrincipal = repaymentPeriod.getDuePrincipal();
+ Money dueInterest = repaymentPeriod.getDueInterest();
+ if (scheduleModel.loanProductRelatedDetail().getLoanScheduleProcessingType() == LoanScheduleProcessingType.VERTICAL
+ && notFullyRepaidRepaymentPeriodCount > 1) {
+ duePrincipal = repaymentPeriod.getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest();
+ }
return new PeriodDueDetails(repaymentPeriod.getEmi(), //
- repaymentPeriod.getDuePrincipal(), //
- repaymentPeriod.getDueInterest()); //
+ duePrincipal, //
+ dueInterest); //
}
@Override
@@ -700,6 +718,8 @@ public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterest
moveOutstandingAmountsFromPeriodsBeforeTransactionDate(scheduleModel.repaymentPeriods(), targetDate);
+ collapseIntermediateStubPeriods(scheduleModel);
+
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryScheduleModel(loanApplicationTerms, mc,
reAgePeriodStartDate, reAgePeriodStartDate);
@@ -884,7 +904,7 @@ private void moveOutstandingAmountsFromPeriodsBeforeTransactionDateForEqualInter
}
rp.setEmi(rp.getTotalPaidAmount());
rp.moveOutstandingDueToReAging();
- rp.setNoUnrecognisedInterest(true);
+ rp.setInterestMovedDownward(true);
});
}
@@ -919,9 +939,9 @@ private boolean adjustOverduePrincipal(final LocalDate currentDate, final Repaym
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
- addBalanceCorrection(model, fromDate, overduePrincipal);
+ addOverdueBalanceCorrection(model, fromDate, overduePrincipal);
} else {
- addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
+ addOverdueBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
}
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
@@ -931,7 +951,7 @@ private boolean adjustOverduePrincipal(final LocalDate currentDate, final Repaym
} else {
lastOverdueBalanceChange = currentDate;
}
- addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
+ addOverdueBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
}
return true;
@@ -1064,6 +1084,31 @@ private static void moveOutstandingAmountsFromPeriodsBeforeTransactionDate(final
});
}
+ private void collapseIntermediateStubPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) {
+ final List periods = scheduleModel.repaymentPeriods();
+ if (periods.size() <= 1) {
+ return;
+ }
+ // Only collapse if ALL periods are zero-EMI stubs (no principal due, no interest due, no paid amounts).
+ // This handles the repeated re-aging case where each re-age leaves behind a 1-day stub period,
+ // without affecting legitimate paid installments in multi-disbursement scenarios.
+ final boolean allPeriodsAreStubs = periods.stream()
+ .allMatch(rp -> rp.getEmi().isZero() && rp.getDuePrincipal().isZero() && rp.getDueInterest().isZero());
+ if (!allPeriodsAreStubs) {
+ return;
+ }
+ final RepaymentPeriod firstPeriod = periods.getFirst();
+ final RepaymentPeriod lastPeriod = periods.getLast();
+ final LocalDate lastDueDate = lastPeriod.getDueDate();
+
+ firstPeriod.setDueDate(lastDueDate);
+ firstPeriod.getInterestPeriods().getLast().setDueDate(lastDueDate);
+
+ periods.subList(1, periods.size()).clear();
+
+ calculateRateFactorForRepaymentPeriod(firstPeriod, scheduleModel);
+ }
+
private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) {
Money totalDuePaidDiff = scheduleModel.getTotalDuePrincipal().minus(scheduleModel.getTotalPaidPrincipal());
@@ -1086,7 +1131,7 @@ private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestSchedu
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
repaymentPeriod.setFutureUnrecognizedInterest(scheduleModel.zero());
scheduleModel.repaymentPeriods().forEach(rp -> {
- rp.setInterestMoved(false);
+ rp.setInterestMovedUpward(false);
});
MathContext mc = scheduleModel.mc();
@@ -1125,14 +1170,31 @@ private void calculateUnrecognizedInterestTillDateOnScheduleModelCopyAndDefer(Pr
RepaymentPeriod repaymentPeriod, LocalDate tillDate) {
MathContext mc = scheduleModel.mc();
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
+
+ // Reverse overdue corrections on the copy when they exist ON the target period
+ // but NOT beyond it. Beyond-target corrections provide legitimate cascading interest.
+ // Without reversal, overdue-inflated IPs create phantom futureUnrecognizedInterest.
+ final boolean hasOverdueCorrectionsBeyondTarget = scheduleModelCopy.hasOverdueCorrectionsBeyondDate(repaymentPeriod.getDueDate());
+ final boolean hasOverdueCorrectionsOnTarget = scheduleModelCopy.hasOverdueCorrectionsOnDate(repaymentPeriod.getDueDate());
+ final boolean shouldResetOverdue = !hasOverdueCorrectionsBeyondTarget && hasOverdueCorrectionsOnTarget;
+
+ if (shouldResetOverdue) {
+ scheduleModelCopy.reverseOverdueCorrections();
+ }
+
calculateRateFactorForScheduleTillDateInclusive(scheduleModelCopy, tillDate);
+
+ if (shouldResetOverdue) {
+ calculateOutstandingBalance(scheduleModelCopy);
+ }
+
Optional futureUnrecognizedInterestPeriod = getPeriodWithUnrecognizedInterest(repaymentPeriod, scheduleModelCopy);
futureUnrecognizedInterestPeriod.ifPresent(period -> {
repaymentPeriod.setFutureUnrecognizedInterest(period.getUnrecognizedInterest());
scheduleModel.repaymentPeriods().stream().filter(rp -> rp.getDueDate().isAfter(repaymentPeriod.getDueDate())) //
.forEach(rp -> {
- rp.setInterestMoved(true);
+ rp.setInterestMovedUpward(true);
});
});
}
@@ -1501,7 +1563,7 @@ private void calculateEMIOnActualModelWithFlatInterestMethod(List {
@@ -1687,6 +1750,14 @@ private BigDecimal calculateEMIValue(final BigDecimal rateFactorPlus1N, final Bi
return rateFactorPlus1N.multiply(outstandingBalanceForRest, mc).divide(fnResult, mc);
}
+ /**
+ * Calculate the EMI (Equal Monthly Installment) value for fixed interest portion
+ */
+ private BigDecimal calculateEMIValueForFixedInterest(final List repaymentPeriods, MathContext mc) {
+ return repaymentPeriods.stream().map(RepaymentPeriod::getFixedInterest).map(Money::getAmount).reduce(ZERO, BigDecimal::add)
+ .divide(BigDecimal.valueOf(repaymentPeriods.isEmpty() ? 1 : repaymentPeriods.size()), mc);
+ }
+
/**
* To calculate the daily payment, we first need to calculate something called the Rate Factor. We're going to be
* using simple interest. The Rate Factor for simple interest is calculated by the following formula:
@@ -1965,11 +2036,10 @@ public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interest
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
rp.setEmi(rp.getTotalPaidAmount());
rp.moveOutstandingDueToReAging();
- rp.setNoUnrecognisedInterest(true);
+ rp.setInterestMovedDownward(true);
});
- // stop calculate unrecognised interest at this point because all
- interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true);
+ collapseIntermediateStubPeriods(interestSchedule);
if (!originalMaturityDate.isBefore(transactionDate)) {
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OverdueBalanceCorrection.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OverdueBalanceCorrection.java
new file mode 100644
index 00000000000..c6ab29a54d8
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OverdueBalanceCorrection.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanproduct.calc.data;
+
+import java.time.LocalDate;
+import org.apache.fineract.organisation.monetary.domain.Money;
+
+/**
+ * Records an overdue balance correction applied to the schedule model. Used for detecting and reversing overdue
+ * corrections on model copies to prevent phantom futureUnrecognizedInterest.
+ *
+ * @param correctionDate
+ * the date where the correction was applied (matches an InterestPeriod's dueDate after IP split)
+ * @param amount
+ * the correction amount (+X for inflation, -X for deflation)
+ * @param affectedRpDueDate
+ * the dueDate of the RepaymentPeriod that received this correction (for beyond/onTarget detection)
+ */
+public record OverdueBalanceCorrection(LocalDate correctionDate, Money amount, LocalDate affectedRpDueDate) {
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
index c479d6d3a1b..1dabb865daf 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
@@ -68,6 +68,7 @@ public class ProgressiveLoanInterestScheduleModel {
@Setter
private LocalDate lastOverdueBalanceChange;
+ private List overdueCorrections = new ArrayList<>();
public ProgressiveLoanInterestScheduleModel(final List repaymentPeriods,
final ILoanConfigurationDetails loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
@@ -95,9 +96,35 @@ private ProgressiveLoanInterestScheduleModel(final List repayme
loanProductRelatedDetail.isInterestRecalculationEnabled()));
}
- public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
- return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail,
- installmentAmountInMultiplesOf, mc, false);
+ public void recordOverdueCorrection(final LocalDate correctionDate, final Money amount, final LocalDate affectedRpDueDate) {
+ overdueCorrections.add(new OverdueBalanceCorrection(correctionDate, amount, affectedRpDueDate));
+ }
+
+ public boolean hasOverdueCorrectionsBeyondDate(final LocalDate targetDueDate) {
+ return overdueCorrections.stream().anyMatch(oc -> oc.affectedRpDueDate().isAfter(targetDueDate));
+ }
+
+ public boolean hasOverdueCorrectionsOnDate(final LocalDate targetDueDate) {
+ return overdueCorrections.stream().anyMatch(oc -> oc.affectedRpDueDate().isEqual(targetDueDate));
+ }
+
+ /**
+ * Reverses all recorded overdue corrections on this model by subtracting each correction's amount from the
+ * corresponding InterestPeriod's balanceCorrectionAmount.
+ */
+ public void reverseOverdueCorrections() {
+ for (final OverdueBalanceCorrection oc : overdueCorrections) {
+ changeOutstandingBalanceAndUpdateInterestPeriods(oc.correctionDate(), zero(), oc.amount().negated(), zero());
+ }
+ overdueCorrections.clear();
+ this.lastOverdueBalanceChange = null;
+ }
+
+ public ProgressiveLoanInterestScheduleModel deepCopy(final MathContext mc) {
+ final ProgressiveLoanInterestScheduleModel copy = new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc, false);
+ copy.overdueCorrections = new ArrayList<>(this.overdueCorrections);
+ return copy;
}
public ProgressiveLoanInterestScheduleModel copyWithoutPaidAmounts() {
@@ -140,10 +167,18 @@ public Optional findRepaymentPeriodByFromAndDueDate(final Local
if (repaymentPeriodDueDate == null) {
return Optional.empty();
}
- return repaymentPeriods.stream()//
- .filter(repaymentPeriodItem -> DateUtils.isEqual(repaymentPeriodItem.getFromDate(), repaymentPeriodFromDate)
- && DateUtils.isEqual(repaymentPeriodItem.getDueDate(), repaymentPeriodDueDate))//
+ // Exact match first
+ Optional result = repaymentPeriods.stream()
+ .filter(rp -> DateUtils.isEqual(rp.getFromDate(), repaymentPeriodFromDate)
+ && DateUtils.isEqual(rp.getDueDate(), repaymentPeriodDueDate))
.findFirst();
+ if (result.isEmpty()) {
+ // Fallback: find a period that encompasses the requested date range
+ // This handles collapsed stub periods where multiple periods were merged into one
+ result = repaymentPeriods.stream().filter(rp -> !DateUtils.isAfter(rp.getFromDate(), repaymentPeriodFromDate)
+ && !DateUtils.isBefore(rp.getDueDate(), repaymentPeriodDueDate)).findFirst();
+ }
+ return result;
}
public List getRelatedRepaymentPeriods(final LocalDate calculateFromRepaymentPeriodDueDate) {
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
index 95011fb9558..eca58ee334b 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
@@ -77,7 +77,7 @@ public class RepaymentPeriod {
private Memo outstandingBalanceCalculation;
@Getter
@Setter
- private boolean isInterestMoved = false;
+ private boolean isInterestMovedUpward = false;
@Setter
private Money totalDisbursedAmount;
@@ -99,7 +99,7 @@ public class RepaymentPeriod {
private Money creditedInterestMovedDueReAge;
@Setter
@Getter
- private boolean noUnrecognisedInterest;
+ private boolean isInterestMovedDownward;
@Setter
@Getter
private boolean reAged;
@@ -111,7 +111,7 @@ public class RepaymentPeriod {
protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List interestPeriods,
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
- ILoanConfigurationDetails loanProductRelatedDetail, boolean noUnrecognisedInterest, boolean reAged,
+ ILoanConfigurationDetails loanProductRelatedDetail, boolean isInterestMovedDownward, boolean reAged,
boolean reAgedEarlyRepaymentHolder, Money fixedInterest) {
this.previous = previous;
this.fromDate = fromDate;
@@ -124,7 +124,7 @@ protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDat
this.futureUnrecognizedInterest = futureUnrecognizedInterest;
this.mc = mc;
this.loanProductRelatedDetail = loanProductRelatedDetail;
- this.noUnrecognisedInterest = noUnrecognisedInterest;
+ this.isInterestMovedDownward = isInterestMovedDownward;
this.reAged = reAged;
this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder;
this.fixedInterest = fixedInterest;
@@ -151,13 +151,13 @@ public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod rep
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(),
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
- repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
+ repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isInterestMovedDownward(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getFixedInterest());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
newRepaymentPeriod.setTotalCapitalizedIncomeAmount(repaymentPeriod.getTotalCapitalizedIncomeAmount());
- newRepaymentPeriod.setInterestMoved(repaymentPeriod.isInterestMoved());
+ newRepaymentPeriod.setInterestMovedUpward(repaymentPeriod.isInterestMovedUpward());
newRepaymentPeriod.setCurrency(repaymentPeriod.getCurrency());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -170,13 +170,16 @@ public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, R
final Money zero = Money.zero(repaymentPeriod.getCurrency(), mc);
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
- zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
+ zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isInterestMovedDownward(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getFixedInterest());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
+ if (repaymentPeriod.isInterestMovedDownward()) {
+ newRepaymentPeriod.setFixedInterest(repaymentPeriod.getPaidInterest());
+ }
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
newRepaymentPeriod.setTotalCapitalizedIncomeAmount(repaymentPeriod.getTotalCapitalizedIncomeAmount());
- newRepaymentPeriod.setInterestMoved(repaymentPeriod.isInterestMoved());
+ newRepaymentPeriod.setInterestMovedUpward(repaymentPeriod.isInterestMovedUpward());
newRepaymentPeriod.setCurrency(repaymentPeriod.getCurrency());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -217,8 +220,9 @@ private BigDecimal calculateRateFactorPlus1() {
@NotNull
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
- calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { previous, interestPeriods,
- futureUnrecognizedInterest, isInterestMoved, totalDisbursedAmount, fixedInterest, reAged });
+ calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest,
+ () -> new Object[] { previous, interestPeriods, futureUnrecognizedInterest, isInterestMovedUpward,
+ isInterestMovedDownward, totalDisbursedAmount, fixedInterest, reAged });
}
return calculatedDueInterestCalculation.get();
}
@@ -242,7 +246,7 @@ public Money calculateFixedInterestTillDate() {
public Money calculateCalculatedDueInterest() {
Money calculatedDueInterest = getZero();
- if (!isInterestMoved()) {
+ if (!isInterestMovedUpward() && !isInterestMovedDownward()) {
calculatedDueInterest = Money.of(getEmi().getCurrencyData(),
getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add),
mc);
@@ -367,7 +371,7 @@ public boolean isFullyPaid() {
* @return
*/
public Money getUnrecognizedInterest() {
- return noUnrecognisedInterest ? getZero() : getCalculatedDueInterest().minus(getDueInterest(), getMc());
+ return MathUtil.negativeToZero(getCalculatedDueInterest().minus(getDueInterest(), getMc()), getMc());
}
public Money getCreditedAmounts() {
diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle
index e5751e020a7..f79a834a841 100644
--- a/fineract-provider/build.gradle
+++ b/fineract-provider/build.gradle
@@ -27,6 +27,26 @@ apply plugin: 'io.swagger.core.v3.swagger-gradle-plugin'
apply plugin: 'com.google.cloud.tools.jib'
apply plugin: 'org.springframework.boot'
apply plugin: 'se.thinkcode.cucumber-runner'
+apply plugin: 'com.docktape.swagger-brake'
+
+swaggerBrake {
+ newApi = "${project.buildDir}/resources/main/static/fineract.json"
+ oldApi = findProperty('apiBaseline') ?: "${projectDir}/config/swagger/fineract-baseline.json"
+ outputFormats = ['JSON']
+ outputFilePath = "${project.buildDir}/swagger-brake"
+ deprecatedApiDeletionAllowed = true
+ strictValidation = false
+}
+
+checkBreakingChanges.dependsOn resolve
+checkBreakingChanges.onlyIf {
+ def baseline = findProperty('apiBaseline') ?: "${projectDir}/config/swagger/fineract-baseline.json"
+ def exists = file(baseline).exists()
+ if (!exists) {
+ logger.lifecycle("Skipping checkBreakingChanges: baseline file not found at ${baseline}")
+ }
+ exists
+}
check.dependsOn('cucumber')
@@ -96,10 +116,10 @@ configurations {
dependencies {
implementation project(':fineract-core')
- implementation 'org.springframework.boot:spring-boot-starter-test'
- implementation 'org.mockito:mockito-core'
- implementation 'org.mockito:mockito-junit-jupiter'
- implementation 'org.junit.jupiter:junit-jupiter-api'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.mockito:mockito-core'
+ testImplementation 'org.mockito:mockito-junit-jupiter'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.liquibase:liquibase-core'
}
@@ -165,7 +185,7 @@ tasks.register('createDB') {
description = "Creates the MariaDB Database. Needs database name to be passed (like: -PdbName=someDBname)"
doLast {
def sql = Sql.newInstance('jdbc:mariadb://localhost:3306/', mysqlUser, mysqlPassword, 'org.mariadb.jdbc.Driver')
- sql.execute('CREATE DATABASE ' + "`$dbName` CHARACTER SET utf8mb4")
+ sql.execute('CREATE DATABASE ' + "`$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
}
}
@@ -197,7 +217,7 @@ tasks.register('createMySQLDB') {
description = "Creates the MySQL Database. Needs database name to be passed (like: -PdbName=someDBname)"
doLast {
def sql = Sql.newInstance('jdbc:mysql://localhost:3306/', mysqlUser, mysqlPassword, 'com.mysql.cj.jdbc.Driver')
- sql.execute('CREATE DATABASE ' + "`$dbName` CHARACTER SET utf8mb4")
+ sql.execute('CREATE DATABASE ' + "`$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
}
}
diff --git a/fineract-provider/dependencies.gradle b/fineract-provider/dependencies.gradle
index 35b5daee841..dff3110e8d6 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -42,6 +42,7 @@ dependencies {
implementation(project(path: ':fineract-loan-origination'))
implementation(project(path: ':fineract-security'))
implementation(project(path: ':fineract-working-capital-loan'))
+ implementation(project(path: ':fineract-mix'))
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
@@ -58,7 +59,7 @@ dependencies {
'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-validation',
'org.springframework.boot:spring-boot-starter-security',
- "org.springframework.boot:spring-boot-starter-oauth2-authorization-server",
+ 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server',
'org.springframework.boot:spring-boot-starter-cache',
'org.springframework.boot:spring-boot-starter-oauth2-resource-server',
'org.springframework.boot:spring-boot-starter-actuator',
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java
index ca547efb915..5a94ee78fa8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResource.java
@@ -99,7 +99,7 @@ public class JournalEntriesApiResource {
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
- @Operation(summary = "List Journal Entries", description = "The list capability of journal entries can support pagination and sorting.\n\n"
+ @Operation(summary = "List Journal Entries", operationId = "retrieveAllJournalEntries", description = "The list capability of journal entries can support pagination and sorting.\n\n"
+ "Example Requests:\n" + "\n" + "journalentries\n" + "\n" + "journalentries?transactionId=PB37X8Y21EQUY4S\n" + "\n"
+ "journalentries?officeId=1&manualEntriesOnly=true&fromDate=1 July 2013&toDate=15 July 2013&dateFormat=dd MMMM yyyy&locale=en\n"
+ "\n" + "journalentries?fields=officeName,glAccountName,transactionDate\n" + "\n" + "journalentries?offset=10&limit=50\n"
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
index 66e91b0336d..1833fad3920 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
@@ -212,12 +212,12 @@ public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") f
TransactionTypeEnumData transactionTypeEnumData = null;
- if (PortfolioAccountType.fromInt(entityTypeId).isLoanAccount()) {
+ if (PortfolioAccountType.LOAN.equals(PortfolioAccountType.fromInt(entityTypeId))) {
final LoanTransactionEnumData loanTransactionType = LoanEnumerations
.transactionType(JdbcSupport.getInteger(rs, "loanTransactionType"));
transactionTypeEnumData = new TransactionTypeEnumData(loanTransactionType.getId(), loanTransactionType.getCode(),
loanTransactionType.getValue());
- } else if (PortfolioAccountType.fromInt(entityTypeId).isSavingsAccount()) {
+ } else if (PortfolioAccountType.SAVINGS.equals(PortfolioAccountType.fromInt(entityTypeId))) {
final SavingsAccountTransactionEnumData savingsTransactionType = SavingsEnumerations
.transactionType(JdbcSupport.getInteger(rs, "savingsTransactionType"));
transactionTypeEnumData = new TransactionTypeEnumData(savingsTransactionType.getId(), savingsTransactionType.getCode(),
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateSavingsAccountChargeCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateSavingsAccountChargeCommandStrategy.java
new file mode 100644
index 00000000000..fc8b49170d0
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateSavingsAccountChargeCommandStrategy.java
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.batch.command.internal;
+
+import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
+
+import com.google.common.base.Splitter;
+import jakarta.ws.rs.core.UriInfo;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.batch.command.CommandStrategy;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.fineract.portfolio.savings.api.SavingsAccountChargesApiResource;
+import org.apache.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+/**
+ * Implements {@link CommandStrategy} and Create Charge for a Savings Account. It passes the contents of the body from
+ * the BatchRequest to {@link SavingsAccountChargesApiResource} and gets back the response. This class will also catch
+ * any errors raised by {@link SavingsAccountChargesApiResource} and map those errors to appropriate status codes in
+ * BatchResponse.
+ *
+ * @see CommandStrategy
+ * @see BatchRequest
+ * @see BatchResponse
+ */
+@Component
+@RequiredArgsConstructor
+public class CreateSavingsAccountChargeCommandStrategy implements CommandStrategy {
+
+ private final SavingsAccountChargesApiResource savingsAccountChargesApiResource;
+
+ @Override
+ public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) {
+
+ final BatchResponse response = new BatchResponse();
+ final String responseBody;
+
+ response.setRequestId(request.getRequestId());
+ response.setHeaders(request.getHeaders());
+
+ final List pathParameters = Splitter.on('/').splitToList(relativeUrlWithoutVersion(request));
+ final Long savingsAccountId = Long.parseLong(pathParameters.get(1));
+
+ // Create a new charge for a savings account
+ responseBody = savingsAccountChargesApiResource.addSavingsAccountCharge(savingsAccountId, request.getBody());
+
+ response.setStatusCode(HttpStatus.SC_OK);
+ // Set the body of the response after Charge has been successfully created
+ response.setBody(responseBody);
+
+ return response;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/COBCatchUpExecutorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/COBCatchUpExecutorHelper.java
new file mode 100644
index 00000000000..0605c112bec
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/COBCatchUpExecutorHelper.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.api;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+import org.apache.fineract.cob.service.COBCatchUpService;
+
+public final class COBCatchUpExecutorHelper {
+
+ private COBCatchUpExecutorHelper() {}
+
+ public static Response executeLoanCOBCatchUp(COBCatchUpService loanCOBCatchUpService) {
+ if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
+ return Response.status(Response.Status.BAD_REQUEST).build();
+ }
+ loanCOBCatchUpService.unlockHardLockedLoans();
+ OldestCOBProcessedLoanDTO oldestCOBProcessedLoan = loanCOBCatchUpService.getOldestCOBProcessedLoan();
+
+ if (oldestCOBProcessedLoan.getCobProcessedDate().equals(oldestCOBProcessedLoan.getCobBusinessDate())) {
+ return Response.status(Response.Status.OK).build();
+ }
+ loanCOBCatchUpService.executeLoanCOBCatchUp();
+ return Response.status(Response.Status.ACCEPTED).build();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
index 8c690ba68ce..4fc055b67be 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
@@ -38,7 +38,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.data.COBPartition;
import org.apache.fineract.cob.loan.LoanCOBConstant;
-import org.apache.fineract.cob.loan.RetrieveLoanIdService;
+import org.apache.fineract.cob.service.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
@@ -63,7 +63,7 @@ public class InternalCOBApiResource implements InitializingBean {
private static final String DATETIME_PATTERN = "dd MMMM yyyy";
- private final RetrieveLoanIdService retrieveLoanIdService;
+ private final RetrieveLoanIdService retrieveIdService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final ToApiJsonSerializer toApiJsonSerializerForList;
private final LoanRepositoryWrapper loanRepositoryWrapper;
@@ -90,7 +90,7 @@ public void afterPropertiesSet() throws Exception {
public String getCobPartitions(@Context final UriInfo uriInfo, @PathParam("partitionSize") int partitionSize) {
LocalDate businessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.BUSINESS_DATE);
log.info("RetrieveLoanCOBPartitions is called with partitionSize {} for {}", partitionSize, businessDate);
- List loanCOBPartitions = retrieveLoanIdService.retrieveLoanCOBPartitions(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND,
+ List loanCOBPartitions = retrieveIdService.retrieveLoanCOBPartitions(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND,
businessDate, false, partitionSize);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializerForList.serialize(settings, loanCOBPartitions);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
index f2986612595..e2b4eb01b02 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
@@ -32,7 +32,8 @@
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
-import org.apache.fineract.cob.service.LoanCOBCatchUpService;
+import org.apache.fineract.cob.service.COBCatchUpService;
+import org.apache.fineract.cob.service.LoanCOBCatchUpServiceImpl;
import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.stereotype.Component;
@@ -43,7 +44,7 @@
@RequiredArgsConstructor
public class LoanCOBCatchUpApiResource {
- private final Optional loanCOBCatchUpServiceOp;
+ private final Optional loanCOBCatchUpServiceOp;
@GET
@Path("oldest-cob-closed")
@@ -51,7 +52,7 @@ public class LoanCOBCatchUpApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieves the oldest COB processed loan", description = "Retrieves the COB business date and the oldest COB processed loan")
public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
- return loanCOBCatchUpServiceOp.map(LoanCOBCatchUpService::getOldestCOBProcessedLoan)
+ return loanCOBCatchUpServiceOp.map(COBCatchUpService::getOldestCOBProcessedLoan)
.orElseThrow(() -> new JobIsNotFoundOrNotEnabledException(JobName.LOAN_COB.name()));
}
@@ -64,19 +65,8 @@ public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
@ApiResponse(responseCode = "202", description = "Catch Up has been started")
@ApiResponse(responseCode = "400", description = "Catch Up is already running")
public Response executeLoanCOBCatchUp() {
- return loanCOBCatchUpServiceOp.map(loanCOBCatchUpService -> {
- if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
- return Response.status(Response.Status.BAD_REQUEST).build();
- }
- loanCOBCatchUpService.unlockHardLockedLoans();
- OldestCOBProcessedLoanDTO oldestCOBProcessedLoan = loanCOBCatchUpService.getOldestCOBProcessedLoan();
-
- if (oldestCOBProcessedLoan.getCobProcessedDate().equals(oldestCOBProcessedLoan.getCobBusinessDate())) {
- return Response.status(Response.Status.OK).build();
- }
- loanCOBCatchUpService.executeLoanCOBCatchUp();
- return Response.status(Response.Status.ACCEPTED).build();
- }).orElseThrow(() -> new JobIsNotFoundOrNotEnabledException(JobName.LOAN_COB.name()));
+ return loanCOBCatchUpServiceOp.map(COBCatchUpExecutorHelper::executeLoanCOBCatchUp)
+ .orElseThrow(() -> new JobIsNotFoundOrNotEnabledException(JobName.LOAN_COB.name()));
}
@GET
@@ -85,6 +75,6 @@ public Response executeLoanCOBCatchUp() {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieves whether Loan COB catch up is running", description = "Retrieves whether Loan COB catch up is running, and the current execution date if it is running.")
public IsCatchUpRunningDTO isCatchUpRunning() {
- return loanCOBCatchUpServiceOp.map(LoanCOBCatchUpService::isCatchUpRunning).orElseGet(() -> new IsCatchUpRunningDTO(false, null));
+ return loanCOBCatchUpServiceOp.map(COBCatchUpService::isCatchUpRunning).orElseGet(() -> new IsCatchUpRunningDTO(false, null));
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/WorkingCapitalLoanCOBCatchUpApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/WorkingCapitalLoanCOBCatchUpApiResource.java
new file mode 100644
index 00000000000..ef490baa920
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/WorkingCapitalLoanCOBCatchUpApiResource.java
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
+import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+import org.apache.fineract.cob.service.COBCatchUpService;
+import org.apache.fineract.cob.service.WorkingCapitalLoanCOBCatchUpServiceImpl;
+import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException;
+import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.springframework.stereotype.Component;
+
+@Path("/v1/working-capital-loans")
+@Component
+@Tag(name = "Working Capital Loan COB Catch Up", description = "")
+@RequiredArgsConstructor
+public class WorkingCapitalLoanCOBCatchUpApiResource {
+
+ private final Optional loanCOBCatchUpServiceOp;
+
+ @GET
+ @Path("oldest-cob-closed")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Retrieves the oldest COB processed Working Capital Loan", description = "Retrieves the COB business date and the oldest COB processed loan")
+ public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
+ return loanCOBCatchUpServiceOp.map(COBCatchUpService::getOldestCOBProcessedLoan)
+ .orElseThrow(() -> new JobIsNotFoundOrNotEnabledException(JobName.LOAN_COB.name()));
+ }
+
+ @POST
+ @Path("catch-up")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Executes Working Capital Loan COB Catch Up", description = "Executes the Working Capital Loan COB job on every day from the oldest Loan to the current COB business date")
+ @ApiResponse(responseCode = "200", description = "All loans are up to date")
+ @ApiResponse(responseCode = "202", description = "Catch Up has been started")
+ @ApiResponse(responseCode = "400", description = "Catch Up is already running")
+ public Response executeLoanCOBCatchUp() {
+ return loanCOBCatchUpServiceOp.map(COBCatchUpExecutorHelper::executeLoanCOBCatchUp)
+ .orElseThrow(() -> new JobIsNotFoundOrNotEnabledException(JobName.LOAN_COB.name()));
+ }
+
+ @GET
+ @Path("is-catch-up-running")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Operation(summary = "Retrieves whether Working Capital Loan COB catch up is running", description = "Retrieves whether Working Capital Loan COB catch up is running, and the current execution date if it is running.")
+ public IsCatchUpRunningDTO isCatchUpRunning() {
+ return loanCOBCatchUpServiceOp.map(COBCatchUpService::isCatchUpRunning).orElseGet(() -> new IsCatchUpRunningDTO(false, null));
+ }
+}
diff --git a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java
similarity index 98%
rename from fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java
rename to fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java
index 98f2a0ae8eb..9453e59e86c 100644
--- a/fineract-cob/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/CustomLoanAccountLockRepositoryImpl.java
@@ -26,7 +26,7 @@
@Repository
@RequiredArgsConstructor
-public class CustomLoanAccountLockRepositoryImpl implements CustomLoanAccountLockRepository {
+public class CustomLoanAccountLockRepositoryImpl implements CustomLoanAccountLockRepository {
@PersistenceContext
private EntityManager entityManager;
@@ -52,4 +52,5 @@ and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING')
entityManager.createNativeQuery(sql).executeUpdate();
entityManager.flush();
}
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
index 45f77d9b964..41faf7ef1ea 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
@@ -18,63 +18,20 @@
*/
package org.apache.fineract.cob.domain;
-import jakarta.persistence.Column;
import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.Id;
import jakarta.persistence.Table;
-import jakarta.persistence.Version;
import java.time.LocalDate;
-import java.time.OffsetDateTime;
-import lombok.Getter;
import lombok.NoArgsConstructor;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity
@Table(name = "m_loan_account_locks")
@NoArgsConstructor
-@Getter
-public class LoanAccountLock {
+public class LoanAccountLock extends AccountLock {
- @Id
- @Column(name = "loan_id", nullable = false)
- private Long loanId;
-
- @Version
- @Column(name = "version")
- private Long version;
-
- @Enumerated(EnumType.STRING)
- @Column(name = "lock_owner", nullable = false)
- private LockOwner lockOwner;
-
- @Column(name = "lock_placed_on", nullable = false)
- private OffsetDateTime lockPlacedOn;
-
- @Column(name = "error")
- private String error;
-
- @Column(name = "stacktrace")
- private String stacktrace;
-
- @Column(name = "lock_placed_on_cob_business_date")
- private LocalDate lockPlacedOnCobBusinessDate;
+ private static final long serialVersionUID = 5267165818666471447L;
public LoanAccountLock(Long loanId, LockOwner lockOwner, LocalDate lockPlacedOnCobBusinessDate) {
- this.loanId = loanId;
- this.lockOwner = lockOwner;
- this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
- this.lockPlacedOnCobBusinessDate = lockPlacedOnCobBusinessDate;
- }
-
- public void setError(String errorMessage, String stacktrace) {
- this.error = errorMessage;
- this.stacktrace = stacktrace;
+ super(loanId, lockOwner, lockPlacedOnCobBusinessDate);
}
- public void setNewLockOwner(LockOwner newLockOwner) {
- this.lockOwner = newLockOwner;
- this.lockPlacedOn = DateUtils.getAuditOffsetDateTime();
- }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
index 08e908fad6b..3724185c46f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
@@ -22,28 +22,31 @@
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+@Repository
public interface LoanAccountLockRepository
- extends CustomLoanAccountLockRepository, JpaRepository, JpaSpecificationExecutor {
+ extends AccountLockRepository, JpaRepository, JpaSpecificationExecutor {
+ @Override
Optional findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+ @Override
void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
+ @Override
List findAllByLoanIdIn(List loanIds);
+ @Override
boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+ @Override
boolean existsByLoanIdAndLockOwnerAndErrorIsNotNull(Long loanId, LockOwner lockOwner);
- @Query("""
- delete from LoanAccountLock lck where lck.lockPlacedOnCobBusinessDate is not null and lck.error is not null and
- lck.lockOwner in (org.apache.fineract.cob.domain.LockOwner.LOAN_COB_CHUNK_PROCESSING,org.apache.fineract.cob.domain.LockOwner.LOAN_INLINE_COB_PROCESSING)
- """)
- @Modifying(flushAutomatically = true)
- void removeLockByOwner();
-
+ @Override
List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner);
+
+ @Override
+ void removeByLockOwnerInAndErrorIsNotNullAndLockPlacedOnCobBusinessDateIsNotNull(List lockOwners);
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java
index 818dd8fd6bb..a5cbe3f2ca4 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/ChunkProcessingLoanItemListener.java
@@ -18,18 +18,23 @@
*/
package org.apache.fineract.cob.listener;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.loan.LoanLockingService;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.transaction.support.TransactionTemplate;
-public class ChunkProcessingLoanItemListener extends AbstractLoanItemListener {
+@Slf4j
+public class ChunkProcessingLoanItemListener extends AbstractLoanItemListener {
- public ChunkProcessingLoanItemListener(LoanLockingService loanLockingService, TransactionTemplate transactionTemplate) {
- super(loanLockingService, transactionTemplate);
+ public ChunkProcessingLoanItemListener(LockingService lockingService, TransactionTemplate transactionTemplate) {
+ super(lockingService, transactionTemplate);
}
@Override
protected LockOwner getLockOwner() {
return LockOwner.LOAN_COB_CHUNK_PROCESSING;
}
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java
index 548c21328ff..7ad279804f1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/InlineCOBLoanItemListener.java
@@ -18,14 +18,16 @@
*/
package org.apache.fineract.cob.listener;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.loan.LoanLockingService;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.transaction.support.TransactionTemplate;
-public class InlineCOBLoanItemListener extends AbstractLoanItemListener {
+public class InlineCOBLoanItemListener extends AbstractLoanItemListener {
- public InlineCOBLoanItemListener(LoanLockingService loanLockingService, TransactionTemplate transactionTemplate) {
- super(loanLockingService, transactionTemplate);
+ public InlineCOBLoanItemListener(LockingService lockingService, TransactionTemplate transactionTemplate) {
+ super(lockingService, transactionTemplate);
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java
new file mode 100644
index 00000000000..471a60253fb
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/WorkingCapitalChunkProcessingLoanItemListener.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.listener;
+
+import org.apache.fineract.cob.conditions.BatchWorkerCondition;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock;
+import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Component
+@Conditional(BatchWorkerCondition.class)
+public class WorkingCapitalChunkProcessingLoanItemListener
+ extends AbstractLoanItemListener {
+
+ public WorkingCapitalChunkProcessingLoanItemListener(
+ LockingService workingCapitalLoanAccountLockLockingService,
+ TransactionTemplate transactionTemplate) {
+ super(workingCapitalLoanAccountLockLockingService, transactionTemplate);
+ }
+
+ @Override
+ protected LockOwner getLockOwner() {
+ return LockOwner.LOAN_COB_CHUNK_PROCESSING;
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java
index 42f140b3a6e..8a5ae95ec44 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java
@@ -18,55 +18,29 @@
*/
package org.apache.fineract.cob.loan;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.stream.Collectors;
-import lombok.AccessLevel;
-import lombok.RequiredArgsConstructor;
-import lombok.Setter;
-import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService;
-import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
+import org.apache.fineract.cob.processor.AbstractItemProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanModelProcessingService;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
-import org.springframework.batch.core.ExitStatus;
-import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.annotation.AfterStep;
-import org.springframework.batch.item.ExecutionContext;
-import org.springframework.batch.item.ItemProcessor;
import org.springframework.lang.NonNull;
-@RequiredArgsConstructor
-@Slf4j
-public abstract class AbstractLoanItemProcessor implements ItemProcessor {
+public abstract class AbstractLoanItemProcessor extends AbstractItemProcessor {
- private final COBBusinessStepService cobBusinessStepService;
private final ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
- @Setter(AccessLevel.PROTECTED)
- private ExecutionContext executionContext;
- private LocalDate businessDate;
+ public AbstractLoanItemProcessor(COBBusinessStepService cobBusinessStepService,
+ ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService) {
+ super(cobBusinessStepService);
+ this.progressiveLoanModelProcessingService = progressiveLoanModelProcessingService;
+ }
- @SuppressWarnings({ "unchecked" })
@Override
public Loan process(@NonNull Loan loan) throws Exception {
if (needToRebuildModel(loan)) {
progressiveLoanModelProcessingService.recalculateModelAndSave(loan.getId());
}
- Set businessSteps = (Set) executionContext.get(LoanCOBConstant.BUSINESS_STEPS);
- if (businessSteps == null) {
- throw new IllegalStateException("No business steps found in the execution context");
- }
- TreeMap businessStepMap = getBusinessStepMap(businessSteps);
-
- Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, loan);
- alreadyProcessedLoan.setLastClosedBusinessDate(businessDate);
- return alreadyProcessedLoan;
+ return super.process(loan);
}
private boolean needToRebuildModel(Loan loan) {
@@ -74,22 +48,9 @@ private boolean needToRebuildModel(Loan loan) {
ProgressiveLoanInterestScheduleModel.getModelVersion());
}
- private TreeMap getBusinessStepMap(Set businessSteps) {
- Map businessStepMap = businessSteps.stream()
- .collect(Collectors.toMap(BusinessStepNameAndOrder::getStepOrder, BusinessStepNameAndOrder::getStepName));
- return new TreeMap<>(businessStepMap);
- }
-
- @AfterStep
- public ExitStatus afterStep(@NonNull StepExecution stepExecution) {
- return ExitStatus.COMPLETED;
- }
-
- protected void setBusinessDate(StepExecution stepExecution) {
- this.businessDate = LocalDate.parse(
- Objects.requireNonNull(
- (String) stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)),
- DateTimeFormatter.ISO_DATE);
+ @Override
+ public void setLastRun(Loan processedLoan) {
+ processedLoan.setLastClosedBusinessDate(getBusinessDate());
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java
index 759c2add2d7..c9ad1c5b44f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemReader.java
@@ -23,33 +23,33 @@
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.cob.exceptions.LoanReadException;
-import org.apache.fineract.portfolio.loanaccount.domain.Loan;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.apache.fineract.cob.exceptions.LockedReadException;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.item.ItemReader;
+import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
@Slf4j
@RequiredArgsConstructor
-public abstract class AbstractLoanItemReader implements ItemReader {
+public abstract class AbstractLoanItemReader> implements ItemReader {
- protected final LoanRepository loanRepository;
+ protected final CrudRepository loanRepository;
@Setter(AccessLevel.PROTECTED)
private LinkedBlockingQueue remainingData;
@Override
- public Loan read() throws Exception {
+ public T read() throws Exception {
final Long loanId = remainingData.poll();
if (loanId != null) {
try {
return loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
} catch (Exception e) {
- throw new LoanReadException(loanId, e);
+ throw new LockedReadException(loanId, e);
}
}
return null;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java
index f7e2b60bf37..9518ffef960 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemWriter.java
@@ -21,7 +21,9 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.batch.item.Chunk;
@@ -32,7 +34,7 @@
@RequiredArgsConstructor
public abstract class AbstractLoanItemWriter extends RepositoryItemWriter {
- private final LoanLockingService loanLockingService;
+ private final LockingService loanLockingService;
@Override
public void write(@NonNull Chunk extends Loan> items) throws Exception {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
index b8eda2d144f..01daf033532 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
@@ -18,95 +18,31 @@
*/
package org.apache.fineract.cob.loan;
-import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
-
-import com.google.common.collect.Lists;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.cob.converter.COBParameterConverter;
-import org.apache.fineract.cob.data.COBParameter;
+import org.apache.fineract.cob.COBConstant;
import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.exceptions.LoanLockCannotBeAppliedException;
-import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.service.RetrieveIdService;
+import org.apache.fineract.cob.tasklet.ApplyCommonLockTasklet;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
-import org.springframework.batch.core.StepContribution;
-import org.springframework.batch.core.scope.context.ChunkContext;
-import org.springframework.batch.core.step.tasklet.Tasklet;
-import org.springframework.batch.item.ExecutionContext;
-import org.springframework.batch.repeat.RepeatStatus;
-import org.springframework.lang.NonNull;
-import org.springframework.transaction.TransactionStatus;
-import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Slf4j
-@RequiredArgsConstructor
-public class ApplyLoanLockTasklet implements Tasklet {
-
- private static final long NUMBER_OF_RETRIES = 3;
- private final FineractProperties fineractProperties;
- private final LoanLockingService loanLockingService;
- private final RetrieveLoanIdService retrieveLoanIdService;
- private final TransactionTemplate transactionTemplate;
-
- @Override
- @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
- public RepeatStatus execute(@NonNull StepContribution contribution, @NonNull ChunkContext chunkContext)
- throws LoanLockCannotBeAppliedException {
- ExecutionContext executionContext = contribution.getStepExecution().getExecutionContext();
- long numberOfExecutions = contribution.getStepExecution().getCommitCount();
- COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER));
- boolean isCatchUp = CatchUpFlagResolver.resolve(contribution.getStepExecution());
- List loanIds;
- if (Objects.isNull(loanCOBParameter)
- || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId()))
- || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) {
- loanIds = Collections.emptyList();
- } else {
- loanIds = new ArrayList<>(
- retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp));
- }
- List> loanIdPartitions = Lists.partition(loanIds, getInClauseParameterSizeLimit());
- List accountLocks = new ArrayList<>();
- loanIdPartitions.forEach(loanIdPartition -> accountLocks.addAll(loanLockingService.findAllByLoanIdIn(loanIdPartition)));
-
- List toBeProcessedLoanIds = new ArrayList<>(loanIds);
- List alreadyLockedAccountIds = accountLocks.stream().map(LoanAccountLock::getLoanId).toList();
+public class ApplyLoanLockTasklet extends ApplyCommonLockTasklet {
- toBeProcessedLoanIds.removeAll(alreadyLockedAccountIds);
- try {
- applyLocks(toBeProcessedLoanIds);
- } catch (Exception e) {
- if (numberOfExecutions > NUMBER_OF_RETRIES) {
- String message = "There was an error applying lock to loan accounts.";
- log.error("{}", message, e);
- throw new LoanLockCannotBeAppliedException(message, e);
- } else {
- return RepeatStatus.CONTINUABLE;
- }
- }
-
- return RepeatStatus.FINISHED;
+ public ApplyLoanLockTasklet(FineractProperties fineractProperties, LockingService loanLockingService,
+ RetrieveIdService retrieveIdService, TransactionTemplate transactionTemplate) {
+ super(fineractProperties, loanLockingService, retrieveIdService, transactionTemplate);
}
- private void applyLocks(List toBeProcessedLoanIds) {
- transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
- transactionTemplate.execute(new TransactionCallbackWithoutResult() {
-
- @Override
- protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
- loanLockingService.applyLock(toBeProcessedLoanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING);
- }
- });
+ @Override
+ public String getCOBParameter() {
+ return COBConstant.COB_PARAMETER;
}
- private int getInClauseParameterSizeLimit() {
- return fineractProperties.getQuery().getInClauseParameterSizeLimit();
+ @Override
+ public LockOwner getLockOwner() {
+ return LockOwner.LOAN_COB_CHUNK_PROCESSING;
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java
index c25a9c33f97..fc33af0c439 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemReader.java
@@ -20,13 +20,14 @@
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.lang.NonNull;
-public class InlineCOBLoanItemReader extends AbstractLoanItemReader {
+public class InlineCOBLoanItemReader extends AbstractLoanItemReader {
public InlineCOBLoanItemReader(LoanRepository loanRepository) {
super(loanRepository);
@@ -36,7 +37,7 @@ public InlineCOBLoanItemReader(LoanRepository loanRepository) {
@SuppressWarnings({ "unchecked" })
public void beforeStep(@NonNull StepExecution stepExecution) {
ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext();
- List loanIds = (List) executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER);
+ List loanIds = (List) executionContext.get(LoanCOBConstant.COB_PARAMETER);
setRemainingData(new LinkedBlockingQueue<>(loanIds));
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java
index 9a839d32688..07b8acb84c6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemWriter.java
@@ -18,11 +18,13 @@
*/
package org.apache.fineract.cob.loan;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
public class InlineCOBLoanItemWriter extends AbstractLoanItemWriter {
- public InlineCOBLoanItemWriter(LoanLockingService loanLockingService) {
+ public InlineCOBLoanItemWriter(LockingService loanLockingService) {
super(loanLockingService);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
index 400a3589308..3e7f7f2193f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
@@ -29,11 +29,14 @@
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.COBBusinessStep;
import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.cob.COBConstant;
import org.apache.fineract.cob.common.CustomJobParameterResolver;
import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
import org.apache.fineract.cob.exceptions.CustomJobParameterNotFoundException;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
@@ -49,25 +52,30 @@
@Slf4j
@RequiredArgsConstructor
-public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet {
+public class InlineLoanCOBBuildExecutionContextTasklet, B extends COBBusinessStep>
+ implements Tasklet {
private final COBBusinessStepService cobBusinessStepService;
private final CustomJobParameterRepository customJobParameterRepository;
private final CustomJobParameterResolver customJobParameterResolver;
+ private final Class businessStepClass;
+ private final String cobJobName;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
+ public Set resolveBusinessSteps() {
+ return cobBusinessStepService.getCOBBusinessSteps(businessStepClass, cobJobName);
+ }
+
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
HashMap businessDates = ThreadLocalContextUtil.getBusinessDates();
ThreadLocalContextUtil.setActionContext(ActionContext.COB);
- Set cobBusinessSteps = cobBusinessStepService.getCOBBusinessSteps(LoanCOBBusinessStep.class,
- LoanCOBConstant.LOAN_COB_JOB_NAME);
- contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.LOAN_COB_PARAMETER,
- getLoanIdsFromJobParameters(chunkContext));
- contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_STEPS, cobBusinessSteps);
+ Set cobBusinessSteps = resolveBusinessSteps();
+ contribution.getStepExecution().getExecutionContext().put(COBConstant.COB_PARAMETER, getLoanIdsFromJobParameters(chunkContext));
+ contribution.getStepExecution().getExecutionContext().put(COBConstant.BUSINESS_STEPS, cobBusinessSteps);
String businessDateString = getBusinessDateFromJobParameters(chunkContext);
- contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDateString);
+ contribution.getStepExecution().getExecutionContext().put(COBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDateString);
LocalDate businessDate = LocalDate.parse(businessDateString, DateTimeFormatter.ISO_DATE);
businessDates.put(BusinessDateType.COB_DATE, businessDate);
businessDates.put(BusinessDateType.BUSINESS_DATE, businessDate.plusDays(1));
@@ -76,15 +84,14 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon
}
private String getBusinessDateFromJobParameters(ChunkContext chunkContext) {
- Long customJobParameterId = (Long) chunkContext.getStepContext().getJobParameters()
- .get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME);
+ Long customJobParameterId = (Long) chunkContext.getStepContext().getJobParameters().get(COBConstant.BUSINESS_DATE_PARAMETER_NAME);
CustomJobParameter customJobParameter = customJobParameterRepository.findById(customJobParameterId)
.orElseThrow(() -> new LoanNotFoundException(customJobParameterId));
String parameterJson = customJobParameter.getParameterJson();
Set jobParameters = gson.fromJson(parameterJson, new TypeToken>() {}.getType());
JobParameterDTO businessDateParameter = jobParameters.stream()
- .filter(jobParameterDTO -> jobParameterDTO.getParameterName().equals(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME))
- .findFirst().orElseThrow(() -> new CustomJobParameterNotFoundException(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME));
+ .filter(jobParameterDTO -> jobParameterDTO.getParameterName().equals(COBConstant.BUSINESS_DATE_PARAMETER_NAME)).findFirst()
+ .orElseThrow(() -> new CustomJobParameterNotFoundException(COBConstant.BUSINESS_DATE_PARAMETER_NAME));
return businessDateParameter.getParameterValue();
}
@@ -93,8 +100,8 @@ private List getLoanIdsFromJobParameters(ChunkContext chunkContext) {
.getCustomJobParameterSet(chunkContext.getStepContext().getStepExecution())
.orElseThrow(() -> new LoanNotFoundException(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY));
JobParameterDTO loanIdsParameter = jobParameters.stream()
- .filter(jobParameterDTO -> jobParameterDTO.getParameterName().equals(LoanCOBConstant.LOAN_IDS_PARAMETER_NAME)).findFirst()
- .orElseThrow(() -> new CustomJobParameterNotFoundException(LoanCOBConstant.LOAN_IDS_PARAMETER_NAME));
+ .filter(jobParameterDTO -> jobParameterDTO.getParameterName().equals(COBConstant.INLINE_IDS_PARAMETER_NAME)).findFirst()
+ .orElseThrow(() -> new CustomJobParameterNotFoundException(COBConstant.INLINE_IDS_PARAMETER_NAME));
return gson.fromJson(loanIdsParameter.getParameterValue(), new TypeToken>() {}.getType());
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java
index ad21d3fcf9d..1d4868bb54c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java
@@ -25,14 +25,11 @@ public final class LoanCOBConstant extends COBConstant {
public static final String JOB_NAME = "LOAN_COB";
public static final String JOB_HUMAN_READABLE_NAME = "Loan COB";
public static final String LOAN_COB_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS";
- public static final String LOAN_COB_PARAMETER = "loanCobParameter";
public static final String LOAN_COB_WORKER_STEP = "loanCOBWorkerStep";
public static final String INLINE_LOAN_COB_JOB_NAME = "INLINE_LOAN_COB";
- public static final String LOAN_IDS_PARAMETER_NAME = "LoanIds";
public static final String LOAN_COB_PARTITIONER_STEP = "Loan COB partition - Step";
- public static final String PARTITION_KEY = "partition";
private LoanCOBConstant() {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
index 61b08ba21b6..c81e92a44e5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
@@ -24,6 +24,7 @@
import org.apache.fineract.cob.common.CustomJobParameterResolver;
import org.apache.fineract.cob.conditions.BatchManagerCondition;
import org.apache.fineract.cob.listener.COBExecutionListenerRunner;
+import org.apache.fineract.cob.service.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService;
@@ -70,7 +71,7 @@ public class LoanCOBManagerConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Autowired
- private RetrieveLoanIdService retrieveLoanIdService;
+ private RetrieveLoanIdService retrieveIdService;
@Autowired
private BusinessEventNotifierService businessEventNotifierService;
@Autowired
@@ -79,7 +80,7 @@ public class LoanCOBManagerConfiguration {
@Bean
@StepScope
public LoanCOBPartitioner partitioner(@Value("#{stepExecution}") StepExecution stepExecution) {
- return new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveLoanIdService, jobOperator, stepExecution,
+ return new LoanCOBPartitioner(propertyService, cobBusinessStepService, retrieveIdService, jobOperator, stepExecution,
LoanCOBConstant.NUMBER_OF_DAYS_BEHIND);
}
@@ -109,7 +110,7 @@ public ResolveLoanCOBCustomJobParametersTasklet resolveCustomJobParametersTaskle
@Bean
public StayedLockedLoansTasklet stayedLockedTasklet() {
- return new StayedLockedLoansTasklet(businessEventNotifierService, retrieveLoanIdService);
+ return new StayedLockedLoansTasklet(businessEventNotifierService, retrieveIdService);
}
@Bean(name = "loanCOBJob")
@@ -117,7 +118,8 @@ public Job loanCOBJob(LoanCOBPartitioner partitioner) {
return new JobBuilder(JobName.LOAN_COB.name(), jobRepository) //
.listener(new COBExecutionListenerRunner(applicationContext, JobName.LOAN_COB.name())) //
.start(resolveCustomJobParametersStep()) //
- .next(loanCOBStep(partitioner)).next(stayedLockedStep()) //
+ .next(loanCOBStep(partitioner)) //
+ .next(stayedLockedStep()) //
.incrementer(new RunIdIncrementer()) //
.build();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
index b3cfdbc65d9..9bd2559f348 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
@@ -18,42 +18,33 @@
*/
package org.apache.fineract.cob.loan;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.cob.common.CommonPartitioner;
import org.apache.fineract.cob.data.BusinessStepNameAndOrder;
-import org.apache.fineract.cob.data.COBParameter;
-import org.apache.fineract.cob.data.COBPartition;
-import org.apache.fineract.cob.resolver.BusinessDateResolver;
-import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.apache.fineract.cob.service.RetrieveIdService;
import org.apache.fineract.infrastructure.springbatch.PropertyService;
import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.launch.JobExecutionNotRunningException;
import org.springframework.batch.core.launch.JobOperator;
-import org.springframework.batch.core.launch.NoSuchJobExecutionException;
import org.springframework.batch.core.partition.support.Partitioner;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.lang.NonNull;
-import org.springframework.util.StopWatch;
@Slf4j
-@RequiredArgsConstructor
-public class LoanCOBPartitioner implements Partitioner {
-
- public static final String PARTITION_PREFIX = "partition_";
+public class LoanCOBPartitioner extends CommonPartitioner implements Partitioner {
private final PropertyService propertyService;
private final COBBusinessStepService cobBusinessStepService;
- private final RetrieveLoanIdService retrieveLoanIdService;
- private final JobOperator jobOperator;
- private final StepExecution stepExecution;
- private final Long numberOfDays;
+
+ public LoanCOBPartitioner(PropertyService propertyService, COBBusinessStepService cobBusinessStepService,
+ RetrieveIdService retrieveIdService, JobOperator jobOperator, StepExecution stepExecution, Long numberOfDaysBehind) {
+ super(jobOperator, stepExecution, numberOfDaysBehind, retrieveIdService);
+ this.propertyService = propertyService;
+ this.cobBusinessStepService = cobBusinessStepService;
+
+ }
@NonNull
@Override
@@ -64,54 +55,4 @@ public Map partition(int gridSize) {
return getPartitions(partitionSize, cobBusinessSteps);
}
- private Map getPartitions(int partitionSize, Set cobBusinessSteps) {
- if (cobBusinessSteps.isEmpty()) {
- stopJobExecution();
- return Map.of();
- }
- LocalDate businessDate = BusinessDateResolver.resolve(stepExecution);
- boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution);
- StopWatch sw = new StopWatch();
- sw.start();
- List loanCOBPartitions = new ArrayList<>(
- retrieveLoanIdService.retrieveLoanCOBPartitions(numberOfDays, businessDate, isCatchUp, partitionSize));
- sw.stop();
- // if there is no loan to be closed, we still would like to create at least one partition
-
- if (loanCOBPartitions.isEmpty()) {
- loanCOBPartitions.add(new COBPartition(0L, 0L, 1L, 0L));
- }
- log.info(
- "LoanCOBPartitioner found {} loans to be processed as part of COB. {} partitions were created using partition size {}. RetrieveLoanCOBPartitions was executed in {} ms.",
- getLoanCount(loanCOBPartitions), loanCOBPartitions.size(), partitionSize, sw.getTotalTimeMillis());
- return loanCOBPartitions.stream().collect(Collectors.toMap(l -> PARTITION_PREFIX + l.getPageNo(),
- l -> createNewPartition(cobBusinessSteps, l, businessDate, isCatchUp)));
- }
-
- private long getLoanCount(List loanCOBPartitions) {
- return loanCOBPartitions.stream().map(COBPartition::getCount).reduce(0L, Long::sum);
- }
-
- private ExecutionContext createNewPartition(Set cobBusinessSteps, COBPartition loanCOBPartition,
- LocalDate businessDate, boolean isCatchUp) {
- ExecutionContext executionContext = new ExecutionContext();
- executionContext.put(LoanCOBConstant.BUSINESS_STEPS, cobBusinessSteps);
- executionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER,
- new COBParameter(loanCOBPartition.getMinId(), loanCOBPartition.getMaxId()));
- executionContext.put(LoanCOBConstant.PARTITION_KEY, PARTITION_PREFIX + loanCOBPartition.getPageNo());
- executionContext.put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, businessDate.toString());
- executionContext.put(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, Boolean.toString(isCatchUp));
- return executionContext;
- }
-
- private void stopJobExecution() {
- Long jobId = stepExecution.getJobExecution().getId();
- try {
- jobOperator.stop(jobId);
- } catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
- log.error("There is no running execution for the given execution ID. Execution ID: {}", jobId);
- throw new RuntimeException(e);
- }
-
- }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
index 17d97f57090..b84b8001593 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
@@ -22,7 +22,11 @@
import org.apache.fineract.cob.common.InitialisationTasklet;
import org.apache.fineract.cob.common.ResetContextTasklet;
import org.apache.fineract.cob.conditions.BatchWorkerCondition;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LockingService;
import org.apache.fineract.cob.listener.ChunkProcessingLoanItemListener;
+import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper;
+import org.apache.fineract.cob.service.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService;
@@ -74,12 +78,12 @@ public class LoanCOBWorkerConfiguration {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
- private RetrieveLoanIdService retrieveLoanIdService;
+ private RetrieveLoanIdService retrieveIdService;
@Autowired
private FineractProperties fineractProperties;
@Autowired
- private LoanLockingService loanLockingService;
+ private LockingService loanLockingService;
@Autowired
private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
@@ -165,7 +169,7 @@ public ChunkProcessingLoanItemListener loanItemListener() {
@Bean
public ApplyLoanLockTasklet applyLock() {
- return new ApplyLoanLockTasklet(fineractProperties, loanLockingService, retrieveLoanIdService, transactionTemplate);
+ return new ApplyLoanLockTasklet(fineractProperties, loanLockingService, retrieveIdService, transactionTemplate);
}
@Bean
@@ -176,7 +180,7 @@ public ResetContextTasklet resetContext() {
@Bean
@StepScope
public LoanItemReader cobWorkerItemReader() {
- return new LoanItemReader(loanRepository, retrieveLoanIdService, loanLockingService);
+ return new LoanItemReader(loanRepository, new BeforeStepLockingItemReaderHelper<>(retrieveIdService, loanLockingService));
}
@Bean
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java
index bd00a20ad3e..98aa5603c27 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInlineCOBConfig.java
@@ -22,6 +22,8 @@
import org.apache.fineract.cob.common.CustomJobParameterResolver;
import org.apache.fineract.cob.common.ResetContextTasklet;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LockingService;
import org.apache.fineract.cob.listener.InlineCOBLoanItemListener;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
import org.apache.fineract.infrastructure.jobs.service.JobName;
@@ -67,14 +69,14 @@ public class LoanInlineCOBConfig {
@Autowired
private CustomJobParameterResolver customJobParameterResolver;
@Autowired
- private LoanLockingService loanLockingService;
+ private LockingService loanLockingService;
@Autowired
private ProgressiveLoanModelProcessingService progressiveLoanModelProcessingService;
@Bean
- public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() {
- return new InlineLoanCOBBuildExecutionContextTasklet(cobBusinessStepService, customJobParameterRepository,
- customJobParameterResolver);
+ public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() {
+ return new InlineLoanCOBBuildExecutionContextTasklet<>(cobBusinessStepService, customJobParameterRepository,
+ customJobParameterResolver, LoanCOBBusinessStep.class, LoanCOBConstant.LOAN_COB_JOB_NAME);
}
@Bean
@@ -136,7 +138,7 @@ public ResetContextTasklet inlineCOBResetContext() {
@Bean
public ExecutionContextPromotionListener inlineCobPromotionListener() {
ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
- listener.setKeys(new String[] { LoanCOBConstant.LOAN_COB_PARAMETER, LoanCOBConstant.BUSINESS_STEPS,
+ listener.setKeys(new String[] { LoanCOBConstant.COB_PARAMETER, LoanCOBConstant.BUSINESS_STEPS,
LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME });
return listener;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
index f6c52f2df1c..0305897ea50 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
@@ -18,61 +18,29 @@
*/
package org.apache.fineract.cob.loan;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.LinkedBlockingQueue;
import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.cob.converter.COBParameterConverter;
-import org.apache.fineract.cob.data.COBParameter;
import org.apache.fineract.cob.domain.LoanAccountLock;
-import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.resolver.CatchUpFlagResolver;
+import org.apache.fineract.cob.service.BeforeStepLockingItemReaderHelper;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
-import org.springframework.batch.item.ExecutionContext;
import org.springframework.lang.NonNull;
@Slf4j
-public class LoanItemReader extends AbstractLoanItemReader {
+public class LoanItemReader extends AbstractLoanItemReader {
- private final RetrieveLoanIdService retrieveLoanIdService;
- private final LoanLockingService loanLockingService;
+ private final BeforeStepLockingItemReaderHelper beforeStepLockingItemReaderHelper;
- public LoanItemReader(LoanRepository loanRepository, RetrieveLoanIdService retrieveLoanIdService,
- LoanLockingService loanLockingService) {
+ public LoanItemReader(LoanRepository loanRepository,
+ BeforeStepLockingItemReaderHelper beforeStepLockingItemReaderHelper) {
super(loanRepository);
- this.retrieveLoanIdService = retrieveLoanIdService;
- this.loanLockingService = loanLockingService;
+ this.beforeStepLockingItemReaderHelper = beforeStepLockingItemReaderHelper;
}
@BeforeStep
- @SuppressWarnings({ "unchecked" })
public void beforeStep(@NonNull StepExecution stepExecution) {
- ExecutionContext executionContext = stepExecution.getExecutionContext();
- COBParameter loanCOBParameter = COBParameterConverter.convert(executionContext.get(LoanCOBConstant.LOAN_COB_PARAMETER));
- List loanIds;
- boolean isCatchUp = CatchUpFlagResolver.resolve(stepExecution);
- if (Objects.isNull(loanCOBParameter)
- || (Objects.isNull(loanCOBParameter.getMinAccountId()) && Objects.isNull(loanCOBParameter.getMaxAccountId()))
- || (loanCOBParameter.getMinAccountId().equals(0L) && loanCOBParameter.getMaxAccountId().equals(0L))) {
- loanIds = Collections.emptyList();
- } else {
- loanIds = retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter,
- isCatchUp);
- if (!loanIds.isEmpty()) {
- List lockedByCOBChunkProcessingAccountIds = getLoanIdsLockedWithChunkProcessingLock(loanIds);
- loanIds.retainAll(lockedByCOBChunkProcessingAccountIds);
- }
- }
- setRemainingData(new LinkedBlockingQueue<>(loanIds));
+ setRemainingData(beforeStepLockingItemReaderHelper.filterRemainingData(stepExecution));
}
- private List getLoanIdsLockedWithChunkProcessingLock(List loanIds) {
- List accountLocks = new ArrayList<>(
- loanLockingService.findAllByLoanIdInAndLockOwner(loanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING));
- return accountLocks.stream().map(LoanAccountLock::getLoanId).toList();
- }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java
index 2696ee03a13..28c83148e13 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java
@@ -18,11 +18,13 @@
*/
package org.apache.fineract.cob.loan;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.LockingService;
public class LoanItemWriter extends AbstractLoanItemWriter {
- public LoanItemWriter(LoanLockingService loanLockingService) {
+ public LoanItemWriter(LockingService loanLockingService) {
super(loanLockingService);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java
index 660051907fe..91dae8c06d8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingConfiguration.java
@@ -18,7 +18,9 @@
*/
package org.apache.fineract.cob.loan;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockingService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -38,7 +40,7 @@ public class LoanLockingConfiguration {
@Bean
@ConditionalOnMissingBean
- public LoanLockingService retrieveLoanLockingService() {
+ public LockingService retrieveLoanLockingService() {
return new LoanLockingServiceImpl(jdbcTemplate, fineractProperties, loanAccountLockRepository);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
index 5a90c781f4a..18a47a547c1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
@@ -18,79 +18,36 @@
*/
package org.apache.fineract.cob.loan;
-import java.sql.PreparedStatement;
-import java.time.LocalDate;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.AbstractLockingService;
import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
-import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.springframework.jdbc.core.JdbcTemplate;
-@RequiredArgsConstructor
@Slf4j
-public class LoanLockingServiceImpl implements LoanLockingService {
+public class LoanLockingServiceImpl extends AbstractLockingService {
private static final String BATCH_LOAN_LOCK_INSERT = """
INSERT INTO m_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date) VALUES (?,?,?,?,?)
""";
- private final JdbcTemplate jdbcTemplate;
- private final FineractProperties fineractProperties;
- private final LoanAccountLockRepository loanAccountLockRepository;
-
- @Override
- public void upgradeLock(List accountsToLock, LockOwner lockOwner) {
- jdbcTemplate.batchUpdate("""
- UPDATE m_loan_account_locks SET version= version + 1, lock_owner = ?, lock_placed_on = ? WHERE loan_id = ?
- """, accountsToLock, getInClauseParameterSizeLimit(), (ps, id) -> {
- ps.setString(1, lockOwner.name());
- ps.setObject(2, DateUtils.getAuditOffsetDateTime());
- ps.setLong(3, id);
- });
- }
-
- @Override
- public List findAllByLoanIdIn(List loanIds) {
- return loanAccountLockRepository.findAllByLoanIdIn(loanIds);
- }
-
- @Override
- public LoanAccountLock findByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner) {
- return loanAccountLockRepository.findByLoanIdAndLockOwner(loanId, lockOwner).orElseGet(() -> {
- log.warn("There is no lock for loan account with id: {}", loanId);
- return null;
- });
- }
+ private static final String BATCH_LOAN_LOCK_UPGRADE = """
+ UPDATE m_loan_account_locks SET version= version + 1, lock_owner = ?, lock_placed_on = ? WHERE loan_id = ?
+ """;
- @Override
- public List findAllByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) {
- return loanAccountLockRepository.findAllByLoanIdInAndLockOwner(loanIds, lockOwner);
+ public LoanLockingServiceImpl(JdbcTemplate jdbcTemplate, FineractProperties fineractProperties,
+ LoanAccountLockRepository loanAccountLockRepository) {
+ super(jdbcTemplate, fineractProperties, loanAccountLockRepository);
}
@Override
- public void applyLock(List loanIds, LockOwner lockOwner) {
- LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
- jdbcTemplate.batchUpdate(BATCH_LOAN_LOCK_INSERT, loanIds, loanIds.size(), (PreparedStatement ps, Long loanId) -> {
- ps.setLong(1, loanId);
- ps.setLong(2, 1);
- ps.setString(3, lockOwner.name());
- ps.setObject(4, DateUtils.getAuditOffsetDateTime());
- ps.setObject(5, cobBusinessDate);
- });
+ protected String getBatchLoanLockUpgrade() {
+ return BATCH_LOAN_LOCK_UPGRADE;
}
@Override
- public void deleteByLoanIdInAndLockOwner(List loanIds, LockOwner lockOwner) {
- loanAccountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, lockOwner);
- }
-
- private int getInClauseParameterSizeLimit() {
- return fineractProperties.getQuery().getInClauseParameterSizeLimit();
+ protected String getBatchLoanLockInsert() {
+ return BATCH_LOAN_LOCK_INSERT;
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java
similarity index 93%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
rename to fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java
index cdc09d38d5b..8fa29493dfc 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedIdServiceImpl.java
@@ -18,8 +18,6 @@
*/
package org.apache.fineract.cob.loan;
-import java.sql.ResultSet;
-import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -30,6 +28,8 @@
import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.data.COBParameter;
import org.apache.fineract.cob.data.COBPartition;
+import org.apache.fineract.cob.service.RetrieveIdService;
+import org.apache.fineract.cob.service.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
@@ -38,7 +38,7 @@
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@RequiredArgsConstructor
-public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdService {
+public class RetrieveAllNonClosedIdServiceImpl implements RetrieveLoanIdService {
private static final Collection NON_CLOSED_LOAN_STATUSES = new ArrayList<>(
Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED, LoanStatus.ACTIVE,
@@ -67,11 +67,7 @@ public List retrieveLoanCOBPartitions(Long numberOfDays, LocalDate
parameters.addValue("pageSize", partitionSize);
parameters.addValue("statusIds", List.of(100, 200, 300, 303, 304));
parameters.addValue("businessDate", businessDate.minusDays(numberOfDays));
- return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveAllNonClosedLoanIdServiceImpl::mapRow);
- }
-
- private static COBPartition mapRow(ResultSet rs, int rowNum) throws SQLException {
- return new COBPartition(rs.getLong("min"), rs.getLong("max"), rs.getLong("page"), rs.getLong("count"));
+ return namedParameterJdbcTemplate.query(sql.toString(), parameters, RetrieveIdService::mapRow);
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java
index 43aa60e6e15..1c18aa52412 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdConfiguration.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.cob.loan;
+import org.apache.fineract.cob.service.RetrieveLoanIdService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -37,6 +38,6 @@ public class RetrieveLoanIdConfiguration {
@Bean
@ConditionalOnMissingBean
public RetrieveLoanIdService retrieveLoanIdService() {
- return new RetrieveAllNonClosedLoanIdServiceImpl(loanRepository, namedParameterJdbcTemplate);
+ return new RetrieveAllNonClosedIdServiceImpl(loanRepository, namedParameterJdbcTemplate);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java
index 2bc4f773b90..b23e58148f6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/StayedLockedLoansTasklet.java
@@ -26,6 +26,7 @@
import org.apache.fineract.cob.data.COBIdAndExternalIdAndAccountNo;
import org.apache.fineract.cob.data.LoanAccountStayedLockedData;
import org.apache.fineract.cob.data.LoanAccountsStayedLockedData;
+import org.apache.fineract.cob.service.RetrieveIdService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
@@ -39,7 +40,7 @@
public class StayedLockedLoansTasklet implements Tasklet {
private final BusinessEventNotifierService businessEventNotifierService;
- private final RetrieveLoanIdService retrieveLoanIdService;
+ private final RetrieveIdService retrieveIdService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -52,7 +53,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon
private LoanAccountsStayedLockedData buildLoanAccountData() {
LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
- List stayedLockedLoanAccounts = retrieveLoanIdService
+ List stayedLockedLoanAccounts = retrieveIdService
.findAllStayedLockedByCobBusinessDate(cobBusinessDate);
List loanAccounts = new ArrayList<>();
stayedLockedLoanAccounts.forEach(loanAccount -> {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalInlineCOBLoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalInlineCOBLoanItemReader.java
new file mode 100644
index 00000000000..c8407525529
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalInlineCOBLoanItemReader.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.loan;
+
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan;
+import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.annotation.BeforeStep;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.lang.NonNull;
+
+public class WorkingCapitalInlineCOBLoanItemReader extends AbstractLoanItemReader {
+
+ public WorkingCapitalInlineCOBLoanItemReader(WorkingCapitalLoanRepository loanRepository) {
+ super(loanRepository);
+ }
+
+ @BeforeStep
+ @SuppressWarnings({ "unchecked" })
+ public void beforeStep(@NonNull StepExecution stepExecution) {
+ ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext();
+ List loanIds = (List) executionContext.get(COBConstant.COB_PARAMETER);
+ setRemainingData(new LinkedBlockingQueue<>(loanIds));
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalLoanInlineCOBConfig.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalLoanInlineCOBConfig.java
new file mode 100644
index 00000000000..1413596f8b7
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/WorkingCapitalLoanInlineCOBConfig.java
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.loan;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.common.CustomJobParameterResolver;
+import org.apache.fineract.cob.common.ResetContextTasklet;
+import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
+import org.apache.fineract.cob.domain.LockingService;
+import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock;
+import org.apache.fineract.cob.workingcapitalloan.InlineWorkingCapitalLoanCOBWorkerItemListener;
+import org.apache.fineract.cob.workingcapitalloan.InlineWorkingCapitalLoanCOBWorkerItemWriter;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanInlineCOBWorkerItemProcessor;
+import org.apache.fineract.cob.workingcapitalloan.businessstep.WorkingCapitalLoanCOBBusinessStep;
+import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
+import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.apache.fineract.infrastructure.springbatch.PropertyService;
+import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan;
+import org.apache.fineract.portfolio.workingcapitalloanproduct.repository.WorkingCapitalLoanRepository;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.Step;
+import org.springframework.batch.core.configuration.annotation.JobScope;
+import org.springframework.batch.core.job.builder.JobBuilder;
+import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import org.springframework.batch.core.listener.ExecutionContextPromotionListener;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.step.builder.StepBuilder;
+import org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Configuration
+@EnableBatchIntegration
+@Conditional(LoanCOBEnabledCondition.class)
+@RequiredArgsConstructor
+public class WorkingCapitalLoanInlineCOBConfig {
+
+ private final JobRepository jobRepository;
+ private final PlatformTransactionManager transactionManager;
+ private final PropertyService propertyService;
+ private final COBBusinessStepService cobBusinessStepService;
+ private final TransactionTemplate transactionTemplate;
+ private final CustomJobParameterRepository customJobParameterRepository;
+ private final CustomJobParameterResolver customJobParameterResolver;
+ private final LockingService loanLockingService;
+ private final WorkingCapitalLoanRepository loanRepository;
+
+ @Bean
+ public InlineLoanCOBBuildExecutionContextTasklet inlineWorkingCapitalLoanCOBBuildExecutionContextTasklet() {
+ return new InlineLoanCOBBuildExecutionContextTasklet<>(cobBusinessStepService, customJobParameterRepository,
+ customJobParameterResolver, WorkingCapitalLoanCOBBusinessStep.class,
+ WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_LOAN_COB_JOB_NAME);
+ }
+
+ @Bean
+ protected Step inlineWorkingCapitalLoanCOBBuildExecutionContextStep(
+ InlineLoanCOBBuildExecutionContextTasklet inlineWorkingCapitalLoanCOBBuildExecutionContextTasklet,
+ ExecutionContextPromotionListener inlineWorkingCapitalLoanCobPromotionListener) {
+ return new StepBuilder("Inline COB build execution context step", jobRepository)
+ .tasklet(inlineWorkingCapitalLoanCOBBuildExecutionContextTasklet, transactionManager)
+ .listener(inlineWorkingCapitalLoanCobPromotionListener).build();
+ }
+
+ @Bean
+ public Step inlineWorkingCapitalLoanCOBStep(WorkingCapitalInlineCOBLoanItemReader inlineWorkingCapitalLoanCobWorkerItemReader,
+ WorkingCapitalLoanInlineCOBWorkerItemProcessor inlineWorkingCapitalLoanCobWorkerItemProcessor,
+ InlineWorkingCapitalLoanCOBWorkerItemWriter inlineWorkingCapitalLoanCobWorkerItemWriter,
+ InlineWorkingCapitalLoanCOBWorkerItemListener inlineWorkingCapitalLoanCobLoanItemListener) {
+ return new StepBuilder("Inline Working Capital Loan COB Step", jobRepository)
+ .chunk(propertyService.getChunkSize(JobName.WORKING_CAPITAL_LOAN_COB_JOB.name()),
+ transactionManager)
+ .reader(inlineWorkingCapitalLoanCobWorkerItemReader).processor(inlineWorkingCapitalLoanCobWorkerItemProcessor)
+ .writer(inlineWorkingCapitalLoanCobWorkerItemWriter).listener(inlineWorkingCapitalLoanCobLoanItemListener).build();
+ }
+
+ @Bean(name = "inlineWorkingCapitalLoanCOBJob")
+ public Job inlineWorkingCapitalLoanCOBJob(Step inlineWorkingCapitalLoanCOBBuildExecutionContextStep,
+ Step inlineWorkingCapitalLoanCOBStep, Step inlineWorkingCapitalLoanCOBResetContextStep) {
+ return new JobBuilder(WorkingCapitalLoanCOBConstant.INLINE_WORKING_CAPITAL_LOAN_COB_JOB_NAME, jobRepository) //
+ .start(inlineWorkingCapitalLoanCOBBuildExecutionContextStep).next(inlineWorkingCapitalLoanCOBStep)
+ .next(inlineWorkingCapitalLoanCOBResetContextStep) //
+ .incrementer(new RunIdIncrementer()) //
+ .build();
+ }
+
+ @JobScope
+ @Bean
+ public WorkingCapitalInlineCOBLoanItemReader inlineWorkingCapitalLoanCobWorkerItemReader() {
+ return new WorkingCapitalInlineCOBLoanItemReader(loanRepository);
+ }
+
+ @JobScope
+ @Bean
+ public WorkingCapitalLoanInlineCOBWorkerItemProcessor inlineWorkingCapitalLoanCobWorkerItemProcessor() {
+ return new WorkingCapitalLoanInlineCOBWorkerItemProcessor(cobBusinessStepService);
+ }
+
+ @Bean
+ public Step inlineWorkingCapitalLoanCOBResetContextStep(ResetContextTasklet inlineWorkingCapitalLoanCOBResetContext) {
+ return new StepBuilder("Reset context - Step", jobRepository).tasklet(inlineWorkingCapitalLoanCOBResetContext, transactionManager)
+ .build();
+ }
+
+ @Bean
+ public InlineWorkingCapitalLoanCOBWorkerItemWriter inlineWorkingCapitalLoanCobWorkerItemWriter() {
+ return new InlineWorkingCapitalLoanCOBWorkerItemWriter(loanLockingService, loanRepository);
+ }
+
+ @Bean
+ public InlineWorkingCapitalLoanCOBWorkerItemListener inlineWorkingCapitalLoanCobLoanItemListener() {
+ return new InlineWorkingCapitalLoanCOBWorkerItemListener(loanLockingService, transactionTemplate);
+ }
+
+ @Bean
+ public ResetContextTasklet inlineWorkingCapitalLoanCOBResetContext() {
+ return new ResetContextTasklet();
+ }
+
+ @Bean
+ public ExecutionContextPromotionListener inlineWorkingCapitalLoanCobPromotionListener() {
+ ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
+ listener.setKeys(new String[] { COBConstant.COB_PARAMETER, COBConstant.BUSINESS_STEPS, COBConstant.BUSINESS_DATE_PARAMETER_NAME });
+ return listener;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCOBExecutorService.java
new file mode 100644
index 00000000000..fd149cd9cd3
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCOBExecutorService.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+
+public interface AsyncCOBExecutorService {
+
+ void executeLoanCOBCatchUpAsync(FineractContext context);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCommonCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCommonCOBExecutorService.java
new file mode 100644
index 00000000000..b9897d72099
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncCommonCOBExecutorService.java
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.config.TaskExecutorConstant;
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
+import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
+import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
+import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
+import org.apache.fineract.infrastructure.jobs.service.JobStarter;
+import org.apache.fineract.infrastructure.jobs.service.SchedulerServiceConstants;
+import org.quartz.JobExecutionException;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobParametersInvalidException;
+import org.springframework.batch.core.configuration.JobLocator;
+import org.springframework.batch.core.launch.NoSuchJobException;
+import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
+import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
+import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.scheduling.annotation.Async;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AsyncCommonCOBExecutorService implements AsyncCOBExecutorService {
+
+ private final JobLocator jobLocator;
+ private final ScheduledJobDetailRepository scheduledJobDetailRepository;
+ private final JobStarter jobStarter;
+ private final RetrieveIdService retrieveIdService;
+
+ @Override
+ @Async(TaskExecutorConstant.LOAN_COB_CATCH_UP_TASK_EXECUTOR_BEAN_NAME)
+ public void executeLoanCOBCatchUpAsync(FineractContext context) {
+ try {
+ ThreadLocalContextUtil.init(context);
+ LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
+ List loanIdAndLastClosedBusinessDate = retrieveIdService
+ .retrieveLoanIdsOldestCobProcessed(cobBusinessDate);
+
+ LocalDate oldestCOBProcessedDate = !loanIdAndLastClosedBusinessDate.isEmpty()
+ ? loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate()
+ : cobBusinessDate;
+ if (DateUtils.isBefore(oldestCOBProcessedDate, cobBusinessDate)) {
+ executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate, cobBusinessDate);
+ }
+ } catch (NoSuchJobException e) {
+ // Throwing an error here is useless as it will be swallowed hence it is async method
+ log.error("Job not found: {}", getJobName(), new JobNotFoundException(getJobName(), e));
+ } catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException
+ | JobExecutionAlreadyRunningException | JobExecutionException e) {
+ // Throwing an error here is useless as it will be swallowed hence it is async method
+ log.error("Error executing job", e);
+ } finally {
+ ThreadLocalContextUtil.reset();
+ }
+ }
+
+ public abstract String getJobName();
+
+ public abstract String getJobHumanReadableName();
+
+ private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProcessedDate, LocalDate cobBusinessDate)
+ throws NoSuchJobException, JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
+ JobParametersInvalidException, JobRestartException, JobExecutionException {
+ Job job = jobLocator.getJob(getJobName());
+ ScheduledJobDetail scheduledJobDetail = scheduledJobDetailRepository.findByJobName(getJobHumanReadableName());
+ LocalDate executingBusinessDate = oldestCOBProcessedDate.plusDays(1);
+ String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
+
+ while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
+ JobParameterDTO jobParameterDTO = new JobParameterDTO(COBConstant.BUSINESS_DATE_PARAMETER_NAME,
+ executingBusinessDate.format(DateTimeFormatter.ISO_DATE));
+ JobParameterDTO jobParameterCatchUpDTO = new JobParameterDTO(COBConstant.IS_CATCH_UP_PARAMETER_NAME, "true");
+ JobParameterDTO tenantParameterDTO = new JobParameterDTO(SchedulerServiceConstants.TENANT_IDENTIFIER, tenantIdentifier);
+ Set jobParameters = new HashSet<>();
+ Collections.addAll(jobParameters, jobParameterDTO, jobParameterCatchUpDTO, tenantParameterDTO);
+ jobStarter.run(job, scheduledJobDetail, jobParameters, tenantIdentifier);
+ executingBusinessDate = executingBusinessDate.plusDays(1);
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java
index e2001f31f56..ca82d3ce6cf 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java
@@ -18,9 +18,4 @@
*/
package org.apache.fineract.cob.service;
-import org.apache.fineract.infrastructure.core.domain.FineractContext;
-
-public interface AsyncLoanCOBExecutorService {
-
- void executeLoanCOBCatchUpAsync(FineractContext context);
-}
+public interface AsyncLoanCOBExecutorService extends AsyncCOBExecutorService {}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
index 8de65836790..9d53e73d80f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
@@ -18,96 +18,41 @@
*/
package org.apache.fineract.cob.service;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
-import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.loan.LoanCOBConstant;
-import org.apache.fineract.cob.loan.RetrieveLoanIdService;
-import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.TaskExecutorConstant;
import org.apache.fineract.infrastructure.core.domain.FineractContext;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
-import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
-import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
-import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
import org.apache.fineract.infrastructure.jobs.service.JobStarter;
-import org.apache.fineract.infrastructure.jobs.service.SchedulerServiceConstants;
-import org.quartz.JobExecutionException;
-import org.springframework.batch.core.Job;
-import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.configuration.JobLocator;
-import org.springframework.batch.core.launch.NoSuchJobException;
-import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
-import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
-import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.context.annotation.Conditional;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Slf4j
@Service
-@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
-public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorService {
+public class AsyncLoanCOBExecutorServiceImpl extends AsyncCommonCOBExecutorService implements AsyncLoanCOBExecutorService {
- private final JobLocator jobLocator;
- private final ScheduledJobDetailRepository scheduledJobDetailRepository;
- private final JobStarter jobStarter;
- private final RetrieveLoanIdService retrieveLoanIdService;
+ public AsyncLoanCOBExecutorServiceImpl(JobLocator jobLocator, ScheduledJobDetailRepository scheduledJobDetailRepository,
+ JobStarter jobStarter, RetrieveLoanIdService retrieveIdService) {
+ super(jobLocator, scheduledJobDetailRepository, jobStarter, retrieveIdService);
+ }
@Override
@Async(TaskExecutorConstant.LOAN_COB_CATCH_UP_TASK_EXECUTOR_BEAN_NAME)
public void executeLoanCOBCatchUpAsync(FineractContext context) {
- try {
- ThreadLocalContextUtil.init(context);
- LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
- List loanIdAndLastClosedBusinessDate = retrieveLoanIdService
- .retrieveLoanIdsOldestCobProcessed(cobBusinessDate);
-
- LocalDate oldestCOBProcessedDate = !loanIdAndLastClosedBusinessDate.isEmpty()
- ? loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate()
- : cobBusinessDate;
- if (DateUtils.isBefore(oldestCOBProcessedDate, cobBusinessDate)) {
- executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate, cobBusinessDate);
- }
- } catch (NoSuchJobException e) {
- // Throwing an error here is useless as it will be swallowed hence it is async method
- log.error("Job not found: {}", LoanCOBConstant.JOB_NAME, new JobNotFoundException(LoanCOBConstant.JOB_NAME, e));
- } catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException
- | JobExecutionAlreadyRunningException | JobExecutionException e) {
- // Throwing an error here is useless as it will be swallowed hence it is async method
- log.error("Error executing job", e);
- } finally {
- ThreadLocalContextUtil.reset();
- }
+ super.executeLoanCOBCatchUpAsync(context);
}
- private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProcessedDate, LocalDate cobBusinessDate)
- throws NoSuchJobException, JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
- JobParametersInvalidException, JobRestartException, JobExecutionException {
- Job job = jobLocator.getJob(LoanCOBConstant.JOB_NAME);
- ScheduledJobDetail scheduledJobDetail = scheduledJobDetailRepository.findByJobName(LoanCOBConstant.JOB_HUMAN_READABLE_NAME);
- LocalDate executingBusinessDate = oldestCOBProcessedDate.plusDays(1);
- String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
+ @Override
+ public String getJobName() {
+ return LoanCOBConstant.JOB_NAME;
+ }
- while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
- JobParameterDTO jobParameterDTO = new JobParameterDTO(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME,
- executingBusinessDate.format(DateTimeFormatter.ISO_DATE));
- JobParameterDTO jobParameterCatchUpDTO = new JobParameterDTO(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "true");
- JobParameterDTO tenantParameterDTO = new JobParameterDTO(SchedulerServiceConstants.TENANT_IDENTIFIER, tenantIdentifier);
- Set jobParameters = new HashSet<>();
- Collections.addAll(jobParameters, jobParameterDTO, jobParameterCatchUpDTO, tenantParameterDTO);
- jobStarter.run(job, scheduledJobDetail, jobParameters, tenantIdentifier);
- executingBusinessDate = executingBusinessDate.plusDays(1);
- }
+ @Override
+ public String getJobHumanReadableName() {
+ return LoanCOBConstant.JOB_HUMAN_READABLE_NAME;
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorService.java
new file mode 100644
index 00000000000..0be3f76579c
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorService.java
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+public interface AsyncWorkingCapitalLoanCOBExecutorService extends AsyncCOBExecutorService {}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorServiceImpl.java
new file mode 100644
index 00000000000..4564712a900
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncWorkingCapitalLoanCOBExecutorServiceImpl.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanCOBConstant;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanRetrieveIdService;
+import org.apache.fineract.infrastructure.core.config.TaskExecutorConstant;
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
+import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.apache.fineract.infrastructure.jobs.service.JobStarter;
+import org.springframework.batch.core.configuration.JobLocator;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@Conditional(LoanCOBEnabledCondition.class)
+public class AsyncWorkingCapitalLoanCOBExecutorServiceImpl extends AsyncCommonCOBExecutorService
+ implements AsyncWorkingCapitalLoanCOBExecutorService {
+
+ public AsyncWorkingCapitalLoanCOBExecutorServiceImpl(JobLocator jobLocator, ScheduledJobDetailRepository scheduledJobDetailRepository,
+ JobStarter jobStarter, WorkingCapitalLoanRetrieveIdService retrieveIdService) {
+ super(jobLocator, scheduledJobDetailRepository, jobStarter, retrieveIdService);
+ }
+
+ @Override
+ @Async(TaskExecutorConstant.WORKING_CAPITAL_LOAN_COB_CATCH_UP_TASK_EXECUTOR_BEAN_NAME)
+ public void executeLoanCOBCatchUpAsync(FineractContext context) {
+ super.executeLoanCOBCatchUpAsync(context);
+ }
+
+ @Override
+ public String getJobName() {
+ return JobName.WORKING_CAPITAL_LOAN_COB_JOB.name();
+ }
+
+ @Override
+ public String getJobHumanReadableName() {
+ return WorkingCapitalLoanCOBConstant.WORKING_CAPITAL_JOB_HUMAN_READABLE_NAME;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/COBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/COBCatchUpService.java
new file mode 100644
index 00000000000..5d793ab9013
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/COBCatchUpService.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
+import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+
+public interface COBCatchUpService {
+
+ void unlockHardLockedLoans();
+
+ OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan();
+
+ void executeLoanCOBCatchUp();
+
+ IsCatchUpRunningDTO isCatchUpRunning();
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/CommonCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/CommonCOBCatchUpService.java
new file mode 100644
index 00000000000..de102e0bbee
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/CommonCOBCatchUpService.java
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.cob.service;
+
+import java.time.LocalDate;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
+import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
+import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+import org.apache.fineract.cob.domain.AccountLock;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository;
+
+@RequiredArgsConstructor
+public abstract class CommonCOBCatchUpService implements COBCatchUpService {
+
+ private final AsyncCOBExecutorService asyncLoanCOBExecutorService;
+ private final JobExecutionRepository jobExecutionRepository;
+ private final RetrieveIdService retrieveIdService;
+ private final AccountLockService accountLockService;
+
+ @Override
+ public void unlockHardLockedLoans() {
+ accountLockService.updateCobAndRemoveLocks();
+ }
+
+ @Override
+ public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
+ List loanIdAndLastClosedBusinessDate = retrieveIdService
+ .retrieveLoanIdsOldestCobProcessed(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE));
+ OldestCOBProcessedLoanDTO oldestCOBProcessedLoanDTO = new OldestCOBProcessedLoanDTO();
+ oldestCOBProcessedLoanDTO.setLoanIds(loanIdAndLastClosedBusinessDate.stream().map(COBIdAndLastClosedBusinessDate::getId).toList());
+ oldestCOBProcessedLoanDTO
+ .setCobProcessedDate(loanIdAndLastClosedBusinessDate.stream().map(COBIdAndLastClosedBusinessDate::getLastClosedBusinessDate)
+ .findFirst().orElse(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)));
+ oldestCOBProcessedLoanDTO.setCobBusinessDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE));
+ return oldestCOBProcessedLoanDTO;
+ }
+
+ @Override
+ public void executeLoanCOBCatchUp() {
+ FineractContext context = ThreadLocalContextUtil.getContext();
+ asyncLoanCOBExecutorService.executeLoanCOBCatchUpAsync(context);
+ }
+
+ @Override
+ public IsCatchUpRunningDTO isCatchUpRunning() {
+ LocalDate runningCatchUpBusinessDate = jobExecutionRepository.getBusinessDateOfRunningJobByExecutionParameter(getJobName(),
+ COBConstant.COB_CUSTOM_JOB_PARAMETER_KEY, COBConstant.IS_CATCH_UP_PARAMETER_NAME, "true",
+ COBConstant.BUSINESS_DATE_PARAMETER_NAME);
+ return new IsCatchUpRunningDTO(runningCatchUpBusinessDate != null, runningCatchUpBusinessDate);
+ }
+
+ public abstract String getJobName();
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineCommonLockableCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineCommonLockableCOBExecutorService.java
new file mode 100644
index 00000000000..f1ba533bfe3
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineCommonLockableCOBExecutorService.java
@@ -0,0 +1,256 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
+
+import com.google.common.collect.Lists;
+import com.google.gson.Gson;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.cob.COBConstant;
+import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
+import org.apache.fineract.cob.domain.AccountLock;
+import org.apache.fineract.cob.domain.AccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.exceptions.AccountLockCannotBeOverruledException;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
+import org.apache.fineract.infrastructure.core.exception.PlatformRequestBodyItemLimitValidationException;
+import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
+import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
+import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
+import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants;
+import org.springframework.batch.core.BatchStatus;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameter;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.configuration.JobLocator;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.launch.NoSuchJobException;
+import org.springframework.lang.NonNull;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class InlineCommonLockableCOBExecutorService implements InlineExecutorService {
+
+ private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
+ private final AccountLockRepository loanAccountLockRepository;
+ private final InlineLoanCOBExecutionDataParser dataParser;
+ private final JobLauncher jobLauncher;
+ private final JobLocator jobLocator;
+ private final JobExplorer jobExplorer;
+ private final TransactionTemplate transactionTemplate;
+ private final CustomJobParameterRepository customJobParameterRepository;
+ private final PlatformSecurityContext context;
+ private final RetrieveIdService retrieveIdService;
+ private final FineractProperties fineractProperties;
+
+ private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
+
+ public abstract T createAccountLock(Long loanId, LockOwner loanInlineCobProcessing, LocalDate businessDate);
+
+ @Override
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ public CommandProcessingResult executeInlineJob(JsonCommand command, String jobName) throws AccountLockCannotBeOverruledException {
+ List loanIds = dataParser.parseExecution(command);
+ validateLoanIdsListSize(loanIds);
+ execute(loanIds, jobName);
+ return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
+ }
+
+ @Override
+ public void execute(List loanIds, String jobName) {
+ LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
+ List loansToBeProcessed = getLoansToBeProcessed(loanIds, cobBusinessDate);
+ LocalDate executingBusinessDate = getOldestCOBBusinessDate(loansToBeProcessed).plusDays(1);
+ if (!loansToBeProcessed.isEmpty()) {
+ while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
+ execute(getLoanIdsToBeProcessed(loansToBeProcessed, executingBusinessDate), jobName, executingBusinessDate);
+ executingBusinessDate = executingBusinessDate.plusDays(1);
+ }
+ }
+ }
+
+ private List getLoanIdsToBeProcessed(List loansToBeProcessed, LocalDate executingBusinessDate) {
+ List loanIdsToBeProcessed = new ArrayList<>();
+ loansToBeProcessed.forEach(loan -> {
+ if (loan.getLastClosedBusinessDate() != null) {
+ if (DateUtils.isBefore(loan.getLastClosedBusinessDate(), executingBusinessDate)) {
+ loanIdsToBeProcessed.add(loan.getId());
+ }
+ } else {
+ loanIdsToBeProcessed.add(loan.getId());
+ }
+ });
+ return loanIdsToBeProcessed;
+ }
+
+ @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
+ private void execute(List loanIds, String jobName, LocalDate businessDate) {
+ lockLoanAccounts(loanIds, businessDate);
+ Job inlineLoanCOBJob;
+ try {
+ inlineLoanCOBJob = jobLocator.getJob(jobName);
+ } catch (NoSuchJobException e) {
+ throw new JobNotFoundException(jobName, e);
+ }
+ JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(inlineLoanCOBJob)
+ .addJobParameters(new JobParameters(getJobParametersMap(loanIds, businessDate))).toJobParameters();
+ JobExecution jobExecution;
+ try {
+ jobExecution = jobLauncher.run(inlineLoanCOBJob, jobParameters);
+ } catch (Exception e) {
+ log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
+ throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
+ }
+ if (!BatchStatus.COMPLETED.equals(jobExecution.getStatus())) {
+ log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName);
+ throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName);
+ }
+ }
+
+ private LocalDate getOldestCOBBusinessDate(List loans) {
+ COBIdAndLastClosedBusinessDate oldestLoan = loans.stream().min(Comparator
+ .comparing(COBIdAndLastClosedBusinessDate::getLastClosedBusinessDate, Comparator.nullsLast(Comparator.naturalOrder())))
+ .orElse(null);
+ return oldestLoan != null && oldestLoan.getLastClosedBusinessDate() != null ? oldestLoan.getLastClosedBusinessDate()
+ : ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(1);
+ }
+
+ private List getLoansToBeProcessed(List loanIds, LocalDate cobBusinessDate) {
+ List loanIdAndLastClosedBusinessDates = new ArrayList<>();
+ List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
+ partitions.forEach(partition -> loanIdAndLastClosedBusinessDates
+ .addAll(retrieveIdService.retrieveLoanIdsBehindDateOrNull(cobBusinessDate, partition)));
+ return loanIdAndLastClosedBusinessDates;
+ }
+
+ private List getLoanAccountLocks(List loanIds, LocalDate businessDate) {
+ List loanAccountLocks = new ArrayList<>();
+ List alreadyLockedLoanIds = new ArrayList<>();
+ loanIds.forEach(loanId -> {
+ Optional loanLockOptional = loanAccountLockRepository.findById(loanId);
+ if (loanLockOptional.isPresent()) {
+ T loanAccountLock = loanLockOptional.get();
+ if (isLockOverrulable(loanAccountLock)) {
+ loanAccountLocks.add(loanAccountLock);
+ } else {
+ alreadyLockedLoanIds.add(loanId);
+ }
+ } else {
+ loanAccountLocks.add(createAccountLock(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING, businessDate));
+ }
+ });
+ if (!alreadyLockedLoanIds.isEmpty()) {
+ String message = "There is a hard lock on the loan account without any error, so it can't be overruled.";
+ String loanIdsMessage = " Locked loan IDs: " + alreadyLockedLoanIds;
+ throw new AccountLockCannotBeOverruledException(message + loanIdsMessage);
+ }
+
+ return loanAccountLocks;
+ }
+
+ private Long saveCustomJobParameter(String paramName, String paramValue) {
+ JobParameterDTO paramDTO = new JobParameterDTO(paramName, paramValue);
+ Set paramSet = Collections.singleton(paramDTO);
+ return customJobParameterRepository.save(paramSet);
+ }
+
+ private Map> getJobParametersMap(List loanIds, LocalDate businessDate) {
+ String parameterJson = gson.toJson(loanIds);
+ Long loanIdsJobParameterId = saveCustomJobParameter(COBConstant.INLINE_IDS_PARAMETER_NAME, parameterJson);
+ Long businessDateJobParameterId = saveCustomJobParameter(COBConstant.BUSINESS_DATE_PARAMETER_NAME,
+ businessDate.format(DateTimeFormatter.ISO_DATE));
+ Map> jobParameterMap = new HashMap<>();
+ jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter<>(loanIdsJobParameterId, Long.class));
+ jobParameterMap.put(COBConstant.BUSINESS_DATE_PARAMETER_NAME, new JobParameter<>(businessDateJobParameterId, Long.class));
+ return jobParameterMap;
+ }
+
+ private void lockLoanAccounts(List loanIds, LocalDate businessDate) {
+ transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+
+ @Override
+ protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
+ List loanAccountLocks = getLoanAccountLocks(loanIds, businessDate);
+ loanAccountLocks.forEach(loanAccountLock -> {
+ try {
+ loanAccountLock.setNewLockOwner(LockOwner.LOAN_INLINE_COB_PROCESSING);
+ loanAccountLockRepository.saveAndFlush(loanAccountLock);
+ } catch (Exception e) {
+ log.error("Error updating lock on loan account. Locked loan ID: {}", loanAccountLock.getLoanId(), e);
+ throw new AccountLockCannotBeOverruledException(
+ "Error updating lock on loan account. Locked loan ID: %s".formatted(loanAccountLock.getLoanId()), e);
+ }
+ });
+ }
+ });
+ }
+
+ private boolean isLockOverrulable(T loanAccountLock) {
+ if (isBypassUser()) {
+ return true;
+ } else {
+ return StringUtils.isNotBlank(loanAccountLock.getError());
+ }
+ }
+
+ private boolean isBypassUser() {
+ return context.getAuthenticatedUserIfPresent().isBypassUser();
+ }
+
+ private void validateLoanIdsListSize(List loanIds) {
+ int inlineLoanCobRequestItemLimit = fineractProperties.getApi().getBodyItemSizeLimit().getInlineLoanCob();
+ if (loanIds.size() > inlineLoanCobRequestItemLimit) {
+ String userMessage = "Size of the loan IDs list cannot be over " + inlineLoanCobRequestItemLimit;
+ throw new PlatformRequestBodyItemLimitValidationException(userMessage);
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
index 98d9def4bab..fd601a122b6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
@@ -18,243 +18,37 @@
*/
package org.apache.fineract.cob.service;
-import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
-
-import com.google.common.collect.Lists;
-import com.google.gson.Gson;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
-import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
-import org.apache.fineract.cob.exceptions.AccountLockCannotBeOverruledException;
-import org.apache.fineract.cob.loan.LoanCOBConstant;
-import org.apache.fineract.cob.loan.RetrieveLoanIdService;
-import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
-import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
-import org.apache.fineract.infrastructure.core.exception.PlatformRequestBodyItemLimitValidationException;
-import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
-import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
-import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
-import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
-import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants;
-import org.springframework.batch.core.BatchStatus;
-import org.springframework.batch.core.Job;
-import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.JobParameter;
-import org.springframework.batch.core.JobParameters;
-import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
-import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.context.annotation.Conditional;
-import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.TransactionStatus;
-import org.springframework.transaction.annotation.Propagation;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@Slf4j
-@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
-public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService {
-
- private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
- private final LoanAccountLockRepository loanAccountLockRepository;
- private final InlineLoanCOBExecutionDataParser dataParser;
- private final JobLauncher jobLauncher;
- private final JobLocator jobLocator;
- private final JobExplorer jobExplorer;
- private final TransactionTemplate transactionTemplate;
- private final CustomJobParameterRepository customJobParameterRepository;
- private final PlatformSecurityContext context;
- private final RetrieveLoanIdService retrieveLoanIdService;
- private final FineractProperties fineractProperties;
+public class InlineLoanCOBExecutorServiceImpl extends InlineCommonLockableCOBExecutorService {
- private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
-
- @Override
- @Transactional(propagation = Propagation.NOT_SUPPORTED)
- public CommandProcessingResult executeInlineJob(JsonCommand command, String jobName) throws AccountLockCannotBeOverruledException {
- List loanIds = dataParser.parseExecution(command);
- validateLoanIdsListSize(loanIds);
- execute(loanIds, jobName);
- return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
+ public InlineLoanCOBExecutorServiceImpl(LoanAccountLockRepository loanAccountLockRepository,
+ InlineLoanCOBExecutionDataParser dataParser, JobLauncher jobLauncher, JobLocator jobLocator, JobExplorer jobExplorer,
+ TransactionTemplate transactionTemplate, CustomJobParameterRepository customJobParameterRepository,
+ PlatformSecurityContext context, RetrieveLoanIdService retrieveIdService, FineractProperties fineractProperties) {
+ super(loanAccountLockRepository, dataParser, jobLauncher, jobLocator, jobExplorer, transactionTemplate,
+ customJobParameterRepository, context, retrieveIdService, fineractProperties);
}
@Override
- public void execute(List loanIds, String jobName) {
- LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
- List loansToBeProcessed = getLoansToBeProcessed(loanIds, cobBusinessDate);
- LocalDate executingBusinessDate = getOldestCOBBusinessDate(loansToBeProcessed).plusDays(1);
- if (!loansToBeProcessed.isEmpty()) {
- while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
- execute(getLoanIdsToBeProcessed(loansToBeProcessed, executingBusinessDate), jobName, executingBusinessDate);
- executingBusinessDate = executingBusinessDate.plusDays(1);
- }
- }
- }
-
- private List getLoanIdsToBeProcessed(List loansToBeProcessed, LocalDate executingBusinessDate) {
- List loanIdsToBeProcessed = new ArrayList<>();
- loansToBeProcessed.forEach(loan -> {
- if (loan.getLastClosedBusinessDate() != null) {
- if (DateUtils.isBefore(loan.getLastClosedBusinessDate(), executingBusinessDate)) {
- loanIdsToBeProcessed.add(loan.getId());
- }
- } else {
- loanIdsToBeProcessed.add(loan.getId());
- }
- });
- return loanIdsToBeProcessed;
- }
-
- @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
- private void execute(List loanIds, String jobName, LocalDate businessDate) {
- lockLoanAccounts(loanIds, businessDate);
- Job inlineLoanCOBJob;
- try {
- inlineLoanCOBJob = jobLocator.getJob(jobName);
- } catch (NoSuchJobException e) {
- throw new JobNotFoundException(jobName, e);
- }
- JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(inlineLoanCOBJob)
- .addJobParameters(new JobParameters(getJobParametersMap(loanIds, businessDate))).toJobParameters();
- JobExecution jobExecution;
- try {
- jobExecution = jobLauncher.run(inlineLoanCOBJob, jobParameters);
- } catch (Exception e) {
- log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
- throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
- }
- if (!BatchStatus.COMPLETED.equals(jobExecution.getStatus())) {
- log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName);
- throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName);
- }
- }
-
- private LocalDate getOldestCOBBusinessDate(List loans) {
- COBIdAndLastClosedBusinessDate oldestLoan = loans.stream().min(Comparator
- .comparing(COBIdAndLastClosedBusinessDate::getLastClosedBusinessDate, Comparator.nullsLast(Comparator.naturalOrder())))
- .orElse(null);
- return oldestLoan != null && oldestLoan.getLastClosedBusinessDate() != null ? oldestLoan.getLastClosedBusinessDate()
- : ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(1);
- }
-
- private List getLoansToBeProcessed(List loanIds, LocalDate cobBusinessDate) {
- List loanIdAndLastClosedBusinessDates = new ArrayList<>();
- List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
- partitions.forEach(partition -> loanIdAndLastClosedBusinessDates
- .addAll(retrieveLoanIdService.retrieveLoanIdsBehindDateOrNull(cobBusinessDate, partition)));
- return loanIdAndLastClosedBusinessDates;
- }
-
- private List getLoanAccountLocks(List loanIds, LocalDate businessDate) {
- List loanAccountLocks = new ArrayList<>();
- List alreadyLockedLoanIds = new ArrayList<>();
- loanIds.forEach(loanId -> {
- Optional loanLockOptional = loanAccountLockRepository.findById(loanId);
- if (loanLockOptional.isPresent()) {
- LoanAccountLock loanAccountLock = loanLockOptional.get();
- if (isLockOverrulable(loanAccountLock)) {
- loanAccountLocks.add(loanAccountLock);
- } else {
- alreadyLockedLoanIds.add(loanId);
- }
- } else {
- loanAccountLocks.add(new LoanAccountLock(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING, businessDate));
- }
- });
- if (!alreadyLockedLoanIds.isEmpty()) {
- String message = "There is a hard lock on the loan account without any error, so it can't be overruled.";
- String loanIdsMessage = " Locked loan IDs: " + alreadyLockedLoanIds;
- throw new AccountLockCannotBeOverruledException(message + loanIdsMessage);
- }
-
- return loanAccountLocks;
- }
-
- private Long saveCustomJobParameter(String paramName, String paramValue) {
- JobParameterDTO paramDTO = new JobParameterDTO(paramName, paramValue);
- Set paramSet = Collections.singleton(paramDTO);
- return customJobParameterRepository.save(paramSet);
- }
-
- private Map> getJobParametersMap(List loanIds, LocalDate businessDate) {
- String parameterJson = gson.toJson(loanIds);
- Long loanIdsJobParameterId = saveCustomJobParameter(LoanCOBConstant.LOAN_IDS_PARAMETER_NAME, parameterJson);
- Long businessDateJobParameterId = saveCustomJobParameter(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME,
- businessDate.format(DateTimeFormatter.ISO_DATE));
- Map> jobParameterMap = new HashMap<>();
- jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter<>(loanIdsJobParameterId, Long.class));
- jobParameterMap.put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, new JobParameter<>(businessDateJobParameterId, Long.class));
- return jobParameterMap;
- }
-
- private void lockLoanAccounts(List loanIds, LocalDate businessDate) {
- transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
- transactionTemplate.execute(new TransactionCallbackWithoutResult() {
-
- @Override
- protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
- List loanAccountLocks = getLoanAccountLocks(loanIds, businessDate);
- loanAccountLocks.forEach(loanAccountLock -> {
- try {
- loanAccountLock.setNewLockOwner(LockOwner.LOAN_INLINE_COB_PROCESSING);
- loanAccountLockRepository.saveAndFlush(loanAccountLock);
- } catch (Exception e) {
- log.error("Error updating lock on loan account. Locked loan ID: {}", loanAccountLock.getLoanId(), e);
- throw new AccountLockCannotBeOverruledException(
- "Error updating lock on loan account. Locked loan ID: %s".formatted(loanAccountLock.getLoanId()), e);
- }
- });
- }
- });
- }
-
- private boolean isLockOverrulable(LoanAccountLock loanAccountLock) {
- if (isBypassUser()) {
- return true;
- } else {
- return StringUtils.isNotBlank(loanAccountLock.getError());
- }
- }
-
- private boolean isBypassUser() {
- return context.getAuthenticatedUserIfPresent().isBypassUser();
- }
-
- private void validateLoanIdsListSize(List loanIds) {
- int inlineLoanCobRequestItemLimit = fineractProperties.getApi().getBodyItemSizeLimit().getInlineLoanCob();
- if (loanIds.size() > inlineLoanCobRequestItemLimit) {
- String userMessage = "Size of the loan IDs list cannot be over " + inlineLoanCobRequestItemLimit;
- throw new PlatformRequestBodyItemLimitValidationException(userMessage);
- }
+ public LoanAccountLock createAccountLock(Long loanId, LockOwner loanInlineCobProcessing, LocalDate businessDate) {
+ return new LoanAccountLock(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING, businessDate);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineWorkingCapitalLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineWorkingCapitalLoanCOBExecutorServiceImpl.java
new file mode 100644
index 00000000000..d7f08085387
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineWorkingCapitalLoanCOBExecutorServiceImpl.java
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.service;
+
+import java.time.LocalDate;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.domain.WorkingCapitalAccountLockRepository;
+import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanRetrieveIdService;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.springframework.batch.core.configuration.JobLocator;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Service
+@Slf4j
+@Conditional(LoanCOBEnabledCondition.class)
+public class InlineWorkingCapitalLoanCOBExecutorServiceImpl extends InlineCommonLockableCOBExecutorService {
+
+ public InlineWorkingCapitalLoanCOBExecutorServiceImpl(WorkingCapitalAccountLockRepository loanAccountLockRepository,
+ InlineLoanCOBExecutionDataParser dataParser, JobLauncher jobLauncher, JobLocator jobLocator, JobExplorer jobExplorer,
+ TransactionTemplate transactionTemplate, CustomJobParameterRepository customJobParameterRepository,
+ PlatformSecurityContext context, WorkingCapitalLoanRetrieveIdService retrieveIdService, FineractProperties fineractProperties) {
+ super(loanAccountLockRepository, dataParser, jobLauncher, jobLocator, jobExplorer, transactionTemplate,
+ customJobParameterRepository, context, retrieveIdService, fineractProperties);
+ }
+
+ @Override
+ public WorkingCapitalLoanAccountLock createAccountLock(Long loanId, LockOwner loanInlineCobProcessing, LocalDate businessDate) {
+ return new WorkingCapitalLoanAccountLock(loanId, loanInlineCobProcessing, businessDate);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
index a2bc44816c5..1e89ee8e402 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
@@ -18,16 +18,16 @@
*/
package org.apache.fineract.cob.service;
-import java.util.List;
+import org.apache.fineract.cob.domain.AccountLockRepository;
+import org.apache.fineract.cob.domain.CustomLoanAccountLockRepository;
import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.springframework.stereotype.Service;
-public interface LoanAccountLockService {
+@Service
+public class LoanAccountLockService extends AbstractAccountLockService {
- List getLockedLoanAccountByPage(int page, int limit);
-
- boolean isLoanHardLocked(Long loanId);
-
- boolean isLockOverrulable(Long loanId);
-
- void updateCobAndRemoveLocks();
+ public LoanAccountLockService(AccountLockRepository loanAccountLockRepository,
+ CustomLoanAccountLockRepository customLoanAccountLockRepository) {
+ super(loanAccountLockRepository, customLoanAccountLockRepository);
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
index fac0c1bc758..11591ef61ea 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
@@ -16,18 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cob.service;
-
-import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
-import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
-
-public interface LoanCOBCatchUpService {
- void unlockHardLockedLoans();
-
- OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan();
-
- void executeLoanCOBCatchUp();
+package org.apache.fineract.cob.service;
- IsCatchUpRunningDTO isCatchUpRunning();
-}
+public interface LoanCOBCatchUpService extends COBCatchUpService {}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
index 39b346b4a3a..247565de09b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
@@ -18,61 +18,24 @@
*/
package org.apache.fineract.cob.service;
-import java.time.LocalDate;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
-import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate;
-import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
-import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.loan.LoanCOBConstant;
-import org.apache.fineract.cob.loan.RetrieveLoanIdService;
-import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
-import org.apache.fineract.infrastructure.core.domain.FineractContext;
-import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
@Service
-@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
-public class LoanCOBCatchUpServiceImpl implements LoanCOBCatchUpService {
+public class LoanCOBCatchUpServiceImpl extends CommonCOBCatchUpService implements LoanCOBCatchUpService {
- private final AsyncLoanCOBExecutorService asyncLoanCOBExecutorService;
- private final JobExecutionRepository jobExecutionRepository;
- private final RetrieveLoanIdService retrieveLoanIdService;
- private final LoanAccountLockService accountLockService;
-
- @Override
- public void unlockHardLockedLoans() {
- accountLockService.updateCobAndRemoveLocks();
- }
-
- @Override
- public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
- List loanIdAndLastClosedBusinessDate = retrieveLoanIdService
- .retrieveLoanIdsOldestCobProcessed(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE));
- OldestCOBProcessedLoanDTO oldestCOBProcessedLoanDTO = new OldestCOBProcessedLoanDTO();
- oldestCOBProcessedLoanDTO.setLoanIds(loanIdAndLastClosedBusinessDate.stream().map(COBIdAndLastClosedBusinessDate::getId).toList());
- oldestCOBProcessedLoanDTO
- .setCobProcessedDate(loanIdAndLastClosedBusinessDate.stream().map(COBIdAndLastClosedBusinessDate::getLastClosedBusinessDate)
- .findFirst().orElse(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)));
- oldestCOBProcessedLoanDTO.setCobBusinessDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE));
- return oldestCOBProcessedLoanDTO;
- }
-
- @Override
- public void executeLoanCOBCatchUp() {
- FineractContext context = ThreadLocalContextUtil.getContext();
- asyncLoanCOBExecutorService.executeLoanCOBCatchUpAsync(context);
+ public LoanCOBCatchUpServiceImpl(AsyncLoanCOBExecutorService asyncLoanCOBExecutorService, JobExecutionRepository jobExecutionRepository,
+ RetrieveLoanIdService retrieveIdService, LoanAccountLockService accountLockService) {
+ super(asyncLoanCOBExecutorService, jobExecutionRepository, retrieveIdService, accountLockService);
}
@Override
- public IsCatchUpRunningDTO isCatchUpRunning() {
- LocalDate runningCatchUpBusinessDate = jobExecutionRepository.getBusinessDateOfRunningJobByExecutionParameter(
- LoanCOBConstant.JOB_NAME, LoanCOBConstant.COB_CUSTOM_JOB_PARAMETER_KEY, LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "true",
- LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME);
- return new IsCatchUpRunningDTO(runningCatchUpBusinessDate != null, runningCatchUpBusinessDate);
+ public String getJobName() {
+ return LoanCOBConstant.JOB_NAME;
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpService.java
new file mode 100644
index 00000000000..828648243ad
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpService.java
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.cob.service;
+
+public interface WorkingCapitalLoanCOBCatchUpService extends COBCatchUpService {}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpServiceImpl.java
new file mode 100644
index 00000000000..f0188b2a85a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/WorkingCapitalLoanCOBCatchUpServiceImpl.java
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.cob.service;
+
+import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
+import org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock;
+import org.apache.fineract.cob.workingcapitalloan.WorkingCapitalLoanRetrieveIdService;
+import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository;
+import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Service;
+
+@Service
+@Conditional(LoanCOBEnabledCondition.class)
+public class WorkingCapitalLoanCOBCatchUpServiceImpl extends CommonCOBCatchUpService
+ implements WorkingCapitalLoanCOBCatchUpService {
+
+ public WorkingCapitalLoanCOBCatchUpServiceImpl(AsyncWorkingCapitalLoanCOBExecutorService asyncLoanCOBExecutorService,
+ JobExecutionRepository jobExecutionRepository, WorkingCapitalLoanRetrieveIdService retrieveIdService,
+ AccountLockService accountLockService) {
+ super(asyncLoanCOBExecutorService, jobExecutionRepository, retrieveIdService, accountLockService);
+ }
+
+ @Override
+ public String getJobName() {
+ return JobName.WORKING_CAPITAL_LOAN_COB_JOB.name();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/accountnumberformat/service/AccountNumberFormatReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/accountnumberformat/service/AccountNumberFormatReadPlatformServiceImpl.java
index 308ee6cee7e..03bdae55d97 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/accountnumberformat/service/AccountNumberFormatReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/accountnumberformat/service/AccountNumberFormatReadPlatformServiceImpl.java
@@ -54,20 +54,14 @@ public class AccountNumberFormatReadPlatformServiceImpl implements AccountNumber
private static final class AccountNumberFormatMapper implements RowMapper {
- private final String schema;
+ private static final String ACCOUNT_NUMBER_FORMAT_SCHEMA = """
+ anf.id as id, anf.account_type_enum as accountTypeEnum, anf.prefix_type_enum as prefixTypeEnum, anf.prefix_character as prefixCharacter
+ from c_account_number_format anf\s""";
- AccountNumberFormatMapper() {
- final StringBuilder builder = new StringBuilder(400);
-
- builder.append(
- " anf.id as id, anf.account_type_enum as accountTypeEnum, anf.prefix_type_enum as prefixTypeEnum, anf.prefix_character as prefixCharacter");
- builder.append(" from c_account_number_format anf ");
-
- this.schema = builder.toString();
- }
+ AccountNumberFormatMapper() {}
public String schema() {
- return this.schema;
+ return ACCOUNT_NUMBER_FORMAT_SCHEMA;
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
index a93f674073c..c7226e1c771 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
@@ -141,6 +141,7 @@ private TemplatePopulateImportConstants() {
// Guarantor Types
public static final String GUARANTOR_INTERNAL = "Internal";
public static final String GUARANTOR_EXTERNAL = "External";
+ public static final String GUARANTOR_GROUP = "Group";
// Loan Account/Loan repayment Client External Id
public static final Boolean CONTAINS_CLIENT_EXTERNAL_ID = true;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
index e8b8c8c8b62..c51be9c6382 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
@@ -94,6 +94,8 @@ private GuarantorData readGuarantor(final Workbook workbook, final Row row, Long
guarantorTypeId = 1;
} else if (guarantorType.equalsIgnoreCase(TemplatePopulateImportConstants.GUARANTOR_EXTERNAL)) {
guarantorTypeId = 3;
+ } else if (guarantorType.equalsIgnoreCase(TemplatePopulateImportConstants.GUARANTOR_GROUP)) {
+ guarantorTypeId = 4;
}
}
String clientName = ImportHandlerUtils.readAsString(GuarantorConstants.ENTITY_ID_COL, row);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
index 984bee37d1c..ab9326b68a1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
@@ -209,8 +209,9 @@ private void setRules(Sheet worksheet) {
"INDIRECT(CONCATENATE(\"Account_\",SUBSTITUTE(SUBSTITUTE(SUBSTITUTE($B1,\" \",\"_\"),\"(\",\"_\"),\")\",\"_\")))");
DataValidationConstraint savingsaccountNumberConstraint = validationHelper.createFormulaListConstraint(
"INDIRECT(CONCATENATE(\"SavingsAccount_\",SUBSTITUTE(SUBSTITUTE(SUBSTITUTE($G1,\" \",\"_\"),\"(\",\"_\"),\")\",\"_\")))");
- DataValidationConstraint guranterTypeConstraint = validationHelper.createExplicitListConstraint(
- new String[] { TemplatePopulateImportConstants.GUARANTOR_INTERNAL, TemplatePopulateImportConstants.GUARANTOR_EXTERNAL });
+ DataValidationConstraint guranterTypeConstraint = validationHelper
+ .createExplicitListConstraint(new String[] { TemplatePopulateImportConstants.GUARANTOR_INTERNAL,
+ TemplatePopulateImportConstants.GUARANTOR_EXTERNAL, TemplatePopulateImportConstants.GUARANTOR_GROUP });
DataValidationConstraint guarantorRelationshipConstraint = validationHelper.createFormulaListConstraint("GuarantorRelationship");
DataValidationConstraint entityofficeNameConstraint = validationHelper.createFormulaListConstraint("Office");
DataValidationConstraint entityclientNameConstraint = validationHelper
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
index 78cf8d35cd0..221115c176d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
@@ -227,13 +227,14 @@ public ImportData getImport(Long id) {
private static final class ImportMapper implements RowMapper {
+ private static final String IMPORT_SCHEMA = """
+ i.id as id, i.document_id as documentId, d.name as name, i.import_time as importTime, i.end_time as endTime,
+ i.completed as completed, i.total_records as totalRecords, i.success_count as successCount,
+ i.failure_count as failureCount, i.createdby_id as createdBy
+ from m_import_document i inner join m_document d on i.document_id=d.id where i.entity_type= ?\s""";
+
public String schema() {
- final StringBuilder sql = new StringBuilder();
- sql.append("i.id as id, i.document_id as documentId, d.name as name, i.import_time as importTime, i.end_time as endTime, ")
- .append("i.completed as completed, i.total_records as totalRecords, i.success_count as successCount, ")
- .append("i.failure_count as failureCount, i.createdby_id as createdBy ")
- .append("from m_import_document i inner join m_document d on i.document_id=d.id ").append("where i.entity_type= ? ");
- return sql.toString();
+ return IMPORT_SCHEMA;
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java
index ce0aee75c67..8067ea08acd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailCampaignReadPlatformServiceImpl.java
@@ -63,45 +63,41 @@ public EmailCampaignReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate) {
private static final class EmailCampaignMapper implements RowMapper {
- final String schema;
-
- private EmailCampaignMapper() {
- final StringBuilder sql = new StringBuilder(400);
- sql.append("ec.id as id, ");
- sql.append("ec.campaign_name as campaignName, ");
- sql.append("ec.campaign_type as campaignType, ");
- sql.append("ec.business_rule_id as businessRuleId, ");
- sql.append("ec.email_subject as emailSubject, ");
- sql.append("ec.email_message as emailMessage, ");
- sql.append("ec.email_attachment_file_format as emailAttachmentFileFormat, ");
- sql.append("sr.id as stretchyReportId, ");
- sql.append("sr.report_name as reportName, sr.report_type as reportType, sr.report_subtype as reportSubType, ");
- sql.append("sr.report_category as reportCategory, sr.report_sql as reportSql, sr.description as reportDescription, ");
- sql.append("sr.core_report as coreReport, sr.use_report as useReport, ");
- sql.append("ec.stretchy_report_param_map as stretchyReportParamMap, ");
- sql.append("ec.param_value as paramValue, ");
- sql.append("ec.status_enum as statusEnum, ");
- sql.append("ec.recurrence as recurrence, ");
- sql.append("ec.recurrence_start_date as recurrenceStartDate, ");
- sql.append("ec.next_trigger_date as nextTriggerDate, ");
- sql.append("ec.last_trigger_date as lastTriggerDate, ");
- sql.append("ec.submittedon_date as submittedOnDate, ");
- sql.append("sbu.username as submittedByUsername, ");
- sql.append("ec.closedon_date as closedOnDate, ");
- sql.append("clu.username as closedByUsername, ");
- sql.append("acu.username as activatedByUsername, ");
- sql.append("ec.approvedon_date as activatedOnDate ");
- sql.append("from scheduled_email_campaign ec ");
- sql.append("left join m_appuser sbu on sbu.id = ec.submittedon_userid ");
- sql.append("left join m_appuser acu on acu.id = ec.approvedon_userid ");
- sql.append("left join m_appuser clu on clu.id = ec.closedon_userid ");
- sql.append("left join stretchy_report sr on ec.stretchy_report_id = sr.id");
-
- this.schema = sql.toString();
- }
+ private static final String EMAIL_CAMPAIGN_SCHEMA = """
+ ec.id as id,
+ ec.campaign_name as campaignName,
+ ec.campaign_type as campaignType,
+ ec.business_rule_id as businessRuleId,
+ ec.email_subject as emailSubject,
+ ec.email_message as emailMessage,
+ ec.email_attachment_file_format as emailAttachmentFileFormat,
+ sr.id as stretchyReportId,
+ sr.report_name as reportName, sr.report_type as reportType, sr.report_subtype as reportSubType,
+ sr.report_category as reportCategory, sr.report_sql as reportSql, sr.description as reportDescription,
+ sr.core_report as coreReport, sr.use_report as useReport,
+ ec.stretchy_report_param_map as stretchyReportParamMap,
+ ec.param_value as paramValue,
+ ec.status_enum as statusEnum,
+ ec.recurrence as recurrence,
+ ec.recurrence_start_date as recurrenceStartDate,
+ ec.next_trigger_date as nextTriggerDate,
+ ec.last_trigger_date as lastTriggerDate,
+ ec.submittedon_date as submittedOnDate,
+ sbu.username as submittedByUsername,
+ ec.closedon_date as closedOnDate,
+ clu.username as closedByUsername,
+ acu.username as activatedByUsername,
+ ec.approvedon_date as activatedOnDate
+ from scheduled_email_campaign ec
+ left join m_appuser sbu on sbu.id = ec.submittedon_userid
+ left join m_appuser acu on acu.id = ec.approvedon_userid
+ left join m_appuser clu on clu.id = ec.closedon_userid
+ left join stretchy_report sr on ec.stretchy_report_id = sr.id\s""";
+
+ private EmailCampaignMapper() {}
public String schema() {
- return this.schema;
+ return EMAIL_CAMPAIGN_SCHEMA;
}
@Override
@@ -144,28 +140,24 @@ public EmailCampaignData mapRow(ResultSet rs, int rowNum) throws SQLException {
private static final class BusinessRuleMapper implements ResultSetExtractor