From ac45551a588ae8d502027733badd1d10ab1c0b9a Mon Sep 17 00:00:00 2001 From: sayhaed Date: Wed, 18 Jun 2025 10:18:17 -0600 Subject: [PATCH 1/2] FINERACT-2312: Savings Accounts Improvements --- .github/workflows/build-cucumber.yml | 111 ++++++ .github/workflows/build-docker-postgresql.yml | 31 -- ...ld-docker-mariadb.yml => build-docker.yml} | 32 +- .github/workflows/build-e2e-tests.yml | 165 +++++++++ .../build-embeddable-progressive-loan-jar.yml | 47 --- .github/workflows/build-mariadb.yml | 166 ++++++--- .github/workflows/build-mysql.yml | 168 ++++++--- .github/workflows/build-postgresql.yml | 168 ++++++--- .github/workflows/build-tests.yml | 41 --- .github/workflows/smoke-kafka.yml | 37 -- ...smoke-activemq.yml => smoke-messaging.yml} | 32 +- build.gradle | 17 +- ...riodicAccrualForSavingsCommandHandler.java | 42 +++ ...AccountMappingFromApiJsonDeserializer.java | 4 +- .../ProductToGLAccountMappingHelper.java | 1 + ...AccountMappingReadPlatformServiceImpl.java | 2 + ...avingsProductToGLAccountMappingHelper.java | 6 +- .../service/ChargeReadPlatformService.java | 3 + .../common/AccountingConstants.java | 7 +- .../common/AccountingValidations.java | 45 +++ .../handler/NewCommandSourceHandler.java | 3 +- .../service/CommandSourceService.java | 3 +- .../service/CommandWrapperBuilder.java | 18 + .../cache/api/CacheApiResource.java | 65 ++-- .../CacheSwitchCommand.java} | 16 +- .../command/UpdateCacheCommandHandler.java | 89 ----- .../infrastructure/cache/data/CacheData.java | 11 +- .../CacheSwitchRequest.java} | 55 ++- .../cache/data/CacheSwitchResponse.java} | 17 +- .../handler/CacheSwitchCommandHandler.java | 48 +++ .../RuntimeDelegatingCacheManager.java | 4 +- ...ternalEventConfigurationUpdateHandler.java | 8 +- .../infrastructure/jobs/service/JobName.java | 2 +- .../monetary/api/CurrenciesApiResource.java | 80 ++++ .../command/CurrencyUpdateCommand.java | 28 ++ ...ta.java => CurrencyConfigurationData.java} | 26 +- .../monetary/data/CurrencyData.java | 42 +-- ...eyData.java => CurrencyUpdateRequest.java} | 51 ++- .../monetary/data/CurrencyUpdateResponse.java | 61 ++++ .../ApplicationCurrencyRepositoryWrapper.java | 2 +- .../monetary/domain/MonetaryCurrency.java | 15 +- .../handler/CurrencyUpdateCommandHandler.java | 43 +++ .../monetary/mapper/CurrencyMapper.java | 37 ++ ...urrencyCommandFromApiJsonDeserializer.java | 79 ---- .../{gson => }/MoneyDeserializer.java | 3 +- .../{gson => }/MoneySerializer.java | 3 +- .../CurrencyReadPlatformServiceImpl.java | 11 +- .../service/CurrencyWritePlatformService.java | 7 +- ...ganisationCurrencyReadPlatformService.java | 4 +- ...sationCurrencyReadPlatformServiceImpl.java | 13 +- .../office/mapper/OfficeDataMapper.java | 7 +- .../savings/DepositsApiConstants.java | 18 +- .../savings/SavingsApiConstants.java | 3 + .../savings/data/SavingsAccountData.java | 62 ++++ .../data/SavingsAccountTransactionData.java | 84 ++++- .../savings/data/SavingsProductData.java | 35 +- .../interest/AnnualCompoundingPeriod.java | 2 +- .../interest/BiAnnualCompoundingPeriod.java | 2 +- .../interest/CompoundInterestHelper.java | 9 +- .../interest/CompoundInterestValues.java | 2 +- .../domain/interest/CompoundingPeriod.java | 2 +- .../interest/DailyCompoundingPeriod.java | 2 +- .../domain/interest/EndOfDayBalance.java | 52 ++- .../interest/MonthlyCompoundingPeriod.java | 18 +- .../domain/interest/PostingPeriod.java | 76 +++- .../interest/QuarterlyCompoundingPeriod.java | 2 +- ...untTransactionDetailsForPostingPeriod.java | 13 +- .../data/loanproduct/DefaultLoanProduct.java | 1 + .../factory/SavingsProductRequestFactory.java | 6 +- .../global/CurrencyGlobalInitializerStep.java | 11 +- .../LoanProductGlobalInitializerStep.java | 25 +- .../fineract/test/service/JobService.java | 2 +- .../test/stepdef/common/BatchApiStepDef.java | 6 +- .../test/stepdef/loan/LoanStepDef.java | 12 +- .../fineract/test/support/TestContextKey.java | 1 + fineract-e2e-tests-runner/build.gradle | 24 +- .../features/LoanAccrualActivity.feature | 226 +++++++++++- .../resources/features/LoanMigration.feature | 90 +++++ ...ransferOutstandingInterestCalculation.java | 4 +- .../InvestorBusinessEventSerializer.java | 2 +- .../LoanProductToGLAccountMappingHelper.java | 13 +- .../DelinquencyReadPlatformServiceImpl.java | 11 +- .../starter/DelinquencyConfiguration.java | 6 +- .../loanaccount/api/LoanApiConstants.java | 2 + .../data/LoanTransactionEnumData.java | 4 + .../portfolio/loanaccount/domain/Loan.java | 100 +---- .../loanaccount/domain/LoanTransaction.java | 13 +- .../domain/LoanTransactionRepository.java | 102 +++++- .../domain/LoanTransactionType.java | 13 + ...rgeRepaymentScheduleProcessingWrapper.java | 1 - ...RepaymentScheduleTransactionProcessor.java | 25 +- .../LoanTransactionProcessingException.java | 36 ++ ...stractCumulativeLoanScheduleGenerator.java | 43 +-- ...gBalanceInterestLoanScheduleGenerator.java | 7 +- ...tiveFlatInterestLoanScheduleGenerator.java | 7 +- .../mapper/LoanTransactionMapper.java | 6 +- .../serialization/LoanChargeValidator.java | 20 +- .../service/LoanBalanceService.java | 63 +++- .../loanproduct/service/LoanEnumerations.java | 4 + ...edPaymentScheduleTransactionProcessor.java | 22 +- .../handler/AddBuyDownFeeCommandHandler.java | 53 +++ .../service/BuyDownFeePlatformService.java | 29 ++ .../BuyDownFeeWritePlatformServiceImpl.java | 123 +++++++ ...talizedIncomeWritePlatformServiceImpl.java | 2 +- .../InternalProgressiveLoanApiResource.java | 7 +- ...estScheduleModelParserServiceGsonImpl.java | 4 +- .../ProgressiveLoanTransactionValidator.java | 2 + ...ogressiveLoanTransactionValidatorImpl.java | 58 +++ ...ymentScheduleTransactionProcessorTest.java | 4 +- fineract-provider/build.gradle | 38 -- .../api/JournalEntriesApiResourceSwagger.java | 21 +- .../journalentry/data/ChargePaymentDTO.java | 3 + .../data/SavingsTransactionDTO.java | 1 + .../AccountingProcessorForSavings.java | 2 +- .../service/AccountingProcessorHelper.java | 18 +- ...ccrualBasedAccountingProcessorForLoan.java | 24 ++ ...ualBasedAccountingProcessorForSavings.java | 66 +++- ...ashBasedAccountingProcessorForSavings.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 5 +- ...ccountMappingWritePlatformServiceImpl.java | 5 + .../core/config/SecurityConfig.java | 10 + .../LoanRepaymentBusinessEventSerializer.java | 2 +- .../monetary/api/CurrenciesApiResource.java | 88 ----- .../api/CurrenciesApiResourceSwagger.java | 63 ---- ...WritePlatformServiceJpaRepositoryImpl.java | 38 +- .../OrganisationMonetaryConfiguration.java | 18 +- .../domain/AccountTransferRepository.java | 1 + .../AdjustAccountTransferCommandHandler.java} | 20 +- .../AccountTransfersWritePlatformService.java | 2 + ...ountTransfersWritePlatformServiceImpl.java | 34 ++ .../ChargeReadPlatformServiceImpl.java | 15 + .../portfolio/group/data/GroupSummary.java | 55 --- .../api/LoanTransactionsApiResource.java | 4 + .../loanaccount/data/LoanPointInTimeData.java | 3 +- .../domain/LoanAccountDomainServiceJpa.java | 3 +- ...nAccrualActivityProcessingServiceImpl.java | 7 +- .../LoanAccrualsProcessingServiceImpl.java | 4 +- .../service/LoanAssemblerImpl.java | 10 +- .../service/LoanDisbursementService.java | 3 +- .../LoanProductRelatedDetailUpdateUtil.java | 2 +- .../LoanTransactionProcessingServiceImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 15 +- .../ProgressiveLoanSummaryDataProvider.java | 7 +- .../ReprocessLoanTransactionsServiceImpl.java | 21 +- .../LoanContractTerminationServiceImpl.java | 2 +- .../starter/LoanAccountAutoStarter.java | 6 +- .../starter/LoanAccountConfiguration.java | 20 +- .../api/LoanProductsApiResource.java | 2 +- .../LoanProductDataValidator.java | 3 +- .../api/SavingsAccountsApiResource.java | 6 +- .../api/SavingsProductsApiResource.java | 3 +- .../SavingsProductsApiResourceSwagger.java | 28 +- .../domain/DepositAccountAssembler.java | 6 + .../DepositAccountDomainServiceJpa.java | 44 +-- .../savings/domain/FixedDepositAccount.java | 2 +- .../domain/RecurringDepositAccount.java | 2 +- .../SavingsAccountDomainServiceJpa.java | 344 ++++++++++++++++-- ...actionsToSavingsAccountCommandHandler.java | 44 +++ ...AddAccrualTransactionForSavingsConfig.java | 60 +++ ...ddAccrualTransactionForSavingsTasklet.java | 50 +++ ...WritePlatformServiceJpaRepositoryImpl.java | 30 +- ...WritePlatformServiceJpaRepositoryImpl.java | 11 + ...ingsAccountInterestPostingServiceImpl.java | 171 ++++++++- ...SavingsAccountReadPlatformServiceImpl.java | 156 +++++++- .../SavingsAccrualWritePlatformService.java | 40 ++ ...avingsAccrualWritePlatformServiceImpl.java | 278 ++++++++++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 15 +- .../savings/starter/SavingsConfiguration.java | 141 +++---- .../src/main/resources/application.properties | 2 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0186_add_buy_down_fee_to_loan_product.xml | 44 +++ ...nchronousCommandProcessingServiceTest.java | 17 +- ...ntDelinquencyRangeEventSerializerTest.java | 10 +- .../loanaccount/domain/LoanBuilder.java | 11 +- .../savings/data/SavingsAccrualData.java | 52 +++ .../data/SavingsProductDataValidator.java | 10 +- .../DepositAccountInterestRateChart.java | 8 + .../savings/domain/DepositTermDetail.java | 5 + .../savings/domain/FixedDepositProduct.java | 2 +- .../savings/domain/SavingsAccount.java | 266 ++++++++++++-- .../domain/SavingsAccountRepository.java | 4 + .../SavingsAccountRepositoryWrapper.java | 11 + .../domain/SavingsAccountTransaction.java | 81 +++-- .../savings/domain/SavingsEvent.java | 2 + .../savings/domain/SavingsProduct.java | 57 ++- .../domain/SavingsProductAssembler.java | 67 +--- .../domain/SavingsProductBaseAssembler.java | 86 +++++ .../SavingsAccountBlockedException.java | 5 + ...SavingsAccountCreditsBlockedException.java | 5 + .../SavingsAccountDebitsBlockedException.java | 4 + ...ositsWithActiveTransferFundsException.java | 35 ++ ...ferTransactionsAlreadyUndoneException.java | 34 ++ .../service/SavingsAccountDomainService.java | 14 +- .../SavingsAccountReadPlatformService.java | 5 + .../SavingsSchedularInterestPoster.java | 72 ++-- .../fineract/validation/messages.properties | 9 + .../validation/messages_en.properties | 9 + .../AccountingScenarioIntegrationTest.java | 9 + .../AccrualsOnLoanClosureTest.java | 3 + .../ActuatorIntegrationTest.java | 10 + .../integrationtests/CurrenciesTest.java | 54 ++- ...linquencyAndChargebackIntegrationTest.java | 10 +- .../DelinquencyBucketsIntegrationTest.java | 110 +++--- .../integrationtests/LoanBuyDownFeeTest.java | 264 ++++++++++++++ .../LoanCOBCreateAccrualsTest.java | 43 +++ ...paymentWithDownPaymentIntegrationTest.java | 2 + .../common/CurrenciesHelper.java | 52 +-- .../common/GlobalConfigurationHelper.java | 2 +- .../integrationtests/common/OfficeHelper.java | 2 +- .../common/SchedulerJobHelper.java | 2 +- .../common/loans/LoanProductTestBuilder.java | 19 + .../common/loans/LoanTransactionHelper.java | 4 + .../SearchExternalAssetOwnerTransferTest.java | 6 +- .../base/BaseSavingsIntegrationTest.java | 7 +- scripts/split-features.sh | 110 ++++++ scripts/split-tests.sh | 52 +++ 217 files changed, 5422 insertions(+), 1893 deletions(-) create mode 100644 .github/workflows/build-cucumber.yml delete mode 100644 .github/workflows/build-docker-postgresql.yml rename .github/workflows/{build-docker-mariadb.yml => build-docker.yml} (67%) create mode 100644 .github/workflows/build-e2e-tests.yml delete mode 100644 .github/workflows/build-embeddable-progressive-loan-jar.yml delete mode 100644 .github/workflows/build-tests.yml delete mode 100644 .github/workflows/smoke-kafka.yml rename .github/workflows/{smoke-activemq.yml => smoke-messaging.yml} (79%) create mode 100644 fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java rename fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/{data/request/CacheRequest.java => command/CacheSwitchCommand.java} (69%) delete mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java rename fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/{api/CacheApiResourceSwagger.java => data/CacheSwitchRequest.java} (55%) rename fineract-core/src/main/java/org/apache/fineract/{organisation/monetary/data/request/CurrencyRequest.java => infrastructure/cache/data/CacheSwitchResponse.java} (71%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/command/CurrencyUpdateCommand.java rename fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/{ApplicationCurrencyConfigurationData.java => CurrencyConfigurationData.java} (68%) rename fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/{MoneyData.java => CurrencyUpdateRequest.java} (56%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateResponse.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/mapper/CurrencyMapper.java delete mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/CurrencyCommandFromApiJsonDeserializer.java rename fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/{gson => }/MoneyDeserializer.java (94%) rename fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/{gson => }/MoneySerializer.java (93%) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionProcessingException.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddBuyDownFeeCommandHandler.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java rename fineract-provider/src/main/java/org/apache/fineract/{organisation/monetary/handler/UpdateCurrencyCommandHandler.java => portfolio/account/handler/AdjustAccountTransferCommandHandler.java} (66%) delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupSummary.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0186_add_buy_down_fee_to_loan_product.xml create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java create mode 100755 scripts/split-features.sh create mode 100755 scripts/split-tests.sh diff --git a/.github/workflows/build-cucumber.yml b/.github/workflows/build-cucumber.yml new file mode 100644 index 00000000000..ce4a18fd9ac --- /dev/null +++ b/.github/workflows/build-cucumber.yml @@ -0,0 +1,111 @@ +name: Fineract Build & Cucumber tests (without E2E tests) + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + include: + - task: build-core + job_type: main + - task: cucumber + job_type: main + - task: build-progressive-loan + job_type: progressive-loan + + env: + TZ: Asia/Kolkata + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Setup Gradle and Validate Wrapper + uses: gradle/actions/setup-gradle@v4.4.1 + with: + validate-wrappers: true + + - name: Run Gradle Task + if: matrix.job_type == 'main' + run: | + case "${{ matrix.task }}" in + build-core) + ./gradlew --no-daemon build -x test -x cucumber -x doc + ;; + cucumber) + ./gradlew --no-daemon cucumber -x :fineract-e2e-tests-runner:cucumber -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + esac + + - name: Build and Test Progressive Loan + if: matrix.job_type == 'progressive-loan' + run: | + # Build the JAR + ./gradlew --no-daemon --console=plain :fineract-progressive-loan-embeddable-schedule-generator:shadowJar + + # Store the JAR filename in an environment variable + EMBEDDABLE_JAR_FILE=$(ls fineract-progressive-loan-embeddable-schedule-generator/build/libs/*-all.jar | head -n 1) + echo "EMBEDDABLE_JAR_FILE=$EMBEDDABLE_JAR_FILE" >> $GITHUB_ENV + echo "JAR file: $EMBEDDABLE_JAR_FILE" + + # Run unit tests + ./gradlew --no-daemon --console=plain :fineract-progressive-loan-embeddable-schedule-generator:test + + # Build and run sample application + mkdir -p sample-app + javac -cp "$EMBEDDABLE_JAR_FILE" -d sample-app fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java + java -cp "$EMBEDDABLE_JAR_FILE:sample-app" Main + java -cp "$EMBEDDABLE_JAR_FILE:sample-app" Main 25 + + - name: Archive test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.task }} + path: | + **/build/reports/ + **/fineract-progressive-loan-embeddable-schedule-generator/build/reports/ + if-no-files-found: ignore + retention-days: 5 + + - name: Archive Progressive Loan JAR + if: matrix.job_type == 'progressive-loan' && always() + uses: actions/upload-artifact@v4 + with: + name: progressive-loan-jar + path: ${{ env.EMBEDDABLE_JAR_FILE }} + retention-days: 5 + if-no-files-found: ignore + + - name: Archive server logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: server-logs-${{ matrix.task }} + path: '**/build/cargo/' + retention-days: 5 diff --git a/.github/workflows/build-docker-postgresql.yml b/.github/workflows/build-docker-postgresql.yml deleted file mode 100644 index 9da361e8a09..00000000000 --- a/.github/workflows/build-docker-postgresql.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Fineract Docker build - PostgreSQL -on: [push, pull_request] -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-24.04 - env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - IMAGE_NAME: fineract - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 - with: - java-version: '21' - distribution: 'zulu' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - - name: Build the image - run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber - - name: Start the Standalone Stack - run: docker compose -f docker-compose-postgresql.yml up -d - - name: Check the stack - run: docker ps - - name: Check health - run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/health - - name: Check info - run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/info | wc --chars) > 100 )) diff --git a/.github/workflows/build-docker-mariadb.yml b/.github/workflows/build-docker.yml similarity index 67% rename from .github/workflows/build-docker-mariadb.yml rename to .github/workflows/build-docker.yml index 9c633a62e59..641cf598ab7 100644 --- a/.github/workflows/build-docker-mariadb.yml +++ b/.github/workflows/build-docker.yml @@ -1,31 +1,53 @@ -name: Fineract Docker build - MariaDB +name: Fineract Docker Builds + on: [push, pull_request] + permissions: contents: read + jobs: build: runs-on: ubuntu-24.04 + strategy: + matrix: + db_type: [mariadb, postgresql] + include: + - db_type: mariadb + compose_file: docker-compose.yml + check_worker_health: true + - db_type: postgresql + compose_file: docker-compose-postgresql.yml + check_worker_health: false + env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} IMAGE_NAME: fineract + steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 + - name: Set up JDK 21 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: java-version: '21' distribution: 'zulu' + - name: Setup Gradle uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber - - name: Start the stack - run: docker compose -f docker-compose.yml up -d + + - name: Start the ${{ matrix.db_type }} stack + run: docker compose -f ${{ matrix.compose_file }} up -d + - name: Check the stack run: docker ps - - name: Check health + + - name: Check health Manager run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/health - - name: Check info + + - name: Check info Manager run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/info | wc --chars) > 100 )) diff --git a/.github/workflows/build-e2e-tests.yml b/.github/workflows/build-e2e-tests.yml new file mode 100644 index 00000000000..8648f48b296 --- /dev/null +++ b/.github/workflows/build-e2e-tests.yml @@ -0,0 +1,165 @@ +name: Fineract E2E Tests + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + name: E2E Tests (Shard ${{ matrix.shard_index }} of ${{ matrix.total_shards }}) + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + # Define the number of shards (1-based indexing) + shard_index: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + total_shards: [10] + + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + IMAGE_NAME: fineract + BASE_URL: https://localhost:8443 + TEST_USERNAME: mifos + TEST_PASSWORD: password + TEST_STRONG_PASSWORD: A1b2c3d4e5f$ + TEST_TENANT_ID: default + INITIALIZATION_ENABLED: true + EVENT_VERIFICATION_ENABLED: true + ACTIVEMQ_BROKER_URL: tcp://localhost:61616 + ACTIVEMQ_TOPIC_NAME: events + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4.4.1 + + - name: Make scripts executable + run: chmod +x scripts/split-features.sh + + - name: Split feature files into shards + id: split-features + run: | + ./scripts/split-features.sh ${{ matrix.total_shards }} ${{ matrix.shard_index }} + echo "Shard ${{ matrix.shard_index }} feature files:" + cat feature_shard_${{ matrix.shard_index }}.txt + + - name: Build the image + run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber + + - name: Start the Fineract stack + run: docker compose -f docker-compose-postgresql-test-activemq.yml up -d + + - name: Check the stack + run: docker ps + + - name: Wait for Manager to be ready + run: | + # Wait for the container to be running + echo "Waiting for Manager container to be ready..." + timeout 300 bash -c 'until docker ps --filter "health=healthy" --filter "name=fineract" --format "{{.Status}}" | grep -q "healthy"; do + if docker ps --filter "name=fineract" --format "{{.Status}}" | grep -q "unhealthy"; then + echo "Container is unhealthy. Stopping..." + docker ps -a + docker logs $(docker ps -q --filter name=fineract) || true + exit 1 + fi + echo "Waiting for Manager to be ready..." + sleep 5 + done' + + # Check the health endpoint + echo "Checking Manager health endpoint..." + curl -f -k --retry 30 --retry-all-errors --connect-timeout 10 --retry-delay 10 \ + https://localhost:8443/fineract-provider/actuator/health + + - name: Execute tests for shard ${{ matrix.shard_index }} + run: | + # Read feature files from the shard file + FEATURES=$(tr '\n' ',' < "feature_shard_${{ matrix.shard_index }}.txt" | sed 's/,$//') + + if [ -z "$FEATURES" ]; then + echo "No features to test in this shard. Skipping..." + exit 0 + fi + + # Create a directory for individual allure results + mkdir -p "allure-results-shard-${{ matrix.shard_index }}" + + # Read each feature file path and run tests one by one + while IFS= read -r feature_file || [ -n "$feature_file" ]; do + # Skip empty lines + [ -z "$feature_file" ] && continue + + # Create a safe filename for the results + safe_name=$(echo "$feature_file" | tr '/' '-' | tr ' ' '_') + + echo "Testing feature: $feature_file" + + # Run tests with individual allure results directory + ./gradlew --no-daemon --console=plain \ + :fineract-e2e-tests-runner:cucumber \ + -Pcucumber.features="$feature_file" \ + -Dallure.results.directory="allure-results-shard-${{ matrix.shard_index }}/$safe_name" \ + allureReport || echo "Test failed for $feature_file, continuing with next feature..." + + # Copy the results to a merged directory + if [ -d "allure-results-shard-${{ matrix.shard_index }}/$safe_name" ]; then + cp -r "allure-results-shard-${{ matrix.shard_index }}/$safe_name/." "allure-results-merged/" || true + fi + done < "feature_shard_${{ matrix.shard_index }}.txt" + + # Generate individual report for this shard + if [ -d "allure-results-merged" ]; then + mkdir -p "allure-report-shard-${{ matrix.shard_index }}" + allure generate "allure-results-merged" --clean -o "allure-report-shard-${{ matrix.shard_index }}" || echo "Failed to generate Allure report for shard ${{ matrix.shard_index }}" + fi + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-results-shard-${{ matrix.shard_index }} + path: | + allure-results-shard-${{ matrix.shard_index }} + allure-results-merged + **/build/allure-results + **/build/reports/tests/test + **/build/test-results/test + retention-days: 5 + + - name: Upload Allure Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-report-shard-${{ matrix.shard_index }} + path: allure-report-shard-${{ matrix.shard_index }} + retention-days: 5 + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-shard-${{ matrix.shard_index }} + path: | + **/build/reports/tests/ + **/logs/ + **/out/ + retention-days: 5 + + - name: Clean up + if: always() + run: | + docker compose -f docker-compose-postgresql-test-activemq.yml down -v + docker system prune -f diff --git a/.github/workflows/build-embeddable-progressive-loan-jar.yml b/.github/workflows/build-embeddable-progressive-loan-jar.yml deleted file mode 100644 index e2912595549..00000000000 --- a/.github/workflows/build-embeddable-progressive-loan-jar.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Fineract Build Progressive Loan Embeddable Jar & Test with a Sample Application -on: [push, pull_request] -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 - with: - java-version: '21' - distribution: 'zulu' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - - name: Build Embeddable Jar - run: ./gradlew --no-daemon --console=plain :fineract-progressive-loan-embeddable-schedule-generator:shadowJar - - name: Pick up the JAR filename - run: | - EMBEDDABLE_JAR_FILE=(`ls fineract-progressive-loan-embeddable-schedule-generator/build/libs/*-all.jar | head -n 1`) - echo "EMBEDDABLE_JAR_FILE=$EMBEDDABLE_JAR_FILE" >> $GITHUB_ENV - - name: Run unit tests - run: ./gradlew --no-daemon --console=plain :fineract-progressive-loan-embeddable-schedule-generator:test - - name: Build Sample Application - run: | - mkdir sample-app - javac -cp $EMBEDDABLE_JAR_FILE -d sample-app fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java - env: - EMBEDDABLE_JAR_FILE: ${{ env.EMBEDDABLE_JAR_FILE }} - - name: Run Schedule Generator Sample Application - run: | - java -cp $EMBEDDABLE_JAR_FILE:sample-app Main - java -cp $EMBEDDABLE_JAR_FILE:sample-app Main 25 - env: - EMBEDDABLE_JAR_FILE: ${{ env.EMBEDDABLE_JAR_FILE }} - - name: Archive test results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-results - path: | - build/reports/ - fineract-progressive-loan-embeddable-schedule-generator/build/reports/ diff --git a/.github/workflows/build-mariadb.yml b/.github/workflows/build-mariadb.yml index c59b242724c..056433f8b69 100644 --- a/.github/workflows/build-mariadb.yml +++ b/.github/workflows/build-mariadb.yml @@ -1,92 +1,142 @@ -name: Fineract Build & Test - MariaDB +name: Fineract Cargo & Unit- & Integration tests - MariaDB + on: [push, pull_request] + permissions: contents: read + jobs: - build: + test: runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + task: [test-core-1, test-core-2, test-core-3, test-core-4, test-core-5, test-twofactor, test-oauth2] + services: - mariad: - image: mariadb:11.5.2 - ports: - - 3306:3306 - env: - MARIADB_ROOT_PASSWORD: mysql - options: --health-cmd="healthcheck.sh --su-mysql --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 - mock-oauth2-server: - image: ghcr.io/navikt/mock-oauth2-server:2.1.10 - ports: - - 9000:9000 - env: - SERVER_PORT: 9000 - JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + mariadb: + image: mariadb:11.5.2 + ports: + - 3306:3306 + env: + MARIADB_ROOT_PASSWORD: mysql + options: --health-cmd="healthcheck.sh --su-mysql --connect --innodb_initialized" --health-interval=5s --health-timeout=2s --health-retries=3 + + mock-oauth2-server: + image: ghcr.io/navikt/mock-oauth2-server:2.1.10 + ports: + - 9000:9000 + env: + SERVER_PORT: 9000 + JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + env: - TZ: Asia/Kolkata - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + TZ: Asia/Kolkata + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + AWS_ENDPOINT_URL: http://localhost:4566 + AWS_ACCESS_KEY_ID: localstack + AWS_SECRET_ACCESS_KEY: localstack + AWS_REGION: us-east-1 + FINERACT_REPORT_EXPORT_S3_ENABLED: true + FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true + - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'zulu' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + uses: gradle/actions/setup-gradle@v4.4.1 with: validate-wrappers: true + - name: Verify MariaDB connection run: | - while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do - sleep 1 - done + while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do + sleep 1 + done + - name: Initialise databases run: | - ./gradlew --no-daemon -q createDB -PdbName=fineract_tenants - ./gradlew --no-daemon -q createDB -PdbName=fineract_default + ./gradlew --no-daemon -q createDB -PdbName=fineract_tenants + ./gradlew --no-daemon -q createDB -PdbName=fineract_default + - name: Start LocalStack - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 run: | docker run -d --name localstack -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack:2.1 sleep 10 docker exec localstack awslocal s3api create-bucket --bucket fineract-reports - echo "LocalStack initialization complete" - - name: Build & Test - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 - FINERACT_REPORT_EXPORT_S3_ENABLED: true - FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + + - name: Generate test class list (only for test-core-X) + if: startsWith(matrix.task, 'test-core-') + run: | + chmod +x scripts/split-tests.sh + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + ./scripts/split-tests.sh 5 $SHARD_INDEX + cat "shard-tests_${SHARD_INDEX}.txt" + + - name: Run Gradle Task run: | - ./gradlew --no-daemon --console=plain build -x cucumber -x test -x doc - ./gradlew --no-daemon --console=plain cucumber -x :fineract-e2e-tests-runner:cucumber - ./gradlew --no-daemon --console=plain test -x :twofactor-tests:test -x :oauth2-test:test -x :fineract-e2e-tests-runner:test - ./gradlew --no-daemon --console=plain :twofactor-tests:test - ./gradlew --no-daemon --console=plain :oauth2-tests:test + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + case "${{ matrix.task }}" in + test-twofactor) + ./gradlew --no-daemon :twofactor-tests:test -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-oauth2) + ./gradlew --no-daemon :oauth2-tests:test -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-core-*) + echo "Grouping test classes by module..." + declare -A module_tests + + while IFS=, read -r module class; do + module_tests["$module"]+="$class " + done < "shard-tests_${SHARD_INDEX}.txt" + + for module in "${!module_tests[@]}"; do + echo "Running tests in $module:" + for class in ${module_tests[$module]}; do + echo " - $class" + done + + # Build test args + test_args=$(for class in ${module_tests[$module]}; do echo --tests "$class"; done | xargs) + + # Run test task for this module + ./gradlew "$module:test" $test_args -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + done + ;; + esac + - name: Archive test results if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: test-results - path: | - build/reports/ - integration-tests/build/reports/ - twofactor-tests/build/reports/ - oauth2-tests/build/reports/ + name: test-results-${{ matrix.task }} + path: '**/build/reports/' + retention-days: 5 + - name: Archive server logs if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: server-logs - path: | - integration-tests/build/cargo/ - twofactor-tests/build/cargo/ - oauth2-tests/build/cargo/ + name: server-logs-${{ matrix.task }} + path: '**/build/cargo/' + retention-days: 5 diff --git a/.github/workflows/build-mysql.yml b/.github/workflows/build-mysql.yml index dce0d0be843..1b31c9a2637 100644 --- a/.github/workflows/build-mysql.yml +++ b/.github/workflows/build-mysql.yml @@ -1,92 +1,142 @@ -name: Fineract Build & Test - MySQL +name: Fineract Cargo & Unit- & Integration tests - MySQL + on: [push, pull_request] + permissions: contents: read + jobs: - build: + test: runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + task: [test-core-1, test-core-2, test-core-3, test-core-4, test-core-5, test-twofactor, test-oauth2] + services: - mariad: - image: mysql:9.1 - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: mysql - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - mock-oauth2-server: - image: ghcr.io/navikt/mock-oauth2-server:2.1.10 - ports: - - 9000:9000 - env: - SERVER_PORT: 9000 - JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + mysql: + image: mysql:9.1 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: mysql + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + mock-oauth2-server: + image: ghcr.io/navikt/mock-oauth2-server:2.1.10 + ports: + - 9000:9000 + env: + SERVER_PORT: 9000 + JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + env: - TZ: Asia/Kolkata - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + TZ: Asia/Kolkata + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + AWS_ENDPOINT_URL: http://localhost:4566 + AWS_ACCESS_KEY_ID: localstack + AWS_SECRET_ACCESS_KEY: localstack + AWS_REGION: us-east-1 + FINERACT_REPORT_EXPORT_S3_ENABLED: true + FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true + - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'zulu' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + uses: gradle/actions/setup-gradle@v4.4.1 with: validate-wrappers: true - - name: Verify MariaDB connection + + - name: Verify MySQL connection run: | - while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do - sleep 1 - done + while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do + sleep 1 + done + - name: Initialise databases run: | - ./gradlew --no-daemon -q createMySQLDB -PdbName=fineract_tenants - ./gradlew --no-daemon -q createMySQLDB -PdbName=fineract_default + ./gradlew --no-daemon -q createMySQLDB -PdbName=fineract_tenants + ./gradlew --no-daemon -q createMySQLDB -PdbName=fineract_default + - name: Start LocalStack - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 run: | docker run -d --name localstack -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack:2.1 sleep 10 docker exec localstack awslocal s3api create-bucket --bucket fineract-reports - echo "LocalStack initialization complete" - - name: Build & Test - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 - FINERACT_REPORT_EXPORT_S3_ENABLED: true - FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + + - name: Generate test class list (only for test-core-X) + if: startsWith(matrix.task, 'test-core-') + run: | + chmod +x scripts/split-tests.sh + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + ./scripts/split-tests.sh 5 $SHARD_INDEX + cat "shard-tests_${SHARD_INDEX}.txt" + + - name: Run Gradle Task run: | - ./gradlew --no-daemon --console=plain build -x cucumber -x test -x doc - ./gradlew --no-daemon --console=plain cucumber -x :fineract-e2e-tests-runner:cucumber - ./gradlew --no-daemon --console=plain test -x :twofactor-tests:test -x :oauth2-test:test -x :fineract-e2e-tests-runner:test -PdbType=mysql - ./gradlew --no-daemon --console=plain :twofactor-tests:test -PdbType=mysql - ./gradlew --no-daemon --console=plain :oauth2-tests:test -PdbType=mysql + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + case "${{ matrix.task }}" in + test-twofactor) + ./gradlew --no-daemon :twofactor-tests:test -PdbType=mysql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-oauth2) + ./gradlew --no-daemon :oauth2-tests:test -PdbType=mysql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-core-*) + echo "Grouping test classes by module..." + declare -A module_tests + + while IFS=, read -r module class; do + module_tests["$module"]+="$class " + done < "shard-tests_${SHARD_INDEX}.txt" + + for module in "${!module_tests[@]}"; do + echo "Running tests in $module:" + for class in ${module_tests[$module]}; do + echo " - $class" + done + + # Build test args + test_args=$(for class in ${module_tests[$module]}; do echo --tests "$class"; done | xargs) + + # Run test task for this module + ./gradlew "$module:test" $test_args -PdbType=mysql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + done + ;; + esac + - name: Archive test results if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: test-results - path: | - build/reports/ - integration-tests/build/reports/ - twofactor-tests/build/reports/ - oauth2-tests/build/reports/ + name: test-results-${{ matrix.task }} + path: '**/build/reports/' + retention-days: 5 + - name: Archive server logs if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: server-logs - path: | - integration-tests/build/cargo/ - twofactor-tests/build/cargo/ - oauth2-tests/build/cargo/ + name: server-logs-${{ matrix.task }} + path: '**/build/cargo/' + retention-days: 5 diff --git a/.github/workflows/build-postgresql.yml b/.github/workflows/build-postgresql.yml index 9e0ffebc762..f7b712301ea 100644 --- a/.github/workflows/build-postgresql.yml +++ b/.github/workflows/build-postgresql.yml @@ -1,93 +1,143 @@ -name: Fineract Build & Test - PostgreSQL +name: Fineract Cargo & Unit- & Integration tests - PostgreSQL + on: [push, pull_request] + permissions: contents: read + jobs: - build: + test: runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + task: [test-core-1, test-core-2, test-core-3, test-core-4, test-core-5, test-twofactor, test-oauth2] + services: - postgresql: - image: postgres:17.4 - ports: - - 5432:5432 - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: postgres - options: --health-cmd="pg_isready -q -d postgres -U root" --health-interval=5s --health-timeout=2s --health-retries=3 - mock-oauth2-server: - image: ghcr.io/navikt/mock-oauth2-server:2.1.10 - ports: - - 9000:9000 - env: - SERVER_PORT: 9000 - JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + postgresql: + image: postgres:17.4 + ports: + - 5432:5432 + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: postgres + options: --health-cmd="pg_isready -q -d postgres -U root" --health-interval=5s --health-timeout=2s --health-retries=3 + + mock-oauth2-server: + image: ghcr.io/navikt/mock-oauth2-server:2.1.10 + ports: + - 9000:9000 + env: + SERVER_PORT: 9000 + JSON_CONFIG: '{ "interactiveLogin": true, "httpServer": "NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract", "tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match": "fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }' + env: - TZ: Asia/Kolkata - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + TZ: Asia/Kolkata + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + AWS_ENDPOINT_URL: http://localhost:4566 + AWS_ACCESS_KEY_ID: localstack + AWS_SECRET_ACCESS_KEY: localstack + AWS_REGION: us-east-1 + FINERACT_REPORT_EXPORT_S3_ENABLED: true + FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true + - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'zulu' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + uses: gradle/actions/setup-gradle@v4.4.1 with: validate-wrappers: true + - name: Verify PostgreSQL connection run: | - while ! pg_isready -d postgres -U root -h 127.0.0.1 -p 5432 ; do - sleep 1 - done + while ! pg_isready -d postgres -U root -h 127.0.0.1 -p 5432 ; do + sleep 1 + done + - name: Initialise databases run: | - ./gradlew --no-daemon -q createPGDB -PdbName=fineract_tenants - ./gradlew --no-daemon -q createPGDB -PdbName=fineract_default + ./gradlew --no-daemon -q createPGDB -PdbName=fineract_tenants + ./gradlew --no-daemon -q createPGDB -PdbName=fineract_default + - name: Start LocalStack - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 run: | docker run -d --name localstack -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack:2.1 sleep 10 docker exec localstack awslocal s3api create-bucket --bucket fineract-reports - echo "LocalStack initialization complete" - - name: Build & Test - env: - AWS_ENDPOINT_URL: http://localhost:4566 - AWS_ACCESS_KEY_ID: localstack - AWS_SECRET_ACCESS_KEY: localstack - AWS_REGION: us-east-1 - FINERACT_REPORT_EXPORT_S3_ENABLED: true - FINERACT_REPORT_EXPORT_S3_BUCKET_NAME: fineract-reports + + - name: Generate test class list (only for test-core-X) + if: startsWith(matrix.task, 'test-core-') + run: | + chmod +x scripts/split-tests.sh + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + ./scripts/split-tests.sh 5 $SHARD_INDEX + cat "shard-tests_${SHARD_INDEX}.txt" + + - name: Run Gradle Task run: | - ./gradlew --no-daemon --console=plain build -x cucumber -x test -x doc - ./gradlew --no-daemon --console=plain cucumber -x :fineract-e2e-tests-runner:cucumber - ./gradlew --no-daemon --console=plain test -x :twofactor-tests:test -x :oauth2-test:test -x :fineract-e2e-tests-runner:test -PdbType=postgresql - ./gradlew --no-daemon --console=plain :twofactor-tests:test -PdbType=postgresql - ./gradlew --no-daemon --console=plain :oauth2-tests:test -PdbType=postgresql + SHARD_INDEX=$(echo "${{ matrix.task }}" | awk -F'-' '{print $3}') + case "${{ matrix.task }}" in + test-twofactor) + ./gradlew --no-daemon :twofactor-tests:test -PdbType=postgresql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-oauth2) + ./gradlew --no-daemon :oauth2-tests:test -PdbType=postgresql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + ;; + test-core-*) + echo "Grouping test classes by module..." + declare -A module_tests + + while IFS=, read -r module class; do + module_tests["$module"]+="$class " + done < "shard-tests_${SHARD_INDEX}.txt" + + for module in "${!module_tests[@]}"; do + echo "Running tests in $module:" + for class in ${module_tests[$module]}; do + echo " - $class" + done + + # Build test args + test_args=$(for class in ${module_tests[$module]}; do echo --tests "$class"; done | xargs) + + # Run test task for this module + ./gradlew "$module:test" $test_args -PdbType=postgresql -x checkstyleJmh -x checkstyleMain -x checkstyleTest -x spotlessCheck -x spotlessApply -x spotbugsMain -x spotbugsTest -x javadoc -x javadocJar -x modernizer + done + ;; + esac + - name: Archive test results if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: test-results - path: | - build/reports/ - integration-tests/build/reports/ - twofactor-tests/build/reports/ - oauth2-tests/build/reports/ + name: test-results-${{ matrix.task }} + path: '**/build/reports/' + retention-days: 5 + - name: Archive server logs if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@v4 with: - name: server-logs - path: | - integration-tests/build/cargo/ - twofactor-tests/build/cargo/ - oauth2-tests/build/cargo/ + name: server-logs-${{ matrix.task }} + path: '**/build/cargo/' + retention-days: 5 diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml deleted file mode 100644 index 221b316553d..00000000000 --- a/.github/workflows/build-tests.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Fineract Tests -on: [push, pull_request] -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-24.04 - env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - IMAGE_NAME: fineract - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 - with: - java-version: '21' - distribution: 'zulu' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - - name: Build the image - run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber - - name: Start the Fineract stack - run: docker compose -f docker-compose-postgresql-test-activemq.yml up -d - - name: Check the stack - run: docker ps - - name: Check health Manager - run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/health - - name: Execute tests - env: - BASE_URL: https://localhost:8443 - TEST_USERNAME: mifos - TEST_PASSWORD: password - TEST_STRONG_PASSWORD: A1b2c3d4e5f$ - TEST_TENANT_ID: default - INITIALIZATION_ENABLED: true - EVENT_VERIFICATION_ENABLED: true - ACTIVEMQ_BROKER_URL: tcp://localhost:61616 - ACTIVEMQ_TOPIC_NAME: events - run: ./gradlew --no-daemon --console=plain :fineract-e2e-tests-runner:cucumber --tags 'not @Skip' allureReport diff --git a/.github/workflows/smoke-kafka.yml b/.github/workflows/smoke-kafka.yml deleted file mode 100644 index fe0f9d7c5ae..00000000000 --- a/.github/workflows/smoke-kafka.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Fineract Kafka Smoke -on: [push, pull_request] -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-24.04 - env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - IMAGE_NAME: fineract - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 - with: - java-version: '21' - distribution: 'zulu' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - - name: Build the image - run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber - - name: Start the Kafka Stack - run: docker compose -f docker-compose-postgresql-kafka.yml up --scale fineract-worker=1 -d - - name: Check the stack - run: docker ps - - name: Check health Manager - run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/health - - name: Check health Worker1 - run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8444/fineract-provider/actuator/health - - name: Check info Manager - run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/info | wc --chars) > 100 )) - - name: Check info Worker1 - run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8444/fineract-provider/actuator/info | wc --chars) > 100 )) - - name: Run Smoke Test with Remote COB - run: ./gradlew --no-daemon --console=plain :integration-tests:cleanTest :integration-tests:test --tests "org.apache.fineract.integrationtests.investor.externalassetowner.InitiateExternalAssetOwnerTransferTest.saleActiveLoanToExternalAssetOwnerAndBuybackADayLater" -PcargoDisabled diff --git a/.github/workflows/smoke-activemq.yml b/.github/workflows/smoke-messaging.yml similarity index 79% rename from .github/workflows/smoke-activemq.yml rename to .github/workflows/smoke-messaging.yml index e52bcde93ae..0e88f3d132a 100644 --- a/.github/workflows/smoke-activemq.yml +++ b/.github/workflows/smoke-messaging.yml @@ -1,37 +1,61 @@ -name: Fineract ActiveMQ Smoke +name: Fineract Messaging Smoke Tests + on: [push, pull_request] + permissions: contents: read + jobs: - build: + smoke-test: + name: Smoke Test with ${{ matrix.messaging }} runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - messaging: ActiveMQ + compose_file: docker-compose-postgresql-activemq.yml + - messaging: Kafka + compose_file: docker-compose-postgresql-kafka.yml + env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} IMAGE_NAME: fineract + steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 + - name: Set up JDK 21 uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: java-version: '21' distribution: 'zulu' + - name: Setup Gradle uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:jibDockerBuild -Djib.to.image=$IMAGE_NAME -x test -x cucumber - - name: Start the ActiveMQ Stack - run: docker compose -f docker-compose-postgresql-activemq.yml up --scale fineract-worker=1 -d + + - name: Start the ${{ matrix.messaging }} Stack + run: docker compose -f ${{ matrix.compose_file }} up --scale fineract-worker=1 -d + - name: Check the stack run: docker ps + - name: Check health Manager run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/health + - name: Check health Worker1 run: curl -f -k --retry 60 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8444/fineract-provider/actuator/health + - name: Check info Manager run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8443/fineract-provider/actuator/info | wc --chars) > 100 )) + - name: Check info Worker1 run: (( $(curl -f -k --retry 5 --retry-all-errors --connect-timeout 30 --retry-delay 30 https://localhost:8444/fineract-provider/actuator/info | wc --chars) > 100 )) + - name: Run Smoke Test with Remote COB run: ./gradlew --no-daemon --console=plain :integration-tests:cleanTest :integration-tests:test --tests "org.apache.fineract.integrationtests.investor.externalassetowner.InitiateExternalAssetOwnerTransferTest.saleActiveLoanToExternalAssetOwnerAndBuybackADayLater" -PcargoDisabled diff --git a/build.gradle b/build.gradle index e3a50e60ef0..b0b16461233 100644 --- a/build.gradle +++ b/build.gradle @@ -341,7 +341,8 @@ allprojects { "**/generated/**/*MapperImpl.java", '**/META-INF/fineract-test.config', // Apache-specific GitHub metadata settings file - '/.asf.yaml' + '/.asf.yaml', + '**/*.sh' ] } } @@ -376,20 +377,6 @@ configure(project.fineractJavaProjects) { withJavadocJar() } - // TODO: we are forcing Gradle to do something it shouldn't! This effects Gradle caching AND breaks Liquibase! - // tasks.withType(ProcessResources) { - // destinationDir = layout.buildDirectory.dir('classes/java/main').get().asFile - // } - - processResources { - destinationDir = file("${buildDir}/resources/main") - - doLast { - delete fileTree(dir: "${buildDir}/classes/java/main/db") - delete fileTree(dir: "${buildDir}/classes/java/main/META-INF") - } - } - // Add performance optimizations configurations.all { resolutionStrategy { diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.java new file mode 100644 index 00000000000..ecc64eeb6d6 --- /dev/null +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/accrual/handler/ExecutePeriodicAccrualForSavingsCommandHandler.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.accounting.accrual.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "PERIODICACCRUALACCOUNTINGFORSAVINGS", action = "EXECUTE") +@RequiredArgsConstructor +public class ExecutePeriodicAccrualForSavingsCommandHandler implements NewCommandSourceHandler { + + private final AccrualAccountingWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.executeLoansPeriodicAccrual(command); + } +} diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java index 511ce5e801b..6cce7290efc 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java @@ -169,7 +169,8 @@ public void validateForSavingsProductCreate(final String json, DepositAccountTyp Locale.getDefault()); baseDataValidator.reset().parameter(accountingRuleParamName).value(accountingRuleType).notNull().inMinMaxRange(1, 3); - if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) { + if (AccountingValidations.isCashBasedAccounting(accountingRuleType) + || AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { final Long savingsControlAccountId = this.fromApiJsonHelper .extractLongNamed(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), element); @@ -225,7 +226,6 @@ public void validateForSavingsProductCreate(final String json, DepositAccountTyp baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff).notNull() .integerGreaterThanZero(); } - } // Periodic Accrual Accounting aditional GL Accounts diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index 1b11ae5ec43..2c26ae4530f 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -100,6 +100,7 @@ public void mergeProductToAccountMappingChanges(final JsonElement element, final optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditInterestAccountId"); optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditFeesAccountId"); optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditPenaltyAccountId"); + optionalProductToGLAccountMappingEntries.add("interestReceivableAccountId"); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue()); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue()); optionalProductToGLAccountMappingEntries.add(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue()); diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 3c250a0b6df..6e2c44c2378 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -358,6 +358,8 @@ private Map setAccrualPeriodicSavingsProductToGLAccountMaps(fina // Assets if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_REFERENCE)) { accountMappingDetails.put(SavingProductAccountingDataParams.SAVINGS_REFERENCE.getValue(), glAccountData); + } else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_RECEIVABLE)) { + accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_RECEIVABLE.getValue(), glAccountData); } else if (glAccountForSavings.equals(AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL)) { accountMappingDetails.put(SavingProductAccountingDataParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), glAccountData); } else if (glAccountForSavings.equals(AccrualAccountsForSavings.FEES_RECEIVABLE)) { diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java index 903ae202546..27cb564a263 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java @@ -263,9 +263,9 @@ public void handleChangesToSavingsProductToGLAccountMappings(final Long savingsP savingsProductId, AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(), AccrualAccountsForSavings.FEES_RECEIVABLE.toString(), changes); - mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), - savingsProductId, AccrualAccountsForSavings.PENALTIES_RECEIVABLE.getValue(), - AccrualAccountsForSavings.PENALTIES_RECEIVABLE.toString(), changes); + mergeSavingsToAssetAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), + savingsProductId, AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForSavings.INTEREST_RECEIVABLE.toString(), changes); // income mergeSavingsToIncomeAccountMappingChanges(element, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), diff --git a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java index 137c42f0f63..8b18bf9a4d8 100644 --- a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java +++ b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.charge.service; +import java.util.Collection; import java.util.List; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -112,6 +113,8 @@ public interface ChargeReadPlatformService { */ List retrieveSavingsProductCharges(Long savingsProductId); + Collection retrieveSavingsProductAccrualCharges(Long savingsProductId); + /** Retrieve savings account charges **/ List retrieveSavingsAccountApplicableCharges(Long savingsId); diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index 6016b5c29dd..ce7e97c1fec 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -312,7 +312,8 @@ public enum AccrualAccountsForSavings { ESCHEAT_LIABILITY(14), // FEES_RECEIVABLE(15), // PENALTIES_RECEIVABLE(16), // - INTEREST_PAYABLE(17); + INTEREST_PAYABLE(17), // + INTEREST_RECEIVABLE(18); private final Integer value; @@ -366,6 +367,7 @@ public enum SavingProductAccountingParams { LOSSES_WRITTEN_OFF("writeOffAccountId"), // ESCHEAT_LIABILITY("escheatLiabilityId"), // PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), // + INTEREST_RECEIVABLE("interestReceivableAccountId"), // FEES_RECEIVABLE("feesReceivableAccountId"), // INTEREST_PAYABLE("interestPayableAccountId"); @@ -404,7 +406,8 @@ public enum SavingProductAccountingDataParams { ESCHEAT_LIABILITY("escheatLiabilityAccount"), // FEES_RECEIVABLE("feeReceivableAccount"), // PENALTIES_RECEIVABLE("penaltyReceivableAccount"), // - INTEREST_PAYABLE("interestPayableAccount"); // + INTEREST_PAYABLE("interestPayableAccount"), // + INTEREST_RECEIVABLE("interestReceivableAccount"); // private final String value; diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.java new file mode 100644 index 00000000000..70c7613ae47 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingValidations.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.accounting.common; + +public class AccountingValidations { + + protected AccountingValidations() {} + + public static boolean isCashBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.CASH_BASED.getValue().equals(accountingRuleType); + } + + public static boolean isAccrualPeriodicBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType); + } + + public static boolean isUpfrontAccrualAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); + } + + public static boolean isAccrualBasedAccounting(final Integer accountingRuleType) { + return AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingRuleType) + || AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingRuleType); + } + + public static boolean isCashOrAccrualBasedAccounting(final Integer accountingRuleType) { + return isCashBasedAccounting(accountingRuleType) || isAccrualBasedAccounting(accountingRuleType); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java b/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java index d2fd82ce1aa..8dead5d79be 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java @@ -20,8 +20,9 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; public interface NewCommandSourceHandler { - CommandProcessingResult processCommand(JsonCommand command); + CommandProcessingResult processCommand(JsonCommand command) throws MultiException; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index 75840388ddc..3049cc1239e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException; +import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.lang.NonNull; @@ -131,7 +132,7 @@ public CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand @Transactional public CommandProcessingResult processCommand(NewCommandSourceHandler handler, JsonCommand command, CommandSource commandSource, - AppUser user, boolean isApprovedByChecker) { + AppUser user, boolean isApprovedByChecker) throws MultiException { final CommandProcessingResult result = handler.processCommand(command); String permission = commandSource.getPermissionCode(); 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 b470b77b564..8fa0bc73530 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 @@ -3416,6 +3416,15 @@ public CommandWrapperBuilder unblockSavingsAccount(final Long accountId) { return this; } + public CommandWrapperBuilder addAccrualsToSavingsAccount(final Long accountId) { + this.actionName = "ADD_ACCRUALS"; + this.entityName = "SAVINGSACCOUNT"; + this.savingsId = accountId; + this.entityId = null; + this.href = "/savingsaccounts/" + accountId + "?command=addAccrualTransactions"; + return this; + } + public CommandWrapperBuilder disableAdHoc(Long adHocId) { this.actionName = "DISABLE"; this.entityName = "ADHOC"; @@ -3858,4 +3867,13 @@ public CommandWrapperBuilder undoContractTermination(final Long loanId) { this.href = "/loans/" + loanId; return this; } + + public CommandWrapperBuilder makeLoanBuyDownFee(final Long loanId) { + this.actionName = "BUYDOWNFEE"; + this.entityName = "LOAN"; + this.entityId = null; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions/template?command=buyDownFee"; + return this; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java index 800592d5c4f..4d4c8575177 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResource.java @@ -19,13 +19,8 @@ package org.apache.fineract.infrastructure.cache.api; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.PUT; @@ -33,16 +28,16 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import java.util.Collection; +import java.util.UUID; +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.command.core.CommandPipeline; +import org.apache.fineract.infrastructure.cache.command.CacheSwitchCommand; import org.apache.fineract.infrastructure.cache.data.CacheData; -import org.apache.fineract.infrastructure.cache.data.request.CacheRequest; +import org.apache.fineract.infrastructure.cache.data.CacheSwitchRequest; +import org.apache.fineract.infrastructure.cache.data.CacheSwitchResponse; import org.apache.fineract.infrastructure.cache.service.RuntimeDelegatingCacheManager; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @@ -50,37 +45,45 @@ @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Component -@Tag(name = "Cache", description = "The following settings are possible for cache:\n" + "\n" + "No Caching: caching turned off\n" - + "Single node: caching on for single instance deployments of platorm (works for multiple tenants but only one tomcat)\n" - + "By default caching is set to No Caching. Switching between caches results in the cache been clear e.g. from Single node to No cache and back again would clear down the single node cache.") +@Tag(name = "Cache", description = """ + The following settings are possible for cache: + + No Caching: caching turned off + + Single node: caching on for single instance deployments of platorm (works for multiple tenants but only one tomcat). + By default caching is set to No Caching. Switching between caches results in the cache been clear e.g. from single + node to no cache and back again would clear down the single node cache. + """) @RequiredArgsConstructor public class CacheApiResource { - private static final String RESOURCE_NAME_FOR_PERMISSIONS = "CACHE"; - - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @Qualifier("runtimeDelegatingCacheManager") private final RuntimeDelegatingCacheManager cacheService; + private final CommandPipeline commandPipeline; @GET - @Operation(summary = "Retrieve Cache Types", description = "Returns the list of caches.\n" + "\n" + "Example Requests:\n" + "\n" - + "caches") + @Operation(summary = "Retrieve Cache Types", description = """ + Returns the list of caches. + + Example Requests: + + caches + """) public Collection retrieveAll() { - this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); return cacheService.retrieveAll(); } @PUT @Operation(summary = "Switch Cache", description = "Switches the cache to chosen one.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = CacheRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CacheApiResourceSwagger.PutCachesResponse.class))) }) - public CommandProcessingResult switchCache(@Parameter(hidden = true) CacheRequest cacheRequest) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCache() - .withJson(toApiJsonSerializer.serialize(cacheRequest)).build(); + public CacheSwitchResponse switchCache(@Valid CacheSwitchRequest request) { + final var command = new CacheSwitchCommand(); + + command.setId(UUID.randomUUID()); + command.setCreatedAt(DateUtils.getAuditOffsetDateTime()); + command.setPayload(request); + + final Supplier response = commandPipeline.send(command); - return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return response.get(); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/request/CacheRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/CacheSwitchCommand.java similarity index 69% rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/request/CacheRequest.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/CacheSwitchCommand.java index 6adf680e029..a8a95889449 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/request/CacheRequest.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/CacheSwitchCommand.java @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.infrastructure.cache.data.request; +package org.apache.fineract.infrastructure.cache.command; -import java.io.Serial; -import java.io.Serializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.infrastructure.cache.data.CacheSwitchRequest; -public record CacheRequest(Long cacheType) implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; -} +@Data +@EqualsAndHashCode(callSuper = true) +public class CacheSwitchCommand extends Command {} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java deleted file mode 100644 index 99d90afd2a3..00000000000 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/command/UpdateCacheCommandHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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.cache.command; - -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.commands.annotation.CommandType; -import org.apache.fineract.commands.handler.NewCommandSourceHandler; -import org.apache.fineract.infrastructure.cache.CacheApiConstants; -import org.apache.fineract.infrastructure.cache.domain.CacheType; -import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@CommandType(entity = "CACHE", action = "UPDATE") -public class UpdateCacheCommandHandler implements NewCommandSourceHandler { - - private final CacheWritePlatformService cacheService; - private static final Set REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(CacheApiConstants.CACHE_TYPE_PARAMETER)); - - @Autowired - public UpdateCacheCommandHandler(final CacheWritePlatformService cacheService) { - this.cacheService = cacheService; - } - - @Transactional - @Override - public CommandProcessingResult processCommand(final JsonCommand command) { - - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - command.checkForUnsupportedParameters(typeOfMap, json, REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(CacheApiConstants.RESOURCE_NAME.toLowerCase()); - - final int cacheTypeEnum = command.integerValueSansLocaleOfParameterNamed(CacheApiConstants.CACHE_TYPE_PARAMETER); - baseDataValidator.reset().parameter(CacheApiConstants.CACHE_TYPE_PARAMETER).value(Integer.valueOf(cacheTypeEnum)).notNull() - .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - final CacheType cacheType = CacheType.fromInt(cacheTypeEnum); - - final Map changes = this.cacheService.switchToCache(cacheType); - - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).with(changes).build(); - } -} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheData.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheData.java index f487b5544fb..771b6d553b3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheData.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheData.java @@ -18,23 +18,20 @@ */ package org.apache.fineract.infrastructure.cache.data; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +@Builder @Data @NoArgsConstructor -@Accessors(chain = true) +@AllArgsConstructor public final class CacheData { @SuppressWarnings("unused") private EnumOptionData cacheType; @SuppressWarnings("unused") private boolean enabled; - - public static CacheData instance(final EnumOptionData cacheType, final boolean enabled) { - return new CacheData().setCacheType(cacheType).setEnabled(enabled); - } - } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResourceSwagger.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchRequest.java similarity index 55% rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResourceSwagger.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchRequest.java index 0c200e93f65..7aa06c41e71 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/api/CacheApiResourceSwagger.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchRequest.java @@ -16,38 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.infrastructure.cache.api; - -import io.swagger.v3.oas.annotations.media.Schema; - -/** - * Created by sanyam on 28/7/17. - */ -final class CacheApiResourceSwagger { - - private CacheApiResourceSwagger() { - - } - - @Schema(description = "PutCachesResponse") - public static final class PutCachesResponse { - - private PutCachesResponse() { - - } - - public static final class PutCachechangesSwagger { - - private PutCachechangesSwagger() { - - } - - @Schema(example = "2") - public Long cacheType; - - } - - public PutCachechangesSwagger cacheType; - - } +package org.apache.fineract.infrastructure.cache.data; + +import jakarta.validation.constraints.NotNull; +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 CacheSwitchRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @NotNull(message = "{org.apache.fineract.cache.cache-type.not-null}") + private Integer cacheType; } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/request/CurrencyRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchResponse.java similarity index 71% rename from fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/request/CurrencyRequest.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchResponse.java index 97f988f4d53..392412c2ddb 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/request/CurrencyRequest.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/data/CacheSwitchResponse.java @@ -16,14 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.organisation.monetary.data.request; +package org.apache.fineract.infrastructure.cache.data; import java.io.Serial; import java.io.Serializable; -import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -public record CurrencyRequest(List currencies) implements Serializable { +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CacheSwitchResponse implements Serializable { @Serial private static final long serialVersionUID = 1L; + + private Integer cacheType; + private Map changes; } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.java new file mode 100644 index 00000000000..f609863fa10 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/handler/CacheSwitchCommandHandler.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.infrastructure.cache.handler; + +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.infrastructure.cache.data.CacheSwitchRequest; +import org.apache.fineract.infrastructure.cache.data.CacheSwitchResponse; +import org.apache.fineract.infrastructure.cache.domain.CacheType; +import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CacheSwitchCommandHandler implements CommandHandler { + + private final CacheWritePlatformService cacheService; + + @Transactional + @Override + public CacheSwitchResponse handle(final Command command) { + var request = command.getPayload(); + var cacheType = CacheType.fromInt(request.getCacheType()); + var changes = cacheService.switchToCache(cacheType); + + return CacheSwitchResponse.builder().changes(changes).cacheType(request.getCacheType()).build(); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java index 1886701293d..6e8bcb0dc1c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/cache/service/RuntimeDelegatingCacheManager.java @@ -78,8 +78,8 @@ public Collection retrieveAll() { final EnumOptionData noCacheType = CacheEnumerations.cacheType(CacheType.NO_CACHE); final EnumOptionData singleNodeCacheType = CacheEnumerations.cacheType(CacheType.SINGLE_NODE); - final CacheData noCache = CacheData.instance(noCacheType, noCacheEnabled); - final CacheData singleNodeCache = CacheData.instance(singleNodeCacheType, ehCacheEnabled); + final CacheData noCache = CacheData.builder().cacheType(noCacheType).enabled(noCacheEnabled).build(); + final CacheData singleNodeCache = CacheData.builder().cacheType(singleNodeCacheType).enabled(ehCacheEnabled).build(); return Arrays.asList(noCache, singleNodeCache); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java index 43e2dcc077a..2c7ca64b30e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/handler/ExternalEventConfigurationUpdateHandler.java @@ -19,18 +19,18 @@ package org.apache.fineract.infrastructure.event.external.handler; 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.commands.annotation.CommandType; import org.apache.fineract.infrastructure.event.external.data.ExternalEventConfigurationUpdateRequest; import org.apache.fineract.infrastructure.event.external.data.ExternalEventConfigurationUpdateResponse; import org.apache.fineract.infrastructure.event.external.service.ExternalEventConfigurationWritePlatformService; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +@Slf4j +@Component @RequiredArgsConstructor -@Service -@CommandType(entity = "EXTERNAL_EVENT_CONFIGURATION", action = "UPDATE") public class ExternalEventConfigurationUpdateHandler implements CommandHandler { 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 f579fa27699..3d767a7c88a 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 @@ -58,7 +58,7 @@ public enum JobName { PURGE_EXTERNAL_EVENTS("Purge External Events"), // PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), // ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), // - ; + ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"); // private final String name; diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java new file mode 100644 index 00000000000..efea32844bb --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.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.organisation.monetary.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.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.util.UUID; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.command.core.CommandPipeline; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand; +import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; +import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/currencies") +@Component +@Tag(name = "Currency", description = "Application related configuration around viewing/updating the currencies permitted for use within the MFI.") +@RequiredArgsConstructor +public class CurrenciesApiResource { + + private final OrganisationCurrencyReadPlatformService readPlatformService; + private final CommandPipeline commandPipeline; + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Currency Configuration", description = """ + Returns the list of currencies permitted for use AND the list of currencies not selected (but available for selection). + + Example Requests: + + currencies + currencies?fields=selectedCurrencyOptions + """) + public CurrencyConfigurationData retrieveCurrencies() { + return readPlatformService.retrieveCurrencyConfiguration(); + } + + @PUT + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.") + public CurrencyUpdateResponse updateCurrencies(CurrencyUpdateRequest request) { + final var command = new CurrencyUpdateCommand(); + + command.setId(UUID.randomUUID()); + command.setCreatedAt(DateUtils.getAuditOffsetDateTime()); + command.setPayload(request); + + final Supplier response = commandPipeline.send(command); + + return response.get(); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/command/CurrencyUpdateCommand.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/command/CurrencyUpdateCommand.java new file mode 100644 index 00000000000..b93083edd0c --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/command/CurrencyUpdateCommand.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.organisation.monetary.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CurrencyUpdateCommand extends Command {} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/ApplicationCurrencyConfigurationData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyConfigurationData.java similarity index 68% rename from fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/ApplicationCurrencyConfigurationData.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyConfigurationData.java index cb3aabc4c5f..9ff45a5b305 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/ApplicationCurrencyConfigurationData.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyConfigurationData.java @@ -20,23 +20,21 @@ import java.io.Serial; import java.io.Serializable; -import java.util.Collection; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -/** - * Immutable data object for application currency. - */ - -@Getter -@RequiredArgsConstructor -public class ApplicationCurrencyConfigurationData implements Serializable { +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CurrencyConfigurationData implements Serializable { @Serial private static final long serialVersionUID = 1L; - @SuppressWarnings("unused") - private final Collection selectedCurrencyOptions; - @SuppressWarnings("unused") - private final Collection currencyOptions; + private List selectedCurrencyOptions; + private List currencyOptions; } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java index 102cf3d164b..f407bf2ae54 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java @@ -18,26 +18,29 @@ */ package org.apache.fineract.organisation.monetary.data; +import java.io.Serial; import java.io.Serializable; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -/** - * Immutable data object representing currency. - */ -@Getter -@EqualsAndHashCode +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor public class CurrencyData implements Serializable { - private final String code; - private final String name; - private final int decimalPlaces; - private final Integer inMultiplesOf; - private final String displaySymbol; - private final String nameCode; - private final String displayLabel; + @Serial + private static final long serialVersionUID = 1L; + + private String code; + private String name; + private int decimalPlaces; + private Integer inMultiplesOf; + private String displaySymbol; + private String nameCode; + private String displayLabel; public static CurrencyData blank() { return new CurrencyData("", "", 0, 0, "", ""); @@ -90,11 +93,4 @@ private String generateDisplayLabel() { return builder.toString(); } - @org.mapstruct.Mapper(config = MapstructMapperConfig.class) - public interface Mapper { - - default CurrencyData map(MonetaryCurrency source) { - return source.toData(); - } - } } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/MoneyData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateRequest.java similarity index 56% rename from fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/MoneyData.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateRequest.java index 3bca6dcd6fc..1c9cf91d95f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/MoneyData.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateRequest.java @@ -18,33 +18,26 @@ */ package org.apache.fineract.organisation.monetary.data; -import java.math.BigDecimal; - -/** - * Immutable data object representing currency. - */ -public class MoneyData { - - private final String code; - private final BigDecimal amount; - private final int decimalPlaces; - - public MoneyData(final String code, final BigDecimal amount, final int decimalPlaces) { - this.code = code; - this.amount = amount; - this.decimalPlaces = decimalPlaces; - } - - public String getCode() { - return this.code; - } - - public BigDecimal getAmount() { - return this.amount; - } - - public int getDecimalPlaces() { - return this.decimalPlaces; - } - +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CurrencyUpdateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @NotNull(message = "{org.apache.fineract.organisation.monetary.currencies.not-null}") + @NotEmpty(message = "{org.apache.fineract.organisation.monetary.currencies.not-empty}") + private List currencies; } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateResponse.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateResponse.java new file mode 100644 index 00000000000..ab914ebe5b6 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyUpdateResponse.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.organisation.monetary.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CurrencyUpdateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(example = """ + [ + "KES", + "BND", + "LBP", + "GHC", + "USD", + "XOF", + "AED", + "AMD" + ] + """) + private List currencies; + + @Deprecated(forRemoval = true) + @JsonProperty("changes") + public Map getChanges() { + // TODO: remove this one day... we should never use hashmaps in such trivial cases!!! + return Map.of("currencies", currencies); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepositoryWrapper.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepositoryWrapper.java index 43c4a276829..94303ad843c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepositoryWrapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepositoryWrapper.java @@ -51,7 +51,7 @@ public ApplicationCurrency findOneWithNotFoundDetection(final MonetaryCurrency c } final ApplicationCurrency applicationCurrency = ApplicationCurrency.from(defaultApplicationCurrency, - currency.getDigitsAfterDecimal(), currency.getCurrencyInMultiplesOf()); + currency.getDigitsAfterDecimal(), currency.getInMultiplesOf()); return applicationCurrency; } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java index dd50f7bdeca..334bcf7140d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java @@ -20,8 +20,10 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import lombok.Getter; import org.apache.fineract.organisation.monetary.data.CurrencyData; +@Getter @Embeddable public class MonetaryCurrency { @@ -74,17 +76,4 @@ public CurrencyData toData() { } return currencyData; } - - public String getCode() { - return this.code; - } - - public int getDigitsAfterDecimal() { - return this.digitsAfterDecimal; - } - - public Integer getCurrencyInMultiplesOf() { - return this.inMultiplesOf; - } - } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.java new file mode 100644 index 00000000000..2a64efa814e --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/CurrencyUpdateCommandHandler.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.organisation.monetary.handler; + +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.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; +import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CurrencyUpdateCommandHandler implements CommandHandler { + + private final CurrencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CurrencyUpdateResponse handle(final Command command) { + return writePlatformService.updateAllowedCurrencies(command.getPayload()); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/mapper/CurrencyMapper.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/mapper/CurrencyMapper.java new file mode 100644 index 00000000000..19ee2120445 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/mapper/CurrencyMapper.java @@ -0,0 +1,37 @@ +/** + * 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.organisation.monetary.mapper; + +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.mapstruct.Mapping; + +@org.mapstruct.Mapper(config = MapstructMapperConfig.class) +public interface CurrencyMapper { + + @Mapping(target = "nameCode", ignore = true) + @Mapping(target = "name", ignore = true) + @Mapping(target = "displaySymbol", ignore = true) + @Mapping(target = "displayLabel", ignore = true) + @Mapping(source = "code", target = "code") + @Mapping(source = "digitsAfterDecimal", target = "decimalPlaces") + @Mapping(source = "inMultiplesOf", target = "inMultiplesOf") + CurrencyData map(MonetaryCurrency source); +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/CurrencyCommandFromApiJsonDeserializer.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/CurrencyCommandFromApiJsonDeserializer.java deleted file mode 100644 index 0e422b312bc..00000000000 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/CurrencyCommandFromApiJsonDeserializer.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * 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.organisation.monetary.serialization; - -import com.google.gson.JsonElement; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public final class CurrencyCommandFromApiJsonDeserializer { - - public static final String CURRENCIES = "currencies"; - /** - * The parameters supported for this command. - */ - private static final Set SUPPORTED_PARAMETERS = new HashSet<>(List.of(CURRENCIES)); - - private final FromJsonHelper fromApiJsonHelper; - - @Autowired - public CurrencyCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } - - public void validateForUpdate(final String json) { - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(CURRENCIES); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - final String[] currencies = this.fromApiJsonHelper.extractArrayNamed(CURRENCIES, element); - baseDataValidator.reset().parameter(CURRENCIES).value(currencies).arrayNotEmpty(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } -} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneyDeserializer.java similarity index 94% rename from fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneyDeserializer.java index 5214d239a49..ba7a06783dd 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneyDeserializer.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.organisation.monetary.serialization.gson; +package org.apache.fineract.organisation.monetary.serialization; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; @@ -29,6 +29,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; @AllArgsConstructor +@Deprecated(forRemoval = true) public class MoneyDeserializer implements JsonDeserializer { private final MathContext mc; diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneySerializer.java similarity index 93% rename from fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneySerializer.java index b5375857ac4..af3fd0b006c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/MoneySerializer.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.organisation.monetary.serialization.gson; +package org.apache.fineract.organisation.monetary.serialization; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; @@ -25,6 +25,7 @@ import java.lang.reflect.Type; import org.apache.fineract.organisation.monetary.domain.Money; +@Deprecated(forRemoval = true) public class MoneySerializer implements JsonSerializer { @Override diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java index 51276e1c6ad..d0eb3079da9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyReadPlatformServiceImpl.java @@ -23,7 +23,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -31,15 +30,11 @@ @RequiredArgsConstructor public class CurrencyReadPlatformServiceImpl implements CurrencyReadPlatformService { - private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; - private final CurrencyMapper currencyRowMapper = new CurrencyMapper(); + private final CurrencyRowMapper currencyRowMapper = new CurrencyRowMapper(); @Override public List retrieveAllowedCurrencies() { - - this.context.authenticatedUser(); - final String sql = "select " + this.currencyRowMapper.schema() + " from m_organisation_currency c order by c.name"; return this.jdbcTemplate.query(sql, this.currencyRowMapper); // NOSONAR @@ -47,7 +42,6 @@ public List retrieveAllowedCurrencies() { @Override public List retrieveAllPlatformCurrencies() { - final String sql = "select " + this.currencyRowMapper.schema() + " from m_currency c order by c.name"; return this.jdbcTemplate.query(sql, this.currencyRowMapper); // NOSONAR @@ -55,13 +49,12 @@ public List retrieveAllPlatformCurrencies() { @Override public CurrencyData retrieveCurrency(final String code) { - final String sql = "select " + this.currencyRowMapper.schema() + " from m_currency c where c.code = ? order by c.name"; return this.jdbcTemplate.queryForObject(sql, this.currencyRowMapper, new Object[] { code }); // NOSONAR } - private static final class CurrencyMapper implements RowMapper { + private static final class CurrencyRowMapper implements RowMapper { @Override public CurrencyData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformService.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformService.java index 282fb4175e1..b75bcecd99c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformService.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformService.java @@ -18,11 +18,10 @@ */ package org.apache.fineract.organisation.monetary.service; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; public interface CurrencyWritePlatformService { - CommandProcessingResult updateAllowedCurrencies(JsonCommand command); - + CurrencyUpdateResponse updateAllowedCurrencies(CurrencyUpdateRequest request); } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformService.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformService.java index 5816bf3c3e9..2006568b477 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformService.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformService.java @@ -18,10 +18,10 @@ */ package org.apache.fineract.organisation.monetary.service; -import org.apache.fineract.organisation.monetary.data.ApplicationCurrencyConfigurationData; +import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData; public interface OrganisationCurrencyReadPlatformService { - ApplicationCurrencyConfigurationData retrieveCurrencyConfiguration(); + CurrencyConfigurationData retrieveCurrencyConfiguration(); } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformServiceImpl.java index 746f266af50..868f55377be 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/service/OrganisationCurrencyReadPlatformServiceImpl.java @@ -18,10 +18,8 @@ */ package org.apache.fineract.organisation.monetary.service; -import java.util.Collection; import lombok.RequiredArgsConstructor; -import org.apache.fineract.organisation.monetary.data.ApplicationCurrencyConfigurationData; -import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData; @RequiredArgsConstructor public class OrganisationCurrencyReadPlatformServiceImpl implements OrganisationCurrencyReadPlatformService { @@ -29,14 +27,15 @@ public class OrganisationCurrencyReadPlatformServiceImpl implements Organisation private final CurrencyReadPlatformService currencyReadPlatformService; @Override - public ApplicationCurrencyConfigurationData retrieveCurrencyConfiguration() { + public CurrencyConfigurationData retrieveCurrencyConfiguration() { - final Collection selectedCurrencyOptions = this.currencyReadPlatformService.retrieveAllowedCurrencies(); - final Collection currencyOptions = this.currencyReadPlatformService.retrieveAllPlatformCurrencies(); + final var selectedCurrencyOptions = currencyReadPlatformService.retrieveAllowedCurrencies(); + final var currencyOptions = currencyReadPlatformService.retrieveAllPlatformCurrencies(); // remove selected currency options currencyOptions.removeAll(selectedCurrencyOptions); - return new ApplicationCurrencyConfigurationData(selectedCurrencyOptions, currencyOptions); + return CurrencyConfigurationData.builder().selectedCurrencyOptions(selectedCurrencyOptions).currencyOptions(currencyOptions) + .build(); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/office/mapper/OfficeDataMapper.java b/fineract-core/src/main/java/org/apache/fineract/organisation/office/mapper/OfficeDataMapper.java index e62a2fccc74..754d7181dfa 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/office/mapper/OfficeDataMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/office/mapper/OfficeDataMapper.java @@ -33,8 +33,11 @@ default OfficeData toOfficeData(Office office) { if (hierarchy == null) { nameDecorated = ""; } else { - nameDecorated = hierarchy.substring(0, (hierarchy.length() - hierarchy.replace(".", "").length() - 1) * 4) - + Optional.ofNullable(office.getName()).orElse(""); + long count = hierarchy.chars().filter(c -> c == '.').count(); + if (count > 0) { + count--; + } + nameDecorated = "....".repeat((int) count) + Optional.ofNullable(office.getName()).orElse(""); } return new OfficeData(office.getId(), office.getName(), nameDecorated, office.getExternalId(), office.getOpeningDate(), office.getHierarchy(), office.getParent() != null ? office.getParent().getId() : null, diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index 0f44056280e..41f9a8bc304 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -222,7 +222,7 @@ private DepositsApiConstants() { interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), - SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), + SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), @@ -304,14 +304,14 @@ private static Set recurringDepositProductResponseData() { * Depost Account parameters */ - private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, - clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, - nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, - interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, - lockinPeriodFrequencyTypeParamName, chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, - depositPeriodFrequencyIdParamName, savingsAccounts, expectedFirstDepositOnDateParamName, - SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, transferToSavingsIdParamName)); + private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, clientIdParamName, groupIdParamName, + productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, nominalAnnualInterestRateParamName, + interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, + interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, + chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, depositPeriodFrequencyIdParamName, + savingsAccounts, expectedFirstDepositOnDateParamName, SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, + transferToSavingsIdParamName, linkedAccountParamName, transferInterestToSavingsParamName)); public static final Set FIXED_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = fixedDepositAccountRequestData(); public static final Set FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = fixedDepositAccountResponseData(); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index b0ac6cecde9..d074981bfa8 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -35,6 +35,7 @@ public class SavingsApiConstants { public static final String withdrawnByApplicantAction = ".withdrawnByApplicant"; public static final String activateAction = ".activate"; public static final String modifyApplicationAction = ".modify"; + public static final String undoActivateAction = ".undoactivate"; public static final String deleteApplicationAction = ".delete"; public static final String undoTransactionAction = ".undotransaction"; public static final String applyAnnualFeeTransactionAction = ".applyannualfee"; @@ -65,6 +66,7 @@ public class SavingsApiConstants { public static final String COMMAND_BLOCK_DEBIT = "blockDebit"; public static final String COMMAND_UNBLOCK_DEBIT = "unblockDebit"; public static final String COMMAND_UNBLOCK_CREDIT = "unblockCredit"; + public static final String COMMAND_ADD_ACCRUAL_TRANSACTION = "addAccrualTransactions"; // general public static final String localeParamName = "locale"; @@ -156,6 +158,7 @@ public class SavingsApiConstants { // charges parameters public static final String chargeIdParamName = "chargeId"; public static final String chargesParamName = "charges"; + public static final String accrualChargesParamName = "accrualCharges"; public static final String savingsAccountChargeIdParamName = "savingsAccountChargeId"; public static final String chargeNameParamName = "name"; public static final String penaltyParamName = "penalty"; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index e1394661cef..376e03cc015 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -144,6 +144,15 @@ public final class SavingsAccountData implements Serializable { private transient Long glAccountIdForSavingsControl; private transient Long glAccountIdForInterestOnSavings; + private transient Long glAccountIdForSavingsControlAcountPositiveInterestNegative; + private transient Long glAccountIdForInterestReceivablePositiveInterestNegative; + + private transient Long glAccountIdForOverdraftPorfolioNegative; + private transient Long glAccountIdForInterestReceivableNegative; + + private BigDecimal interestPosting; + private BigDecimal overdraftPosting; + public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate, BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum, EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum, @@ -297,6 +306,43 @@ public void setGlAccountIdForInterestOnSavings(final Long glAccountIdForInterest this.glAccountIdForInterestOnSavings = glAccountIdForInterestOnSavings; } + public Long getId() { + return id; + } + + public Long getGlAccountIdForSavingsControlAcountPositiveInterestNegative() { + return glAccountIdForSavingsControlAcountPositiveInterestNegative; + } + + public void setGlAccountIdForSavingsControlAcountPositiveInterestNegative( + Long glAccountIdForSavingsControlAcountPositiveInterestNegative) { + this.glAccountIdForSavingsControlAcountPositiveInterestNegative = glAccountIdForSavingsControlAcountPositiveInterestNegative; + } + + public Long getGlAccountIdForInterestReceivablePositiveInterestNegative() { + return glAccountIdForInterestReceivablePositiveInterestNegative; + } + + public void setGlAccountIdForInterestReceivablePositiveInterestNegative(Long glAccountIdForInterestReceivablePositiveInterestNegative) { + this.glAccountIdForInterestReceivablePositiveInterestNegative = glAccountIdForInterestReceivablePositiveInterestNegative; + } + + public Long getGlAccountIdForOverdraftPorfolioNegative() { + return glAccountIdForOverdraftPorfolioNegative; + } + + public void setGlAccountIdForOverdraftPorfolioNegative(Long glAccountIdForOverdraftPorfolioNegative) { + this.glAccountIdForOverdraftPorfolioNegative = glAccountIdForOverdraftPorfolioNegative; + } + + public Long getGlAccountIdForInterestReceivableNegative() { + return glAccountIdForInterestReceivableNegative; + } + + public void setGlAccountIdForInterestReceivableNegative(Long glAccountIdForInterestReceivableNegative) { + this.glAccountIdForInterestReceivableNegative = glAccountIdForInterestReceivableNegative; + } + public void setHelpers(final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionSummaryWrapper, final SavingsHelper savingsHelper) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; @@ -964,4 +1010,20 @@ public void setLastSavingsAccountTransaction(SavingsAccountTransactionData lastS public boolean isIsDormancyTrackingActive() { return this.isDormancyTrackingActive; } + + public BigDecimal getInterestPosting() { + return interestPosting; + } + + public void setInterestPosting(BigDecimal interestPosting) { + this.interestPosting = interestPosting; + } + + public BigDecimal getOverdraftPosting() { + return overdraftPosting; + } + + public void setOverdraftPosting(BigDecimal overdraftPosting) { + this.overdraftPosting = overdraftPosting; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java index bf1028b5fd4..6135b803ca6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java @@ -105,6 +105,9 @@ public final class SavingsAccountTransactionData implements Serializable { private BigDecimal overdraftAmount; private transient Long modifiedId; private transient String refNo; + private Boolean isNegativeBalance; + private Boolean flagValidationInterest; + private Boolean flagValidationOverdraft; private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate transactionDate, @@ -112,7 +115,7 @@ private SavingsAccountTransactionData(final Long id, final SavingsAccountTransac final boolean reversed, final AccountTransferData transfer, final Collection paymentTypeOptions, final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final String submittedByUsername, final String note, final Boolean isReversal, final Long originalTransactionId, boolean isManualTransaction, final Boolean lienTransaction, - final Long releaseTransactionId, final String reasonForBlock) { + final Long releaseTransactionId, final String reasonForBlock, final Boolean isNegativeBalance) { this.id = id; this.transactionType = transactionType; TransactionEntryType entryType = null; @@ -146,6 +149,7 @@ private SavingsAccountTransactionData(final Long id, final SavingsAccountTransac this.lienTransaction = lienTransaction; this.releaseTransactionId = releaseTransactionId; this.reasonForBlock = reasonForBlock; + this.isNegativeBalance = isNegativeBalance; } private static SavingsAccountTransactionData createData(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -156,7 +160,7 @@ private static SavingsAccountTransactionData createData(final Long id, final Sav final Boolean lienTransaction) { return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, accountId, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, paymentTypeOptions, submittedOnDate, interestedPostedAsOn, - submittedByUsername, note, null, null, false, lienTransaction, null, null); + submittedByUsername, note, null, null, false, lienTransaction, null, null, false); } public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -167,7 +171,8 @@ public static SavingsAccountTransactionData create(final Long id, final SavingsA final Boolean lienTransaction, final Long releaseTransactionId, final String reasonForBlock) { return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transfer, null, submittedOnDate, interestedPostedAsOn, - submittedByUsername, note, isReversal, originalTransactionId, false, lienTransaction, releaseTransactionId, reasonForBlock); + submittedByUsername, note, isReversal, originalTransactionId, false, lienTransaction, releaseTransactionId, reasonForBlock, + false); } public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -235,10 +240,10 @@ public static SavingsAccountTransactionData templateOnTop(final SavingsAccountTr private static SavingsAccountTransactionData createImport(final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsAccountId, final String accountNumber, final LocalDate transactionDate, final BigDecimal transactionAmount, final boolean reversed, final LocalDate submittedOnDate, - boolean isManualTransaction, final Boolean lienTransaction) { + boolean isManualTransaction, final Boolean lienTransaction, final Boolean isNegativeBalance) { SavingsAccountTransactionData data = new SavingsAccountTransactionData(null, transactionType, paymentDetailData, savingsAccountId, accountNumber, transactionDate, null, transactionAmount, null, null, reversed, null, null, submittedOnDate, false, null, - null, null, null, isManualTransaction, lienTransaction, null, null); + null, null, null, isManualTransaction, lienTransaction, null, null, isNegativeBalance); // duplicated import fields data.savingsAccountId = savingsAccountId; data.accountNumber = accountNumber; @@ -251,14 +256,14 @@ public static SavingsAccountTransactionData copyTransaction(SavingsAccountTransa return createImport(accountTransaction.getTransactionType(), accountTransaction.getPaymentDetailData(), accountTransaction.getSavingsAccountId(), null, accountTransaction.getTransactionDate(), accountTransaction.getAmount(), accountTransaction.isReversed(), accountTransaction.getSubmittedOnDate(), accountTransaction.isManualTransaction(), - accountTransaction.getLienTransaction()); + accountTransaction.getLienTransaction(), false); } public static SavingsAccountTransactionData importInstance(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, String checkNumber, String routingCode, String receiptNumber, String bankNumber, String note, Long savingsAccountId, SavingsAccountTransactionEnumData transactionType, Integer rowIndex, String locale, String dateFormat) { SavingsAccountTransactionData data = createImport(transactionType, null, savingsAccountId, accountNumber, transactionDate, - transactionAmount, false, transactionDate, false, false); + transactionAmount, false, transactionDate, false, false, false); data.rowIndex = rowIndex; data.paymentTypeId = paymentTypeId; data.checkNumber = checkNumber; @@ -272,10 +277,11 @@ public static SavingsAccountTransactionData importInstance(BigDecimal transactio } private static SavingsAccountTransactionData createImport(SavingsAccountTransactionEnumData transactionType, Long savingsAccountId, - LocalDate transactionDate, BigDecimal transactionAmount, final LocalDate submittedOnDate, boolean isManualTransaction) { + LocalDate transactionDate, BigDecimal transactionAmount, final LocalDate submittedOnDate, boolean isManualTransaction, + Boolean isNegativeBalance) { // import transaction return createImport(transactionType, null, savingsAccountId, null, transactionDate, transactionAmount, false, submittedOnDate, - isManualTransaction, false); + isManualTransaction, false, isNegativeBalance); } public static SavingsAccountTransactionData interestPosting(final SavingsAccountData savingsAccount, final LocalDate date, @@ -285,17 +291,28 @@ public static SavingsAccountTransactionData interestPosting(final SavingsAccount SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); - return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, false); + } + + public static SavingsAccountTransactionData accrual(final SavingsAccountData savingsAccount, final LocalDate date, final Money amount, + final boolean isManualTransaction) { + final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.ACCRUAL; + SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( + savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), + savingsAccountTransactionType.getValue().toString()); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, false); } public static SavingsAccountTransactionData overdraftInterest(final SavingsAccountData savingsAccount, final LocalDate date, - final Money amount, final boolean isManualTransaction) { + final Money amount, final boolean isManualTransaction, final Boolean isNegativeBalance) { final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.OVERDRAFT_INTEREST; SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); - return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction); + return createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), submittedOnDate, isManualTransaction, + isNegativeBalance); } public static SavingsAccountTransactionData withHoldTax(final SavingsAccountData savingsAccount, final LocalDate date, @@ -306,7 +323,7 @@ public static SavingsAccountTransactionData withHoldTax(final SavingsAccountData savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), savingsAccountTransactionType.getValue().toString()); SavingsAccountTransactionData accountTransaction = createImport(transactionType, savingsAccount.getId(), date, amount.getAmount(), - submittedOnDate, false); + submittedOnDate, false, false); accountTransaction.addTaxDetails(taxDetails); return accountTransaction; } @@ -323,6 +340,22 @@ public boolean isOverdraftInterestAndNotReversed() { return this.transactionType.isIncomeFromInterest() && isNotReversed(); } + public Boolean getFlagValidationInterest() { + return flagValidationInterest; + } + + public void setFlagValidationInterest(Boolean flagValidationInterest) { + this.flagValidationInterest = flagValidationInterest; + } + + public Boolean getFlagValidationOverdraft() { + return flagValidationOverdraft; + } + + public void setFlagValidationOverdraft(Boolean flagValidationOverdraft) { + this.flagValidationOverdraft = flagValidationOverdraft; + } + public boolean isCredit() { return transactionType.isCredit() && isNotReversed() && !isReversalTransaction(); } @@ -388,16 +421,28 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { Money endOfDayBalance = openingBalance.copy(); if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = openingBalance.plus(getAmount()); + endOfDayBalance = Money.of(currency, this.runningBalance); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - - if (openingBalance.isGreaterThanZero()) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (openingBalance.isGreaterThanZero()) { endOfDayBalance = openingBalance.minus(getAmount()); } else { endOfDayBalance = Money.of(currency, this.runningBalance); } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); + } + + public EndOfDayBalance toEndOfDayBalanceDates(final Money openingBalance, LocalDateInterval date) { + final MonetaryCurrency currency = openingBalance.getCurrency(); + Money endOfDayBalance = Money.of(currency, this.runningBalance); + + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, + this.balanceNumberOfDays != null ? this.balanceNumberOfDays : date.endDate().getDayOfMonth(), + currency.getDigitsAfterDecimal()); } public boolean isChargeTransactionAndNotReversed() { @@ -427,7 +472,9 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = endOfDayBalance.plus(getAmount()); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - if (endOfDayBalance.isGreaterThanZero() || isAllowOverdraft) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (endOfDayBalance.isGreaterThanZero() || isAllowOverdraft) { endOfDayBalance = endOfDayBalance.minus(getAmount()); } else { endOfDayBalance = Money.of(currency, this.runningBalance); @@ -441,7 +488,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } public void reverse() { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java index bed16288e09..fc90c1f6289 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java @@ -75,6 +75,7 @@ public final class SavingsProductData implements Serializable { // charges private final Collection charges; + private final Collection accrualCharges; // template private final Collection currencyOptions; @@ -120,6 +121,7 @@ public static SavingsProductData template(final CurrencyData currency, final Enu final Map accountingMappings = null; final Collection paymentChannelToFundSourceMappings = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection feeToIncomeAccountMappings = null; final Collection penaltyToIncomeAccountMappings = null; final boolean allowOverdraft = false; @@ -147,10 +149,11 @@ public static SavingsProductData template(final CurrencyData currency, final Enu penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } - public static SavingsProductData withCharges(final SavingsProductData product, final Collection charges) { + public static SavingsProductData withCharges(final SavingsProductData product, final Collection charges, + final Collection accrualCharges) { return new SavingsProductData(product.id, product.name, product.shortName, product.description, product.currency, product.nominalAnnualInterestRate, product.interestCompoundingPeriodType, product.interestPostingPeriodType, product.interestCalculationType, product.interestCalculationDaysInYearType, product.minRequiredOpeningBalance, @@ -165,7 +168,7 @@ public static SavingsProductData withCharges(final SavingsProductData product, f product.minBalanceForInterestCalculation, product.nominalAnnualInterestRateOverdraft, product.minOverdraftForInterestCalculation, product.withHoldTax, product.taxGroup, product.taxGroupOptions, product.isDormancyTrackingActive, product.daysToInactive, product.daysToDormancy, product.daysToEscheat, - product.accountMappingForPayment); + product.accountMappingForPayment, accrualCharges); } /** @@ -200,7 +203,8 @@ public static SavingsProductData withTemplate(final SavingsProductData existingP existingProduct.maxAllowedLienLimit, existingProduct.lienAllowed, existingProduct.minBalanceForInterestCalculation, existingProduct.nominalAnnualInterestRateOverdraft, existingProduct.minOverdraftForInterestCalculation, existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions, existingProduct.isDormancyTrackingActive, - existingProduct.daysToInactive, existingProduct.daysToDormancy, existingProduct.daysToEscheat, accountMappingForPayment); + existingProduct.daysToInactive, existingProduct.daysToDormancy, existingProduct.daysToEscheat, accountMappingForPayment, + existingProduct.accrualCharges); } public static SavingsProductData withAccountingDetails(final SavingsProductData existingProduct, @@ -237,7 +241,7 @@ public static SavingsProductData withAccountingDetails(final SavingsProductData existingProduct.nominalAnnualInterestRateOverdraft, existingProduct.minOverdraftForInterestCalculation, existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions, existingProduct.isDormancyTrackingActive, existingProduct.daysToInactive, existingProduct.daysToDormancy, - existingProduct.daysToEscheat, existingProduct.accountMappingForPayment); + existingProduct.daysToEscheat, existingProduct.accountMappingForPayment, existingProduct.accrualCharges); } public static SavingsProductData instance(final Long id, final String name, final String shortName, final String description, @@ -268,6 +272,7 @@ public static SavingsProductData instance(final Long id, final String name, fina final Collection chargeOptions = null; final Collection penaltyOptions = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection feeToIncomeAccountMappings = null; final Collection penaltyToIncomeAccountMappings = null; final Collection taxGroupOptions = null; @@ -282,7 +287,7 @@ public static SavingsProductData instance(final Long id, final String name, fina penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } public static SavingsProductData lookup(final Long id, final String name) { @@ -325,6 +330,7 @@ public static SavingsProductData lookup(final Long id, final String name) { final Collection accountingRuleOptions = null; final Map> accountingMappingOptions = null; final Collection charges = null; + final Collection accrualCharges = null; final Collection chargeOptions = null; final Collection penaltyOptions = null; final Collection feeToIncomeAccountMappings = null; @@ -345,16 +351,17 @@ public static SavingsProductData lookup(final Long id, final String name) { penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, allowOverdraft, overdraftLimit, minRequiredBalance, enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroup, taxGroupOptions, - isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment); + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accountMappingForPayment, accrualCharges); } - public static SavingsProductData createForInterestPosting(final Long id, final EnumOptionData accountingRule) { - return new SavingsProductData(id, accountingRule); + public static SavingsProductData createForInterestPosting(final Long id, final String productName, + final EnumOptionData accountingRule) { + return new SavingsProductData(id, productName, accountingRule); } - private SavingsProductData(final Long id, final EnumOptionData accountingRule) { + private SavingsProductData(final Long id, final String productName, final EnumOptionData accountingRule) { this.id = id; - this.name = null; + this.name = productName; this.shortName = null; this.description = null; this.currency = null; @@ -386,6 +393,7 @@ private SavingsProductData(final Long id, final EnumOptionData accountingRule) { this.charges = null;// charges associated with Savings product this.chargeOptions = null;// charges available for adding to + this.accrualCharges = null; // Savings product this.penaltyOptions = null;// penalties available for adding // to Savings product @@ -433,7 +441,7 @@ private SavingsProductData(final Long id, final String name, final String shortN final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, final boolean withHoldTax, final TaxGroupData taxGroup, final Collection taxGroupOptions, final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, - final String accountMappingForPayment) { + final String accountMappingForPayment, final Collection accrualCharges) { this.id = id; this.name = name; this.shortName = shortName; @@ -469,6 +477,7 @@ private SavingsProductData(final Long id, final String name, final String shortN this.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; this.charges = charges;// charges associated with Savings product + this.accrualCharges = accrualCharges; this.chargeOptions = chargeOptions;// charges available for adding to // Savings product this.penaltyOptions = penaltyOptions;// penalties available for adding @@ -594,7 +603,7 @@ public boolean isCashBasedAccountingEnabled() { } public boolean isAccrualBasedAccountingEnabled() { - return isUpfrontAccrualAccounting() || isPeriodicAccrualAccounting(); + return isUpfrontAccrualAccounting() && isPeriodicAccrualAccounting(); } public boolean isUpfrontAccrualAccounting() { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java index 45ce4b6e0fa..5501286deb3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/AnnualCompoundingPeriod.java @@ -47,7 +47,7 @@ public static AnnualCompoundingPeriod create(final LocalDateInterval periodInter public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java index 20ad55506f6..87044aee5ae 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/BiAnnualCompoundingPeriod.java @@ -47,7 +47,7 @@ public static BiAnnualCompoundingPeriod create(final LocalDateInterval periodInt public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java index 4b918c0198a..f7eca8f4f8c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestHelper.java @@ -47,9 +47,14 @@ public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency curren // total interest earned in previous periods but not yet recognised BigDecimal compoundedInterest = BigDecimal.ZERO; BigDecimal unCompoundedInterest = BigDecimal.ZERO; + LocalDate endDay = DateUtils.getBusinessLocalDate(); final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest); for (final PostingPeriod postingPeriod : allPeriods) { + if (postingPeriod.dateOfPostingTransaction().getMonth() != endDay.getMonth()) { + compoundInterestValues.setCompoundedInterest(interestEarned.getAmount()); + } + final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues); final Money moneyToBePostedForPeriod = Money.of(currency, interestEarnedThisPeriod); @@ -61,8 +66,10 @@ public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency curren // calculation. if (!(postingPeriod.isInterestTransfered() || !interestTransferEnabled || (lockUntil != null && !DateUtils.isAfter(postingPeriod.dateOfPostingTransaction(), lockUntil)))) { - compoundInterestValues.setcompoundedInterest(BigDecimal.ZERO); + compoundInterestValues.setCompoundedInterest(BigDecimal.ZERO); } + endDay = postingPeriod.dateOfPostingTransaction(); + } return interestEarned; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java index 09a871e8db0..68cfc07cd77 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundInterestValues.java @@ -38,7 +38,7 @@ public BigDecimal getuncompoundedInterest() { return this.uncompoundedInterest; } - public void setcompoundedInterest(BigDecimal interestToBeCompounded) { + public void setCompoundedInterest(BigDecimal interestToBeCompounded) { this.compoundedInterest = interestToBeCompounded; } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java index 206341710e4..555a513654d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/CompoundingPeriod.java @@ -28,7 +28,7 @@ public interface CompoundingPeriod { BigDecimal calculateInterest(SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, SavingsInterestCalculationType interestCalculationType, BigDecimal interestFromPreviousPostingPeriod, BigDecimal interestRateAsFraction, long daysInYear, BigDecimal minBalanceForInterestCalculation, - BigDecimal overdraftInterestRateAsFraction, BigDecimal minOverdraftForInterestCalculation); + BigDecimal overdraftInterestRateAsFraction, BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual); LocalDateInterval getPeriodInterval(); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java index c46b5518a94..9f198654081 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/DailyCompoundingPeriod.java @@ -77,7 +77,7 @@ private DailyCompoundingPeriod(final LocalDateInterval periodInterval, final Lis public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestFromPreviousPostingPeriod, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; // for daily compounding - each interest calculated from previous daily diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java index ce821427c42..3b435023c3c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/EndOfDayBalance.java @@ -32,17 +32,20 @@ public class EndOfDayBalance { private final Money openingBalance; private final Money endOfDayBalance; private final int numberOfDays; + private int decimals; public static EndOfDayBalance from(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, - final int numberOfDays) { - return new EndOfDayBalance(date, openingBalance, endOfDayBalance, numberOfDays); + final int numberOfDays, final int decimals) { + return new EndOfDayBalance(date, openingBalance, endOfDayBalance, numberOfDays, decimals); } - public EndOfDayBalance(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, final int numberOfDays) { + public EndOfDayBalance(final LocalDate date, final Money openingBalance, final Money endOfDayBalance, final int numberOfDays, + final int decimals) { this.date = date; this.openingBalance = openingBalance; this.endOfDayBalance = endOfDayBalance; this.numberOfDays = numberOfDays; + this.decimals = decimals; } public LocalDate date() { @@ -60,11 +63,15 @@ public BigDecimal cumulativeBalance(final BigDecimal interestToCompound) { MoneyHelper.getRoundingMode()); } + public void setDecimals(int decimals) { + this.decimals = decimals; + } + public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { - BigDecimal interest = BigDecimal.ZERO.setScale(9, MoneyHelper.getRoundingMode()); + BigDecimal interest = BigDecimal.ZERO.setScale(this.decimals, MoneyHelper.getRoundingMode()); final BigDecimal realBalanceForInterestCalculation = this.endOfDayBalance.getAmount().add(interestToCompound); if (realBalanceForInterestCalculation.compareTo(BigDecimal.ZERO) >= 0) { if (realBalanceForInterestCalculation.compareTo(minBalanceForInterestCalculation) >= 0) { @@ -72,7 +79,7 @@ public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound final BigDecimal dailyInterestRate = interestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), MathContext.DECIMAL64); - interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(9, + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, MoneyHelper.getRoundingMode()); } } else { @@ -81,9 +88,40 @@ public BigDecimal calculateInterestOnBalance(final BigDecimal interestToCompound final BigDecimal dailyInterestRate = overdraftInterestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), MathContext.DECIMAL64); - interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(9, + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, + MoneyHelper.getRoundingMode()); + } + } + return interest; + } + + public BigDecimal calculateInterestOnBalanceNegative(final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, + final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, + final BigDecimal minOverdraftForInterestCalculation) { + + BigDecimal interest = BigDecimal.ZERO.setScale(this.decimals, MoneyHelper.getRoundingMode()); + final BigDecimal realBalanceForInterestCalculation = this.endOfDayBalance.getAmount().add(interestToCompound); + if (realBalanceForInterestCalculation.compareTo(BigDecimal.ZERO) >= 0) { + if (realBalanceForInterestCalculation.compareTo(minBalanceForInterestCalculation) >= 0) { + final BigDecimal multiplicand = BigDecimal.ONE.divide(BigDecimal.valueOf(daysInYear), MathContext.DECIMAL64); + final BigDecimal dailyInterestRate = interestRateAsFraction.multiply(multiplicand, MathContext.DECIMAL64); + final BigDecimal periodicInterestRate = dailyInterestRate.multiply(BigDecimal.valueOf(this.numberOfDays), + MathContext.DECIMAL64); + interest = realBalanceForInterestCalculation.multiply(periodicInterestRate, MathContext.DECIMAL64).setScale(this.decimals, MoneyHelper.getRoundingMode()); } + } else { + if (realBalanceForInterestCalculation.compareTo(minOverdraftForInterestCalculation.negate()) < 0) { + final BigDecimal balanceConvertPositive = realBalanceForInterestCalculation.abs(); + final BigDecimal porcentaje = overdraftInterestRateAsFraction.divide(BigDecimal.valueOf(100)); + final BigDecimal forDaysInYear = porcentaje.divide(BigDecimal.valueOf(daysInYear), MathContext.DECIMAL64); + final BigDecimal fordays = forDaysInYear.multiply(BigDecimal.valueOf(this.numberOfDays == 0 ? 1 : this.numberOfDays)); + + final BigDecimal calculationInteresNegative = fordays.multiply(balanceConvertPositive); + + interest = calculationInteresNegative.setScale(this.decimals, MoneyHelper.getRoundingMode()); + + } } return interest; } @@ -163,7 +201,7 @@ public EndOfDayBalance upTo(final LocalDateInterval compoundingPeriodInterval, f daysOfBalance = balancePeriodInterval.daysInPeriodInclusiveOfEndDate(); } - return new EndOfDayBalance(balanceStartDate, startingBalance, this.endOfDayBalance, daysOfBalance); + return new EndOfDayBalance(balanceStartDate, startingBalance, this.endOfDayBalance, daysOfBalance, this.decimals); } public boolean contains(final LocalDateInterval compoundingPeriodInterval) { diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java index 7d406d026dd..6b42ebebc7e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/MonthlyCompoundingPeriod.java @@ -47,14 +47,15 @@ public static MonthlyCompoundingPeriod create(final LocalDateInterval periodInte public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; switch (interestCalculationType) { case DAILY_BALANCE: interestEarned = calculateUsingDailyBalanceMethod(compoundingInterestPeriodType, interestToCompound, interestRateAsFraction, - daysInYear, minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + daysInYear, minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isAccrual); break; case AVERAGE_DAILY_BALANCE: interestEarned = calculateUsingAverageDailyBalanceMethod(interestToCompound, interestRateAsFraction, daysInYear, @@ -114,7 +115,7 @@ private BigDecimal calculateUsingAverageDailyBalanceMethod(final BigDecimal inte private BigDecimal calculateUsingDailyBalanceMethod(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction, - final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; BigDecimal interestOnBalanceUnrounded = BigDecimal.ZERO; @@ -127,8 +128,15 @@ private BigDecimal calculateUsingDailyBalanceMethod(final SavingsCompoundingInte minOverdraftForInterestCalculation); break; case MONTHLY: - interestOnBalanceUnrounded = balance.calculateInterestOnBalance(interestToCompound, interestRateAsFraction, daysInYear, - minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + interestOnBalanceUnrounded = isAccrual + ? balance.calculateInterestOnBalanceNegative(interestToCompound, interestRateAsFraction, daysInYear, + minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation) + : balance.calculateInterestOnBalance(interestToCompound, interestRateAsFraction, daysInYear, + minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); + if (isAccrual && balance.getNumberOfDays() == 0) { + interestOnBalanceUnrounded = BigDecimal.ZERO; + } + break; // case QUATERLY: // break; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java index f0bce2b1277..4e6e1a0e50d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java @@ -26,14 +26,17 @@ import java.util.Collection; import java.util.List; import java.util.TreeSet; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +@Slf4j public final class PostingPeriod { private final LocalDateInterval periodInterval; @@ -64,6 +67,26 @@ public final class PostingPeriod { private Integer financialYearBeginningMonth; + private Boolean isAccrual = false; + private Boolean isNegative = false; + private Boolean isEndTransaction = false; + + public void setAccrual(Boolean accrual) { + isAccrual = accrual; + } + + public Boolean getNegative() { + return isNegative; + } + + public void setNegative(Boolean negative) { + isNegative = negative; + } + + public void setOverdraftInterestRateAsFraction(BigDecimal overdraftInterestRateAsFraction) { + this.overdraftInterestRateAsFraction = overdraftInterestRateAsFraction; + } + public static PostingPeriod createFrom(final LocalDateInterval periodInterval, final Money periodStartingBalance, final List orderedListOfTransactions, final MonetaryCurrency currency, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, @@ -104,12 +127,14 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f // period so no need to do any cropping/bounding final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); openingDayBalance = endOfDayBalance.closingBalance(); } else if (transaction.spansAnyPortionOf(periodInterval)) { final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); closeOfDayBalance = endOfDayBalance.closingBalance(); openingDayBalance = closeOfDayBalance; @@ -139,7 +164,7 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f } final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance, - numberOfDaysOfBalance); + numberOfDaysOfBalance, currency.getDigitsAfterDecimal()); accountEndOfDayBalances.add(endOfDayBalance); @@ -153,7 +178,7 @@ public static PostingPeriod createFrom(final LocalDateInterval periodInterval, f return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, - minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth, false); } public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval, final Money periodStartingBalance, @@ -163,12 +188,13 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval final LocalDate upToInterestCalculationDate, Collection interestPostTransactions, boolean isInterestTransfer, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation, boolean isUserPosting, - int financialYearBeginningMonth, final boolean isAllowOverdraft) { + int financialYearBeginningMonth, final boolean isAllowOverdraft, final boolean isEntraceNewValidation) { final List accountEndOfDayBalances = new ArrayList<>(); boolean interestTransfered = false; Money openingDayBalance = periodStartingBalance; Money closeOfDayBalance = openingDayBalance; + Boolean isEndTransaction = false; for (final SavingsAccountTransactionData transaction : orderedListOfTransactions) { @@ -177,6 +203,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval // period so no need to do any cropping/bounding final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); openingDayBalance = endOfDayBalance.closingBalance(); @@ -184,9 +211,17 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval, isAllowOverdraft); accountEndOfDayBalances.add(endOfDayBalance); + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); closeOfDayBalance = endOfDayBalance.closingBalance(); openingDayBalance = closeOfDayBalance; + } else if (!isEntraceNewValidation && !isEndTransaction && MathUtil.isLessThanZero(transaction.getRunningBalance()) + && DateUtils.isEqual(periodInterval.startDate(), transaction.getDate())) { + final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceDates(openingDayBalance, periodInterval); + accountEndOfDayBalances.add(endOfDayBalance); + openingDayBalance = endOfDayBalance.closingBalance(); + isEndTransaction = true; + endOfDayBalance.setDecimals(currency.getDigitsAfterDecimal()); } // this check is to make sure to add interest if withdrawal is @@ -213,7 +248,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval } final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance, - numberOfDaysOfBalance); + numberOfDaysOfBalance, currency.getDigitsAfterDecimal()); accountEndOfDayBalances.add(endOfDayBalance); @@ -227,7 +262,7 @@ public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, - minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth, isEndTransaction); } private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurrency currency, final Money openingBalance, @@ -235,7 +270,8 @@ private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurr final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, final List compoundingPeriods, boolean interestTransfered, final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final BigDecimal overdraftInterestRateAsFraction, - final Money minOverdraftForInterestCalculation, boolean isUserPosting, Integer financialYearBeginningMonth) { + final Money minOverdraftForInterestCalculation, boolean isUserPosting, Integer financialYearBeginningMonth, + Boolean isEndTransaction) { this.periodInterval = periodInterval; this.currency = currency; this.openingBalance = openingBalance; @@ -257,6 +293,7 @@ private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurr this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; this.isUserPosting = isUserPosting; this.financialYearBeginningMonth = financialYearBeginningMonth; + this.isEndTransaction = isEndTransaction; } public Money interest() { @@ -282,10 +319,11 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres // to be applied to the balanced for interest calculation for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) { + boolean isAccrual = this.isAccrual; final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType, this.interestCalculationType, compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction, this.daysInYear, this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction, - this.minOverdraftForInterestCalculation.getAmount()); + this.minOverdraftForInterestCalculation.getAmount(), isAccrual); BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded); compoundInterestValues.setuncompoundedInterest(unCompoundedInterest); LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate(); @@ -297,7 +335,12 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres if (compoundingPeriodEndDate.equals(compoundingPeriod.getPeriodInterval().endDate())) { BigDecimal interestCompounded = compoundInterestValues.getcompoundedInterest().add(unCompoundedInterest); - compoundInterestValues.setcompoundedInterest(interestCompounded); + if (isNegative) { + compoundInterestValues.setCompoundedInterest(interestCompounded.negate()); + } else { + compoundInterestValues.setCompoundedInterest(interestCompounded); + } + compoundInterestValues.setZeroForInterestToBeUncompounded(); } interestEarned = interestEarned.add(interestUnrounded); @@ -310,7 +353,7 @@ public BigDecimal calculateInterest(final CompoundInterestValues compoundInteres } public Money getInterestEarned() { - return this.interestEarnedRounded; + return this.interestEarnedRounded != null ? this.interestEarnedRounded : Money.zero(this.currency); } private static List compoundingPeriodsInPostingPeriod(final LocalDateInterval postingPeriodInterval, @@ -545,4 +588,19 @@ public Integer getFinancialYearBeginningMonth() { return this.financialYearBeginningMonth; } + public List getCompoundingPeriods() { + return compoundingPeriods; + } + + public Money getClosingBalance() { + return closingBalance; + } + + public Money getOpeningBalance() { + return openingBalance; + } + + public Boolean getEndTransaction() { + return isEndTransaction; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java index c2faa8a4711..81f0983918e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/QuarterlyCompoundingPeriod.java @@ -46,7 +46,7 @@ public static QuarterlyCompoundingPeriod create(final LocalDateInterval periodIn public BigDecimal calculateInterest(final SavingsCompoundingInterestPeriodType compoundingInterestPeriodType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestToCompound, final BigDecimal interestRateAsFraction, final long daysInYear, final BigDecimal minBalanceForInterestCalculation, - final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation) { + final BigDecimal overdraftInterestRateAsFraction, final BigDecimal minOverdraftForInterestCalculation, Boolean isAccrual) { BigDecimal interestEarned = BigDecimal.ZERO; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java index 3a3145b377f..bf3ce6f16a7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/SavingsAccountTransactionDetailsForPostingPeriod.java @@ -64,15 +64,17 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { if (isDeposit() || isDividendPayoutAndNotReversed()) { endOfDayBalance = openingBalance.plus(getAmount(currency)); } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { - - if (openingBalance.isGreaterThanZero() || isAllowOverdraft()) { + if (isWithdrawal()) { + endOfDayBalance = Money.of(currency, this.runningBalance); + } else if (openingBalance.isGreaterThanZero() || isAllowOverdraft()) { endOfDayBalance = openingBalance.minus(getAmount(currency)); } else { endOfDayBalance = Money.of(currency, this.runningBalance); } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, final MonetaryCurrency currency) { @@ -89,7 +91,7 @@ public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, numberOfDays = newInterval.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, final LocalDateInterval boundedBy) { @@ -127,7 +129,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } private Money getAmount(MonetaryCurrency currency) { 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 c724d2ada58..62401063734 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 @@ -137,6 +137,7 @@ public enum DefaultLoanProduct implements LoanProduct { LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_PERCENTAGE_CAPITALIZED_INCOME, // LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, // LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC, // + LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, // ; @Override diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/SavingsProductRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/SavingsProductRequestFactory.java index 54baa519bfb..cfa3914386a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/SavingsProductRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/SavingsProductRequestFactory.java @@ -18,8 +18,8 @@ */ package org.apache.fineract.test.factory; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; import org.apache.fineract.client.models.PostSavingsCharges; import org.apache.fineract.client.models.PostSavingsProductsRequest; @@ -43,7 +43,7 @@ public final class SavingsProductRequestFactory { private SavingsProductRequestFactory() {} public static PostSavingsProductsRequest defaultSavingsProductRequest() { - Set charges = new HashSet<>(); + List charges = new ArrayList<>(); return new PostSavingsProductsRequest().name(DEFAULT_SAVINGS_PRODUCT_NAME)// .shortName(DEFAULT_SAVINGS_PRODUCT_SHORT_NAME)// diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CurrencyGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CurrencyGlobalInitializerStep.java index 3dbbba0d11c..ad8a2d52bf1 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CurrencyGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CurrencyGlobalInitializerStep.java @@ -21,15 +21,13 @@ import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; -import org.apache.fineract.client.models.CurrencyRequest; -import org.apache.fineract.client.models.PutCurrenciesResponse; +import org.apache.fineract.client.models.CurrencyUpdateRequest; import org.apache.fineract.client.services.CurrencyApi; import org.apache.fineract.test.support.TestContext; import org.apache.fineract.test.support.TestContextKey; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import retrofit2.Response; @RequiredArgsConstructor @Component @@ -42,9 +40,8 @@ public class CurrencyGlobalInitializerStep implements FineractGlobalInitializerS @Override public void initialize() throws Exception { - CurrencyRequest currencyRequest = new CurrencyRequest(); - Response putCurrenciesResponse = currencyApi.updateCurrencies(currencyRequest.currencies(CURRENCIES)) - .execute(); - TestContext.INSTANCE.set(TestContextKey.PUT_CURRENCIES_RESPONSE, putCurrenciesResponse); + var request = new CurrencyUpdateRequest(); + var response = currencyApi.updateCurrencies(request.currencies(CURRENCIES)).execute(); + TestContext.INSTANCE.set(TestContextKey.PUT_CURRENCIES_RESPONSE, response); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 77d96629842..d1939eab954 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -2585,7 +2585,8 @@ public void initialize() throws Exception { .name(name104)// .enableDownPayment(true)// .disbursedAmountPercentageForDownPayment(new BigDecimal(25))// - .enableAutoRepaymentForDownPayment(true).daysInYearType(DaysInYearType.DAYS360.value)// + .enableAutoRepaymentForDownPayment(true)// + .daysInYearType(DaysInYearType.DAYS360.value)// .daysInMonthType(DaysInMonthType.DAYS30.value)// .isInterestRecalculationEnabled(true)// .preClosureInterestCalculationStrategy(1)// @@ -2936,6 +2937,7 @@ public void initialize() throws Exception { TestContext.INSTANCE.set( TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcApprovedOverAppliedFlatCapitalizedIncome); + // LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 // + interest recalculation=false, buy down fees enabled final String name114 = DefaultLoanProduct.LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES.getName(); @@ -2953,6 +2955,27 @@ public void initialize() throws Exception { .createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentBuyDownFees).execute(); TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_BUYDOWN_FEES, responseLoanProductsRequestLP2ProgressiveAdvPaymentBuyDownFees); + + // LP2 + interest recalculation + zero-interest chargeOff behaviour + progressive loan schedule + horizontal + // (LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY) + final String name115 = DefaultLoanProduct.LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY + .getName(); + final PostLoanProductsRequest loanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity = loanProductsRequestFactory + .defaultLoanProductsRequestLP2InterestDailyRecalculation()// + .name(name115)// + .enableDownPayment(true)// + .disbursedAmountPercentageForDownPayment(new BigDecimal(25))// + .enableAutoRepaymentForDownPayment(true)// + .paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"))) + .enableAccrualActivityPosting(true)// + .chargeOffBehaviour("ZERO_INTEREST");// + final Response responseLoanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity = loanProductsApi + .createLoanProduct(loanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity) + .execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, + responseLoanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, 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 e8e26533c66..26c9e271943 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) { .pollInterval(Duration.ofMillis(jobPollingProperties.getIntervalInMillis())) // .pollDelay(Duration.ofMillis(jobPollingProperties.getDelayInMillis())) // .until(() -> { - log.info("Waiting for job {} to finish", jobName); + log.debug("Waiting for job {} to finish", jobName); Long jobId = jobResolver.resolve(job); Response getJobsResponse = schedulerJobApi.retrieveOne5(jobId).execute(); ErrorHelper.checkSuccessfulApiCall(getJobsResponse); 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 a0c25219bf7..c171b40ec8a 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 @@ -947,10 +947,10 @@ public void runBatchApiCallWithChargeOffCommand(String chargeOffDate) throws IOE if (batchResponseList.isSuccessful() && batchResponseList.body() != null && !batchResponseList.body().isEmpty()) { BatchResponse response = batchResponseList.body().get(0); - log.info("Batch charge-off API status code: {}", response.getStatusCode()); - log.info("Batch charge-off API response body: {}", response.getBody()); + log.debug("Batch charge-off API status code: {}", response.getStatusCode()); + log.debug("Batch charge-off API response body: {}", response.getBody()); } else { - log.info("Batch charge-off API call failed or returned empty response"); + log.warn("Batch charge-off API call failed or returned empty response"); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 18c2d9c4e87..19f70a561f5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -4029,7 +4029,7 @@ public void transactionsExcluded(String excludedTypes) throws IOException { ErrorHelper.checkSuccessfulApiCall(transactionsByLoanIdFiltered); List transactions = transactionsByLoanIdFiltered.body().getContent(); - log.info("Filtered transactions: {}", transactions); + log.debug("Filtered transactions: {}", transactions); List excludedTypesList = Arrays.stream(excludedTypes.toLowerCase().split(",")).map(String::trim) .collect(Collectors.toList()); @@ -4052,7 +4052,7 @@ public void transactionsExcludedByExternalId(String excludedTypes) throws IOExce ErrorHelper.checkSuccessfulApiCall(transactionsByLoanExternalIdFiltered); List transactions = transactionsByLoanExternalIdFiltered.body().getContent(); - log.info("Filtered transactions: {}", transactions); + log.debug("Filtered transactions: {}", transactions); List excludedTypesList = Arrays.stream(excludedTypes.toLowerCase().split(",")).map(String::trim) .collect(Collectors.toList()); @@ -4514,7 +4514,7 @@ public void adminAddsCapitalizedIncomeAdjustmentToTheLoan(final String transacti // Get current business date to ensure we're not creating backdated transactions String currentBusinessDate = businessDateHelper.getBusinessDate(); - log.info("Current business date: {}, Transaction date: {}", currentBusinessDate, transactionDate); + log.debug("Current business date: {}, Transaction date: {}", currentBusinessDate, transactionDate); final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); @@ -4530,7 +4530,7 @@ public void adminAddsCapitalizedIncomeAdjustmentToTheLoan(final String transacti testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_ADJUSTMENT_RESPONSE, adjustmentResponse); ErrorHelper.checkSuccessfulApiCall(adjustmentResponse); - log.info("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); + log.debug("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); } @And("Admin adds capitalized income adjustment with {string} payment type to the loan on {string} with {string} EUR trn amount with {string} date for capitalized income") @@ -4554,7 +4554,7 @@ public void adminAddsCapitalizedIncomeAdjustmentToTheLoanWithCapitalizedIncomeDa testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_ADJUSTMENT_RESPONSE, adjustmentResponse); ErrorHelper.checkSuccessfulApiCall(adjustmentResponse); - log.info("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); + log.debug("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); } @And("Admin adds invalid capitalized income adjustment with {string} payment type to the loan on {string} with {string} EUR transaction amount") @@ -4565,7 +4565,7 @@ public void adminAddsArbitraryCapitalizedIncomeAdjustmentToTheLoan(final String // Get current business date to ensure we're not creating backdated transactions String currentBusinessDate = businessDateHelper.getBusinessDate(); - log.info("Current business date: {}, Transaction date: {}", currentBusinessDate, transactionDate); + log.debug("Current business date: {}, Transaction date: {}", currentBusinessDate, transactionDate); final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 7cff8fc6e03..3ccde040b98 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -160,6 +160,7 @@ public abstract class TestContextKey { public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSAL_CAPITALIZED_INCOME = "loanProductCreateResponseLP2ProgressiveAdvPayment36030InterestRecalcMultidisbursalCapitalizedIncome"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ZERO_INT_CHARGE_OFF_DELINQUENT_REASON_INT_RECALC_CAPITALIZED_INCOME = "loanProductCreateResponseLP2AdvancedPaymentZeroInterestChargeOffDelinquentReasonInterestRecalculationCapitalizedIncome"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationZeroInterestChargeOffBehaviourAccrualActivity"; + public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity"; public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst"; public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy"; diff --git a/fineract-e2e-tests-runner/build.gradle b/fineract-e2e-tests-runner/build.gradle index 9f233ccf719..ccc64f51d2a 100644 --- a/fineract-e2e-tests-runner/build.gradle +++ b/fineract-e2e-tests-runner/build.gradle @@ -81,13 +81,23 @@ tasks.named('cucumber').get().finalizedBy 'allureReport' tasks.named('cucumber').get().dependsOn 'spotlessCheck' cucumber { - tags = 'not @ignore' - main = 'io.cucumber.core.cli.Main' - shorten = 'argfile' - plugin = [ - 'pretty', - 'io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm' - ] + // Use -Pcucumber.features=... if passed, otherwise default + if (project.hasProperty("cucumber.features")) { + featurePath = project.getProperty("cucumber.features") + } else { + featurePath = "src/test/resources/features"; + } + + tags = 'not @Skip' + if (project.hasProperty("cucumber.tags")) { + tags = [project.getProperty("cucumber.tags")] + } + + if (project.hasProperty("cucumber.name")) { + name = project.getProperty("cucumber.name") + } + + plugin = ['pretty', 'io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm'] } allure { diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature index d8026aee3c9..3ec5e65334b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature @@ -7556,4 +7556,228 @@ Feature: LoanAccrualActivity And Customer makes "AUTOPAY" repayment on "26 May 2025" with 107.75 EUR transaction amount Then Loan status will be "CLOSED_OBLIGATIONS_MET" Then Loan has 0 outstanding amount - + + @TestRailId:C3802 + Scenario: Correct Accrual Activity event publishing for backdated loans when the loan re-opens after reversing a goodwill credit transaction - UC1 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_REFUND_INTEREST_RECALC_DOWNPAYMENT_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + When Customer makes "GOODWILL_CREDIT" transaction with "AUTOPAY" payment type on "19 May 2023" with 270.85 EUR transaction amount and system-generated Idempotency key + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Goodwill Credit | 270.85 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + @TestRailId:C3803 + Scenario: Correct Accrual Activity event publishing for backdated loans when the overpaid loan re-opens after reversing a goodwill credit transaction - UC2 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_REFUND_INTEREST_RECALC_DOWNPAYMENT_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + When Customer makes "GOODWILL_CREDIT" transaction with "AUTOPAY" payment type on "19 May 2023" with 359.79 EUR transaction amount and system-generated Idempotency key + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "OVERPAID" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Goodwill Credit | 359.79 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + @TestRailId:C3805 + Scenario: Correct Accrual Activity event publishing for backdated loans when the loan re-opens after reversing a payout refund transaction - UC3 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + And Admin makes "PAYOUT_REFUND" transaction with "AUTOPAY" payment type on "19 May 2023" with 270.85 EUR transaction amount + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Payout Refund | 270.85 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + @TestRailId:C3806 + Scenario: Correct Accrual Activity event publishing for backdated loans when the loan re-opens after reversing a merchant issue refund transaction - UC4 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_REFUND_INTEREST_RECALC_DOWNPAYMENT_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + When Admin makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "19 May 2023" with 359.79 EUR transaction amount + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "OVERPAID" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Merchant Issued Refund | 359.79 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 19 May 2023 | Interest Refund | 1.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + @TestRailId:C3807 + Scenario: Correct Accrual Activity event publishing for backdated loans when the loan re-opens after reversing a interest payment waiver transaction - UC5 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_REFUND_INTEREST_RECALC_DOWNPAYMENT_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + When Admin makes "INTEREST_PAYMENT_WAIVER" transaction with "AUTOPAY" payment type on "19 May 2023" with 270.85 EUR transaction amount + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Interest Payment Waiver | 270.85 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + @TestRailId:C3808 + Scenario: Correct Accrual Activity event publishing for backdated loans when the loan re-opens after reversing a repayment transaction - UC6 + Given Admin sets the business date to "05 May 2023" + And Admin creates a client with random data + When Admin sets the business date to "24 June 2025" + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_REFUND_INTEREST_RECALC_DOWNPAYMENT_ACCRUAL_ACTIVITY | 05 May 2023 | 359.79 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "05 May 2023" with "359.79" amount and expected disbursement date on "05 May 2023" + And Admin successfully disburse the loan on "05 May 2023" with "359.79" EUR transaction amount + When Customer makes "AUTOPAY" repayment on "19 May 2023" with 359.79 EUR transaction amount + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "19 May 2023" + Then Loan status will be "OVERPAID" + When Customer undo "1"th transaction made on "19 May 2023" + Then Loan status will be "ACTIVE" + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 05 May 2023 | Disbursement | 359.79 | 0.0 | 0.0 | 0.0 | 0.0 | 359.79 | false | false | + | 05 May 2023 | Down Payment | 89.95 | 89.95 | 0.0 | 0.0 | 0.0 | 269.84 | false | false | + | 19 May 2023 | Repayment | 359.79 | 269.84 | 1.01 | 0.0 | 0.0 | 0.0 | true | false | + | 05 June 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | true | + | 05 July 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 August 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 September 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 October 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual | 1.01 | 0.0 | 1.01 | 0.0 | 0.0 | 0.0 | false | false | + | 05 November 2023 | Accrual Activity | 2.25 | 0.0 | 2.25 | 0.0 | 0.0 | 0.0 | false | false | + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 July 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 August 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 September 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 October 2023" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "05 November 2023" + And "Accrual Activity" transaction on "05 June 2023" got reverse-replayed on "24 June 2025" + + When Loan Pay-off is made on "24 June 2025" + Then Loan's all installments have obligations met + + diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanMigration.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanMigration.feature index 68cd0935ac6..df6d41f2c88 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanMigration.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanMigration.feature @@ -683,3 +683,93 @@ Feature: Loan Migration # Verify loan has no overdue amounts and closed date is recorded Then Loan status will be "CLOSED_OBLIGATIONS_MET" Then Loan has 0.0 total overdue amount + + @TestRailId:C3804 + Scenario: Verify backdated loan migration with disbursement reversal and running Loan COB afterwards + When Admin sets the business date to "07 April 2025" + And Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 10000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2025" with "10000" amount and expected disbursement date on "01 January 2025" + And Admin successfully disburse the loan on "01 January 2025" with "10000" EUR transaction amount + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2025 | | 10000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2025 | | 7537.19 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 2 | 28 | 01 March 2025 | | 5074.38 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 3 | 31 | 01 April 2025 | | 2611.57 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 4 | 30 | 01 May 2025 | | 0.0 | 2611.57 | 40.89 | 0.0 | 0.0 | 2652.46 | 0.0 | 0.0 | 0.0 | 2652.46 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 340.89 | 0 | 0 | 10340.89 | 0.0 | 0.0 | 0.0 | 10340.89 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2025 | Disbursement | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 10000.0 | false | false | + When Admin runs inline COB job for Loan + # Verify accrual entries are created correctly + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2025 | | 10000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2025 | | 7537.19 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 2 | 28 | 01 March 2025 | | 5074.38 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 3 | 31 | 01 April 2025 | | 2611.57 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 4 | 30 | 01 May 2025 | | 0.0 | 2611.57 | 40.89 | 0.0 | 0.0 | 2652.46 | 0.0 | 0.0 | 0.0 | 2652.46 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 340.89 | 0.0 | 0.0 | 10340.89 | 0.0 | 0.0 | 0.0 | 10340.89 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2025 | Disbursement | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 10000.0 | false | false | + | 01 February 2025 | Accrual Activity | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2025 | Accrual Activity | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 April 2025 | Accrual Activity | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | false | + | 06 April 2025 | Accrual | 316.67 | 0.0 | 316.67 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "01 January 2025" + When Admin successfully undo disbursal + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2025 | | 10000.0 | | | 0.0 | | 0.0 | | | | 0.0 | + | 1 | 31 | 01 February 2025 | | 7537.19 | 2462.81 | 100.0 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 2 | 28 | 01 March 2025 | | 5049.75 | 2487.44 | 75.37 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 3 | 31 | 01 April 2025 | | 2537.44 | 2512.31 | 50.5 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + | 4 | 30 | 01 May 2025 | | 0.0 | 2537.44 | 25.37 | 0.0 | 0.0 | 2562.81 | 0.0 | 0.0 | 0.0 | 2562.81 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 251.24 | 0.0 | 0.0 | 10251.24 | 0.0 | 0.0 | 0.0 | 10251.24 | + Then Loan Transactions tab has none transaction + When Admin sets the business date to "02 January 2025" + And Admin successfully disburse the loan on "02 January 2025" with "10000" EUR transaction amount + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 02 January 2025 | | 10000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2025 | | 7534.78 | 2465.22 | 96.77 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 2 | 28 | 01 March 2025 | | 5048.14 | 2486.64 | 75.35 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 3 | 31 | 01 April 2025 | | 2536.63 | 2511.51 | 50.48 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 4 | 30 | 01 May 2025 | | 0.0 | 2536.63 | 25.37 | 0.0 | 0.0 | 2562.0 | 0.0 | 0.0 | 0.0 | 2562.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 247.97 | 0 | 0 | 10247.97 | 0.0 | 0.0 | 0.0 | 10247.97 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 02 January 2025 | Disbursement | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 10000.0 | false | false | + When Admin sets the business date to "08 April 2025" + When Admin runs inline COB job for Loan + # Verify accrual entries are created correctly + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 02 January 2025 | | 10000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2025 | | 7534.78 | 2465.22 | 96.77 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 2 | 28 | 01 March 2025 | | 5072.79 | 2461.99 | 100.0 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 3 | 31 | 01 April 2025 | | 2610.8 | 2461.99 | 100.0 | 0.0 | 0.0 | 2561.99 | 0.0 | 0.0 | 0.0 | 2561.99 | + | 4 | 30 | 01 May 2025 | | 0.0 | 2610.8 | 43.35 | 0.0 | 0.0 | 2654.15 | 0.0 | 0.0 | 0.0 | 2654.15 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 340.12 | 0.0 | 0.0 | 10340.12 | 0.0 | 0.0 | 0.0 | 10340.12 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 02 January 2025 | Disbursement | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 10000.0 | false | false | + | 01 February 2025 | Accrual Activity | 96.77 | 0.0 | 96.77 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2025 | Accrual Activity | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 April 2025 | Accrual Activity | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | false | + | 07 April 2025 | Accrual | 242.6 | 0.0 | 242.6 | 0.0 | 0.0 | 0.0 | false | false | diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java index 46422b88c89..eac4d0d9f27 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; @@ -41,6 +42,7 @@ public class ExternalAssetOwnerTransferOutstandingInterestCalculation { private final LoanSummaryProviderDelegate loanSummaryDataProvider; private final ConfigurationDomainService configurationDomainService; private final LoanReadPlatformService loanReadPlatformService; + private final CurrencyMapper currencyMapper; private LoanSummaryDataProvider fetchLoanSummaryDataProvider(Loan loan) { return this.loanSummaryDataProvider.resolveLoanSummaryDataProvider(loan.getTransactionProcessingStrategyCode()); @@ -58,7 +60,7 @@ public BigDecimal calculateOutstandingInterest(Loan loan) { .map(i -> i.getInterestOutstanding(loan.getCurrency())).reduce(Money.zero(loan.getCurrency()), MathUtil::plus); BigDecimal notDuePayableAmount = fetchLoanSummaryDataProvider(loan) .computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan, data.getRepaymentSchedule().getPeriods(), - DateUtils.getBusinessLocalDate(), loan.getCurrency().toData(), duePayableAmount.getAmount()); + DateUtils.getBusinessLocalDate(), currencyMapper.map(loan.getCurrency()), duePayableAmount.getAmount()); yield MathUtil.add(duePayableAmount.getAmount(), notDuePayableAmount); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java index 2549cf84dd2..c9daae3529d 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java @@ -64,7 +64,7 @@ public class InvestorBusinessEventSerializer extends AbstractBusinessEventWithCu private static CurrencyDataV1 getCurrencyFromEvent(InvestorBusinessEvent event) { MonetaryCurrency loanCurrency = event.getLoan().getCurrency(); CurrencyDataV1 currency = CurrencyDataV1.newBuilder().setCode(loanCurrency.getCode()) - .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getCurrencyInMultiplesOf()).build(); + .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getInMultiplesOf()).build(); return currency; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java index 69868e25138..9f43245833c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -173,6 +173,9 @@ public Map populateChangesForNewLoanProductToGLAccountMappingCre final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element); + final Long incomeFromBuyDownFeesAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(), element); + final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element); final Long overPaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.OVERPAYMENT.getValue(), @@ -198,12 +201,14 @@ public Map populateChangesForNewLoanProductToGLAccountMappingCre case ACCRUAL_PERIODIC: populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId, incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId, - incomeFromRecoveryAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId); + incomeFromRecoveryAccountId, incomeFromBuyDownFeesAccountId, receivableInterestAccountId, receivableFeeAccountId, + receivablePenaltyAccountId); break; case ACCRUAL_UPFRONT: populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId, incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId, - incomeFromRecoveryAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId); + incomeFromRecoveryAccountId, incomeFromBuyDownFeesAccountId, receivableInterestAccountId, receivableFeeAccountId, + receivablePenaltyAccountId); break; } @@ -213,8 +218,8 @@ public Map populateChangesForNewLoanProductToGLAccountMappingCre private void populateChangesForAccrualBasedAccounting(final Map changes, final Long fundAccountId, final Long loanPortfolioAccountId, final Long incomeFromInterestId, final Long incomeFromFeeId, final Long incomeFromPenaltyId, final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId, - final Long incomeFromRecoveryAccountId, final Long receivableInterestAccountId, final Long receivableFeeAccountId, - final Long receivablePenaltyAccountId) { + final Long incomeFromRecoveryAccountId, final Long incomeFromBuyDownFeesAccountId, final Long receivableInterestAccountId, + final Long receivableFeeAccountId, final Long receivablePenaltyAccountId) { changes.put(LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), receivableInterestAccountId); changes.put(LoanProductAccountingParams.FEES_RECEIVABLE.getValue(), receivableFeeAccountId); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 29e0b0843d3..7406584aff7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -58,6 +58,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.springframework.lang.NonNull; import org.springframework.transaction.annotation.Transactional; @@ -77,6 +78,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo private final LoanDelinquencyActionRepository loanDelinquencyActionRepository; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final ConfigurationDomainService configurationDomainService; + private final LoanTransactionRepository loanTransactionRepository; @Override public List retrieveAllDelinquencyRanges() { @@ -174,7 +176,7 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { private void addInstallmentLevelDelinquencyData(CollectionData collectionData, Long loanId) { Collection loanInstallmentDelinquencyTagData = retrieveLoanInstallmentsCurrentDelinquencyTag( loanId); - if (loanInstallmentDelinquencyTagData != null && loanInstallmentDelinquencyTagData.size() > 0) { + if (loanInstallmentDelinquencyTagData != null && !loanInstallmentDelinquencyTagData.isEmpty()) { // installment level delinquency grouped by rangeId, and summed up the delinquent amount Collection installmentLevelDelinquencies = loanInstallmentDelinquencyTagData.stream() @@ -254,12 +256,7 @@ private LocalDate getEarliestUnpaidInstallmentDate(final Loan loan) { } } - LocalDate lastTransactionDate = null; - for (final LoanTransaction transaction : loan.getLoanTransactions()) { - if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) { - lastTransactionDate = transaction.getTransactionDate(); - } - } + final LocalDate lastTransactionDate = loanTransactionRepository.findLastRepaymentLikeTransactionDate(loan).orElse(null); LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate; if (DateUtils.isAfter(lastTransactionDate, earliestUnpaidInstallmentDate)) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java index 71e63821966..530ce477f35 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java @@ -42,6 +42,7 @@ import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionReadService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -60,11 +61,12 @@ public DelinquencyReadPlatformService delinquencyReadPlatformService(Delinquency LoanDelinquencyDomainService loanDelinquencyDomainService, LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag, LoanDelinquencyActionRepository loanDelinquencyActionRepository, - DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService) { + DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService, + LoanTransactionRepository loanTransactionRepository) { return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper, - configurationDomainService); + configurationDomainService, loanTransactionRepository); } @Bean diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index fea5061f361..d06a0ee244c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -181,6 +181,8 @@ public interface LoanApiConstants { String CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND = "capitalizedIncomeAdjustment"; String CONTRACT_TERMINATION_COMMAND = "contractTermination"; String UNDO_CONTRACT_TERMINATION_COMMAND = "undoContractTermination"; + String BUY_DOWN_FEE_COMMAND = "buyDownFee"; + String BUY_DOWN_FEE_ADJUSTMENT_COMMAND = "buyDownFeeAdjustment"; // Data Validator names String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index 8785c1caa6a..9de2b093dd3 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -72,6 +72,8 @@ public class LoanTransactionEnumData implements Serializable { private final boolean capitalizedIncomeAdjustment; private final boolean capitalizedIncomeAmortizationAdjustment; private final boolean contractTermination; + private final boolean buyDownFee; + private final boolean buyDownFeeAdjustment; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -114,6 +116,8 @@ public LoanTransactionEnumData(final Long id, final String code, final String va this.capitalizedIncomeAmortizationAdjustment = Long .valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue()).equals(this.id); this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id); + this.buyDownFee = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE.getValue()).equals(this.id); + this.buyDownFeeAdjustment = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue()).equals(this.id); } public boolean isRepaymentType() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 3c8d29df2c1..8951bdd50a7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -59,7 +59,6 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -74,7 +73,6 @@ import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; @@ -433,13 +431,13 @@ public static Loan newIndividualLoanApplication(final String accountNo, final Cl final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, - final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, - final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency, + final LocalDate submittedOnDate) { return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges, collateral, null, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, - enableInstallmentLevelDelinquency, submittedOnDate); + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency, + submittedOnDate); } public static Loan newGroupLoanApplication(final String accountNo, final Group group, final AccountType loanType, @@ -450,13 +448,13 @@ public static Loan newGroupLoanApplication(final String accountNo, final Group g final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, - final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, - final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency, + final LocalDate submittedOnDate) { return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, - enableInstallmentLevelDelinquency, submittedOnDate); + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency, + submittedOnDate); } public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, @@ -467,13 +465,13 @@ public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, - final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, - final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency, + final LocalDate submittedOnDate) { return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, - enableInstallmentLevelDelinquency, submittedOnDate); + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency, + submittedOnDate); } protected Loan() { @@ -488,8 +486,8 @@ private Loan(final String accountNo, final Client client, final Group group, fin final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, - final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, - final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency, + final LocalDate submittedOnDate) { this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; this.isFloatingInterestRate = isFloatingInterestRate; @@ -850,17 +848,6 @@ public void removePostDatedChecks() { this.postDatedChecks = new ArrayList<>(); } - public List retrieveListOfTransactionsExcludeAccruals() { - final List repaymentsOrWaivers = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) { - repaymentsOrWaivers.add(transaction); - } - } - repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE); - return repaymentsOrWaivers; - } - public List retrieveListOfTransactionsByType(final LoanTransactionType transactionType) { return this.loanTransactions.stream() .filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) @@ -880,11 +867,6 @@ public LoanTransaction findWriteOffTransaction() { .orElse(null); } - public Money calculateTotalRecoveredPayments() { - // in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments - return getTotalRecoveredPayments(); - } - public MonetaryCurrency loanCurrency() { return this.loanRepaymentScheduleDetail.getCurrency(); } @@ -1033,17 +1015,6 @@ public Money getTotalPaidInRepayments() { return cumulativePaid; } - public Money getTotalRecoveredPayments() { - Money cumulativePaid = Money.zero(getCurrency()); - - for (final LoanTransaction recoveredPayment : this.loanTransactions) { - if (recoveredPayment.isRecoveryRepayment()) { - cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(getCurrency())); - } - } - return cumulativePaid; - } - public Money getTotalPrincipalOutstandingUntil(LocalDate date) { return getRepaymentScheduleInstallments().stream() .filter(installment -> installment.getDueDate().isBefore(date) || installment.getDueDate().isEqual(date)) @@ -1236,7 +1207,7 @@ public LocalDate getLastUserTransactionDate() { .filter(date -> DateUtils.isBefore(getDisbursementDate(), date)).max(LocalDate::compareTo).orElse(getDisbursementDate()); } - private boolean isUserTransaction(LoanTransaction transaction) { + public boolean isUserTransaction(LoanTransaction transaction) { return !(transaction.isReversed() || transaction.isAccrualRelated() || transaction.isIncomePosting()); } @@ -1391,47 +1362,6 @@ public LocalDate fetchInterestRecalculateFromDate() { return recalculatedOn; } - public void updateLoanOutstandingBalances() { - Money outstanding = Money.zero(getCurrency()); - List loanTransactions = retrieveListOfTransactionsExcludeAccruals(); - for (LoanTransaction loanTransaction : loanTransactions) { - if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) { - outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())) - .minus(loanTransaction.getOverPaymentPortion(getCurrency())); - loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); - } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { - Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()); - if (loanTransaction.isOverPaid()) { - // in case of advanced payment strategy and creditAllocations the full amount is recognized first - if (this.getCreditAllocationRules() != null && !this.getCreditAllocationRules().isEmpty()) { - Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() // - .map(mapping -> mapping.getPrincipalPortion(getCurrency())) // - .reduce(Money.zero(getCurrency()), Money::plus); - transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()).minus(payedPrincipal); - } else { - // in case legacy payment strategy - transactionOutstanding = loanTransaction.getAmount(getCurrency()) - .minus(loanTransaction.getOverPaymentPortion(getCurrency())); - } - if (transactionOutstanding.isLessThanZero()) { - transactionOutstanding = Money.zero(getCurrency()); - } - } - outstanding = outstanding.plus(transactionOutstanding); - loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); - } else if (!loanTransaction.isAccrualActivity()) { - if (this.loanInterestRecalculationDetails != null - && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction() - && !loanTransaction.isRepaymentAtDisbursement()) { - outstanding = outstanding.minus(loanTransaction.getAmount(getCurrency())); - } else { - outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(getCurrency())); - } - loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); - } - } - } - public String transactionProcessingStrategy() { return this.transactionProcessingStrategyCode; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 397343026e3..6c59aa39bbc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -911,10 +911,6 @@ public boolean isPaymentTransaction() { || this.isIncomePosting()); } - public boolean hasLoanTransactionRelations() { - return !loanTransactionRelations.isEmpty(); - } - public List getLoanTransactionRelations(Predicate predicate) { return loanTransactionRelations.stream().filter(predicate).toList(); } @@ -965,4 +961,13 @@ public void updateTransactionDate(final LocalDate transactionDate) { this.dateOf = transactionDate; } + public static LoanTransaction buyDownFee(final Loan loan, final Money amount, final PaymentDetail paymentDetail, + final LocalDate transactionDate, final ExternalId externalId) { + return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.BUY_DOWN_FEE, paymentDetail, amount.getAmount(), + transactionDate, externalId); + } + + public boolean isBuyDownFee() { + return LoanTransactionType.BUY_DOWN_FEE.equals(this.typeOf); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java index 04b791eb493..3934f0253fa 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java @@ -127,7 +127,8 @@ AND lt.typeOf NOT IN ( org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER, org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER, org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER, - org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT ) """) Optional findLastTransactionDateForReprocessing(@Param("loan") Loan loan); @@ -174,7 +175,7 @@ List findTransactionDataForForeclosureIncome( AND lt.typeOf IN :types AND lt.dateOf = :transactionDate """) - Optional findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("type") Set types, + Optional findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("types") Set types, @Param("transactionDate") LocalDate transactionDate); @Query(""" @@ -184,7 +185,7 @@ Optional findNonReversedByLoanAndTypesAndDate(@Param("loan") Lo AND lt.reversed = false AND lt.typeOf IN :types AND lt.dateOf > :transactionDate - ORDER BY lt.dateOf + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndTypesAndAfterDate(@Param("loan") Loan loan, @Param("types") Set types, @Param("transactionDate") LocalDate transactionDate); @@ -196,7 +197,7 @@ List findNonReversedByLoanAndTypesAndAfterDate(@Param("loan") L AND lt.reversed = false AND lt.typeOf = :type AND lt.dateOf > :transactionDate - ORDER BY lt.dateOf + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndTypeAndAfterDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type, @Param("transactionDate") LocalDate transactionDate); @@ -219,6 +220,7 @@ boolean existsNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan AND lt.reversed = false AND lt.typeOf IN :types AND lt.id NOT IN :existingTransactionIds + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Loan loan, @Param("types") Set types, @Param("existingTransactionIds") List existingTransactionIds); @@ -229,6 +231,7 @@ List findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Lo WHERE lt.loan = :loan AND lt.reversed = false AND lt.typeOf IN :types + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndTypes(@Param("loan") Loan loan, @Param("types") Set types); @@ -238,6 +241,7 @@ List findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Lo WHERE lt.loan = :loan AND lt.reversed = false AND lt.typeOf = :type + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndType(@Param("loan") Loan loan, @Param("type") LoanTransactionType type); @@ -248,7 +252,7 @@ List findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Lo AND lt.reversed = false AND lt.dateOf >= :date AND lt.typeOf IN :types - ORDER BY lt.dateOf + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan, @Param("types") Set types, @Param("date") LocalDate date); @@ -333,7 +337,7 @@ List findTransactionsForAccountingBridge(@Param("loan") Loan lo AND lt.reversed = false AND lt.typeOf = :type AND lt.dateOf IN :transactionDates - ORDER BY lt.id + ORDER BY lt.dateOf, lt.createdDate, lt.id """) List findNonReversedLoanAndTypeAndDates(@Param("loan") Loan loan, @Param("type") LoanTransactionType type, @Param("transactionDates") Set transactionDates); @@ -367,4 +371,90 @@ SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanacco AND ltr.relationType = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.ADJUSTMENT """) List findAdjustmentsForCapitalizedIncome(@Param("capitalizedIncome") LoanTransaction capitalizedIncome); + + @Query(""" + SELECT lt FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf NOT IN ( + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT + ) + ORDER BY lt.dateOf, lt.createdDate, lt.id + """) + List findNonReversedTransactionsForReprocessingByLoan(@Param("loan") Loan loan); + + @Query(""" + SELECT MAX(lt.dateOf) FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.amount > 0 + AND lt.typeOf IN ( + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MERCHANT_ISSUED_REFUND, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.PAYOUT_REFUND, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.GOODWILL_CREDIT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_REFUND, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_ADJUSTMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DOWN_PAYMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_PAYMENT_WAIVER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_REFUND, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT + ) + """) + Optional findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan); + + @Query(""" + SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf = :type + AND lt.dateOf > :transactionDate + """) + boolean existsNonReversedByLoanAndTypeAndAfterDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type, + @Param("transactionDate") LocalDate transactionDate); + + @Query(""" + SELECT lt FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf NOT IN ( + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ACTIVITY, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_OFF, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAGE, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRACT_TERMINATION, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT + ) + ORDER BY lt.dateOf, lt.createdDate, lt.id + """) + List findNonReversedMonetaryTransactionsByLoan(@Param("loan") Loan loan); + + @Query(""" + SELECT COALESCE(SUM(lt.amount), 0) + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.RECOVERY_REPAYMENT + """) + BigDecimal calculateTotalRecoveryPaymentAmount(@Param("loan") Loan loan); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index d259e6e1206..0e014d02752 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -73,6 +73,9 @@ public enum LoanTransactionType { CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT(39, "loanTransactionType.capitalizedIncomeAmortizationAdjustment"), // // Kind of Final Transactions CONTRACT_TERMINATION(38, "loanTransactionType.contractTermination"), // + + BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), // + BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), // ; private final Integer value; @@ -128,6 +131,8 @@ public static LoanTransactionType fromInt(final Integer transactionType) { case 37 -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT; case 38 -> LoanTransactionType.CONTRACT_TERMINATION; case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT; + case 40 -> LoanTransactionType.BUY_DOWN_FEE; + case 41 -> LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT; default -> LoanTransactionType.INVALID; }; } @@ -256,4 +261,12 @@ public boolean isCapitalizedIncomeAdjustment() { public boolean isContractTermination() { return this == LoanTransactionType.CONTRACT_TERMINATION; } + + public boolean isBuyDownFee() { + return this == LoanTransactionType.BUY_DOWN_FEE; + } + + public boolean isBuyDownFeeAdjustment() { + return this == LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java index d12449b894b..f73ed71b6d0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java @@ -37,7 +37,6 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper { public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate, final List installments, LoanCharge loanCharge) { - Loan loan = loanCharge.getLoan(); Money zero = Money.zero(currency); Money totalInterest = zero; Money totalPrincipal = zero; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index a6aaa89ca69..b733c459da9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -217,11 +217,9 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur loanTransaction.resetDerivedComponents(); handleRefund(loanTransaction, currency, installments, charges); } else if (loanTransaction.isCreditBalanceRefund()) { - recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed, - overpaymentHolder); + recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, overpaymentHolder); } else if (loanTransaction.isChargeback()) { - recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed, - overpaymentHolder); + recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, overpaymentHolder); reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed); } else if (loanTransaction.isChargeOff()) { recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments); @@ -475,7 +473,7 @@ private void reprocessChargebackTransactionRelation(ChangedTransactionDetail cha protected void reprocessInstallments(LocalDate disbursementDate, List transactions, List installments, MonetaryCurrency currency) { - LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1); + LoanRepaymentScheduleInstallment lastInstallment = installments.getLast(); if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) { installments.remove(lastInstallment); } @@ -519,8 +517,7 @@ protected boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepay } private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, - MonetaryCurrency currency, List installments, List transactionsToBeProcessed, - MoneyHolder overpaymentHolder) { + MonetaryCurrency currency, List installments, MoneyHolder overpaymentHolder) { // pass through for new transactions if (loanTransaction.getId() == null) { return; @@ -606,7 +603,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHo // New installment will be added (N+1 scenario) if (!loanTransactionMapped) { if (loanTransaction.getTransactionDate().equals(pastDueDate)) { - LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.get(installmentToBeProcessed.size() - 1); + LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.getLast(); currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount()); currentInstallment.addToPrincipal(transactionDate, transactionAmount); if (repaidAmount.isGreaterThanZero()) { @@ -647,7 +644,7 @@ protected Money handleTransactionAndCharges(final LoanTransaction loanTransactio final Set loanPenalties = extractPenaltyCharges(charges); Integer installmentNumber = null; if (loanTransaction.isChargePayment() && installments.size() == 1) { - installmentNumber = installments.get(0).getInstallmentNumber(); + installmentNumber = installments.getFirst().getInstallmentNumber(); } if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual() && !loanTransaction.isAccrualActivity()) { @@ -944,12 +941,12 @@ protected LoanCharge getLatestLoanChargeWithSpecificDueDate(Set char return null; } LoanCharge latestCharge = null; - List chargesWithSpecificDueDate = new ArrayList<>(); - chargesWithSpecificDueDate.addAll(charges.stream().filter(charge -> charge.isSpecifiedDueDate()).toList()); + final List chargesWithSpecificDueDate = new ArrayList<>( + charges.stream().filter(LoanCharge::isSpecifiedDueDate).toList()); if (!CollectionUtils.isEmpty(chargesWithSpecificDueDate)) { - Collections.sort(chargesWithSpecificDueDate, - (charge1, charge2) -> DateUtils.compare(charge1.getEffectiveDueDate(), charge2.getEffectiveDueDate())); - latestCharge = chargesWithSpecificDueDate.get(chargesWithSpecificDueDate.size() - 1); + chargesWithSpecificDueDate + .sort((charge1, charge2) -> DateUtils.compare(charge1.getEffectiveDueDate(), charge2.getEffectiveDueDate())); + latestCharge = chargesWithSpecificDueDate.getLast(); } return latestCharge; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionProcessingException.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionProcessingException.java new file mode 100644 index 00000000000..ed90a98c754 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionProcessingException.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.loanaccount.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown when loan transaction processing violates a domain rule. + */ +public class LoanTransactionProcessingException extends AbstractPlatformDomainRuleException { + + public LoanTransactionProcessingException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loan.transaction.processing", defaultUserMessage, defaultUserMessageArgs); + } + + public LoanTransactionProcessingException(final String action, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + super("error.msg.loan.transaction." + action, defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 7ebfb54db97..9dc0ea79483 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -41,6 +41,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO; import org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; @@ -55,6 +56,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; @@ -62,13 +64,13 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; @RequiredArgsConstructor public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator { - private final LoanTransactionService loanTransactionService; + private final LoanTransactionRepository loanTransactionRepository; + private final CurrencyMapper currencyMapper; @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, @@ -402,7 +404,7 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe // this condition is to add the interest from grace period if not // already applied. if (scheduleParams.getTotalOutstandingInterestPaymentDueToGrace().isGreaterThanZero()) { - LoanScheduleModelPeriod installment = periods.get(periods.size() - 1); + LoanScheduleModelPeriod installment = periods.getLast(); installment.addInterestAmount(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); scheduleParams.addTotalRepaymentExpected(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); scheduleParams.addTotalCumulativeInterest(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); @@ -419,7 +421,7 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe if (scheduleParams.getScheduleTillDate() != null) { currentDate = scheduleParams.getScheduleTillDate(); } - if (scheduleParams.applyInterestRecalculation() && scheduleParams.getLatePaymentMap().size() > 0 + if (scheduleParams.applyInterestRecalculation() && !scheduleParams.getLatePaymentMap().isEmpty() && DateUtils.isAfter(currentDate, scheduleParams.getPeriodStartDate())) { Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentDate(mc, loanApplicationTerms, holidayDetailDTO, monetaryCurrency, periods, currentDate, loanRepaymentScheduleTransactionProcessor, transactions, loanCharges, @@ -1343,9 +1345,8 @@ private Money getPrincipalToBeScheduled(final LoanApplicationTerms loanApplicati return principalToBeScheduled.minus(loanApplicationTerms.getDownPaymentAmount()); } - private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber, + private void updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber, Money outstandingBalance) { - boolean isAmountChanged = false; if (loanApplicationTerms.getActualFixedEmiAmount() == null && loanApplicationTerms.getInterestMethod().isDecliningBalance() && loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) { if (periodNumber < loanApplicationTerms.getPrincipalGrace() + 1) { @@ -1354,9 +1355,7 @@ private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApp Money emiAmount = loanApplicationTerms.pmtForInstallment(getPaymentPeriodsInOneYearCalculator(), outstandingBalance, periodNumber, mc); loanApplicationTerms.setFixedEmiAmount(emiAmount.getAmount()); - isAmountChanged = true; } - return isAmountChanged; } private Money fetchArrears(final LoanApplicationTerms loanApplicationTerms, final MonetaryCurrency currency, @@ -1982,7 +1981,7 @@ private BigDecimal getDisbursementAmount(final LoanApplicationTerms loanApplicat // this method relates to multi-disbursement loans BigDecimal principal = BigDecimal.ZERO; - if (loanApplicationTerms.getDisbursementDatas().size() == 0) { + if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { // non tranche loans have no disbursement data entries in submitted and approved status // the appropriate approved amount or applied for amount is used to show a proposed schedule if (loanApplicationTerms.getApprovedPrincipal().getAmount().compareTo(BigDecimal.ZERO) > 0) { @@ -2508,7 +2507,7 @@ public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final Lo totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, uncompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest, newRepaymentScheduleInstallments, recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, - currency.toData(), applyInterestRecalculation, mc); + currencyMapper.map(currency), applyInterestRecalculation, mc); retainedInstallments.addAll(newRepaymentScheduleInstallments); loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations); loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getSummary().getTotalInterestCharged())); @@ -2519,10 +2518,9 @@ public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final Lo } - if (retainedInstallments.size() > 0 - && retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion() != null) { + if (!retainedInstallments.isEmpty() && retainedInstallments.getLast().getRescheduleInterestPortion() != null) { loanApplicationTerms.setInterestTobeApproppriated( - Money.of(loan.getCurrency(), retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion())); + Money.of(loan.getCurrency(), retainedInstallments.getLast().getRescheduleInterestPortion())); } LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO, loanScheduleParams); @@ -2721,19 +2719,18 @@ private Money calculateExpectedPrincipalPortion(final Money interestPortion, fin return principalPortionCalculated; } - private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List installments, + private void addLoanRepaymentScheduleInstallment(final List installments, final LoanScheduleModelPeriod scheduledLoanInstallment) { - LoanRepaymentScheduleInstallment installment = null; if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { - installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(), - scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(), - scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(), - scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(), - scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(), - scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod()); + final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null, + scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), + scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), + scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), + scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(), + scheduledLoanInstallment.getLoanCompoundingDetails(), scheduledLoanInstallment.rescheduleInterestPortion(), + scheduledLoanInstallment.isDownPaymentPeriod()); installments.add(installment); } - return installment; } private LoanScheduleModelPeriod createLoanScheduleModelDownPaymentPeriod(final LoanRepaymentScheduleInstallment installment, @@ -2802,7 +2799,7 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency cu LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, loanRepaymentScheduleTransactionProcessor, onDate, calculateTill); - final List loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); + final List loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java index a3ce6e25bc9..c68596013e1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java @@ -27,8 +27,9 @@ import java.util.TreeMap; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.springframework.stereotype.Component; @@ -63,8 +64,8 @@ public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends Abs public CumulativeDecliningBalanceInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator, final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, - final LoanTransactionService loanTransactionService) { - super(loanTransactionService); + final LoanTransactionRepository loanTransactionRepository, final CurrencyMapper currencyMapper) { + super(loanTransactionRepository, currencyMapper); this.scheduledDateGenerator = scheduledDateGenerator; this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java index ad58d1f2f55..1c3cb39999f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java @@ -25,8 +25,9 @@ import java.util.Map; import java.util.TreeMap; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.springframework.stereotype.Component; @Component @@ -37,8 +38,8 @@ public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulat public CumulativeFlatInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator, final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, - final LoanTransactionService loanTransactionService) { - super(loanTransactionService); + final LoanTransactionRepository loanTransactionRepository, final CurrencyMapper currencyMapper) { + super(loanTransactionRepository, currencyMapper); this.scheduledDateGenerator = scheduledDateGenerator; this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java index 101165eab9f..97733c12af6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java @@ -19,12 +19,14 @@ package org.apache.fineract.portfolio.loanaccount.mapper; import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(config = MapstructMapperConfig.class, uses = { LoanTransactionRelationMapper.class, LoanChargePaidByMapper.class }) +@Mapper(config = MapstructMapperConfig.class, uses = { LoanTransactionRelationMapper.class, LoanChargePaidByMapper.class, + CurrencyMapper.class }) public interface LoanTransactionMapper { @Mapping(target = "numberOfRepayments", ignore = true) @@ -46,6 +48,6 @@ public interface LoanTransactionMapper { @Mapping(target = "netDisbursalAmount", source = "loan.netDisbursalAmount") @Mapping(target = "transactionType", expression = "java(org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations.transactionType(loanTransaction.getTypeOf()))") @Mapping(target = "paymentDetailData", expression = "java(loanTransaction.getPaymentDetail() != null ? loanTransaction.getPaymentDetail().toData() : null)") - @Mapping(target = "currency", expression = "java(loanTransaction.getLoan().getCurrency().toData())") + @Mapping(target = "currency", source = "loan.currency") LoanTransactionData mapLoanTransaction(LoanTransaction loanTransaction); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java index 34e2eb06037..e84dc7841d9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java @@ -19,18 +19,24 @@ package org.apache.fineract.portfolio.loanaccount.serialization; import java.time.LocalDate; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public final class LoanChargeValidator { + private final LoanTransactionRepository loanTransactionRepository; + public void validateLoanIsNotClosed(final Loan loan, final LoanCharge loanCharge) { if (loan.isClosed()) { final String defaultUserMessage = "This charge cannot be added as the loan is already closed."; @@ -80,13 +86,13 @@ public void validateChargeAdditionForDisbursedLoan(final Loan loan, final LoanCh public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransaction repaymentTransaction, final String reversedOrCreated) { if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) { - for (LoanTransaction txn : loan.getLoanTransactions()) { - if (txn.isChargeRefund() && DateUtils.isBefore(repaymentTransaction.getTransactionDate(), txn.getTransactionDate())) { - final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; - final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated - + " because a Later Charge Refund Exists."; - throw new LoanChargeRefundException(errorMessage, details); - } + final boolean existsChargeRefund = loanTransactionRepository.existsNonReversedByLoanAndTypeAndAfterDate(loan, + LoanTransactionType.CHARGE_REFUND, repaymentTransaction.getTransactionDate()); + if (existsChargeRefund) { + final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; + final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated + + " because a Later Charge Refund Exists."; + throw new LoanChargeRefundException(errorMessage, details); } } } 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 cbacb2a24e8..a93d21b6963 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 @@ -21,10 +21,12 @@ import jakarta.persistence.FlushModeType; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -34,6 +36,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.springframework.stereotype.Service; @@ -43,6 +47,7 @@ public class LoanBalanceService { private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService; private final FlushModeHandler flushModeHandler; + private final LoanTransactionRepository loanTransactionRepository; public Money calculateTotalOverpayment(final Loan loan) { Money totalPaidInRepayments = loan.getTotalPaidInRepayments(); @@ -107,7 +112,7 @@ public void refreshSummaryAndBalancesForDisbursedLoan(final Loan loan) { loan.setTotalOverpaid(overpaidBy.getAmountDefaultedToNullIfZero()); } - final Money recoveredAmount = loan.calculateTotalRecoveredPayments(); + final Money recoveredAmount = calculateTotalRecoveredPayments(loan); loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero()); final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal(); @@ -115,7 +120,61 @@ public void refreshSummaryAndBalancesForDisbursedLoan(final Loan loan) { final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan); loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(), capitalizedIncome, capitalizedIncomeAdjustment); - loan.updateLoanOutstandingBalances(); + updateLoanOutstandingBalances(loan); + } + + private Money calculateTotalRecoveredPayments(Loan loan) { + // in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments + final BigDecimal totalRecoveryAmount = loanTransactionRepository.calculateTotalRecoveryPaymentAmount(loan); + return Money.of(loan.getCurrency(), totalRecoveryAmount); + } + + public void updateLoanOutstandingBalances(Loan loan) { + Money outstanding = Money.zero(loan.getCurrency()); + final List loanTransactions = new ArrayList<>(); + for (final LoanTransaction transaction : loan.getLoanTransactions()) { + if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) { + loanTransactions.add(transaction); + } + } + loanTransactions.sort(LoanTransactionComparator.INSTANCE); + + for (LoanTransaction loanTransaction : loanTransactions) { + if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) { + outstanding = outstanding.plus(loanTransaction.getAmount(loan.getCurrency())) + .minus(loanTransaction.getOverPaymentPortion(loan.getCurrency())); + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); + } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { + Money transactionOutstanding = loanTransaction.getPrincipalPortion(loan.getCurrency()); + if (loanTransaction.isOverPaid()) { + // in case of advanced payment strategy and creditAllocations the full amount is recognized first + if (loan.getCreditAllocationRules() != null && !loan.getCreditAllocationRules().isEmpty()) { + Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() // + .map(mapping -> mapping.getPrincipalPortion(loan.getCurrency())) // + .reduce(Money.zero(loan.getCurrency()), Money::plus); + transactionOutstanding = loanTransaction.getPrincipalPortion(loan.getCurrency()).minus(payedPrincipal); + } else { + // in case legacy payment strategy + transactionOutstanding = loanTransaction.getAmount(loan.getCurrency()) + .minus(loanTransaction.getOverPaymentPortion(loan.getCurrency())); + } + if (transactionOutstanding.isLessThanZero()) { + transactionOutstanding = Money.zero(loan.getCurrency()); + } + } + outstanding = outstanding.plus(transactionOutstanding); + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); + } else if (!loanTransaction.isAccrualActivity()) { + if (loan.getLoanInterestRecalculationDetails() != null + && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction() + && !loanTransaction.isRepaymentAtDisbursement()) { + outstanding = outstanding.minus(loanTransaction.getAmount(loan.getCurrency())); + } else { + outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(loan.getCurrency())); + } + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); + } + } } public void updateLoanToLastDisbursalState(final Loan loan, final LoanDisbursementDetails disbursementDetail) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index 91e37060c81..9d9f0b0b48d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -338,6 +338,10 @@ public static LoanTransactionEnumData transactionType(final LoanTransactionType case CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT -> new LoanTransactionEnumData( LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue().longValue(), LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getCode(), "Capitalized Income Amortization Adjustment"); + case BUY_DOWN_FEE -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE.getValue().longValue(), + LoanTransactionType.BUY_DOWN_FEE.getCode(), "Buy Down Fee"); + case BUY_DOWN_FEE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue().longValue(), + LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getCode(), "Buy Down Fee Adjustment"); }; } 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 d8d4bda2fe2..f9ed025ea22 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 @@ -84,6 +84,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; @@ -93,7 +94,6 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; @@ -121,17 +121,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private final LoanRepositoryWrapper loanRepositoryWrapper; private final InterestRefundService interestRefundService; private final LoanScheduleComponent loanSchedule; - private final LoanTransactionService loanTransactionService; + private final LoanTransactionRepository loanTransactionRepository; public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper, final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory, - final LoanScheduleComponent loanSchedule, final LoanTransactionService loanTransactionRepository, + final LoanScheduleComponent loanSchedule, final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { super(externalIdFactory, loanChargeValidator, loanBalanceService); this.emiCalculator = emiCalculator; this.loanRepositoryWrapper = loanRepositoryWrapper; this.interestRefundService = interestRefundService; - this.loanTransactionService = loanTransactionRepository; + this.loanTransactionRepository = loanTransactionRepository; this.loanSchedule = loanSchedule; } @@ -212,7 +212,7 @@ public Pair repr } MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); - final Loan loan = loanTransactions.get(0).getLoan(); + final Loan loan = loanTransactions.getFirst().getLoan(); List loanTermVariations = loan.getActiveLoanTermVariations().stream().map(LoanTermVariations::toData) .collect(Collectors.toCollection(ArrayList::new)); final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf(); @@ -276,7 +276,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) { Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); - final List transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); + final List transactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency(); List installments = loan.getRepaymentScheduleInstallments(); Set charges = loan.getActiveCharges(); @@ -552,8 +552,7 @@ protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, Transa if (!loanTransactionMapped) { if (transactionDate.equals(pastDueDate)) { // Transaction is on Maturity date, no additional installment is needed - LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed - .get(installmentToBeProcessed.size() - 1); + LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.getLast(); emiCalculator.creditPrincipal(model, transactionDate, transactionAmount); updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx); @@ -827,7 +826,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac // New installment will be added (N+1 scenario) if (!loanTransactionMapped) { if (loanTransaction.getTransactionDate().equals(pastDueDate)) { - LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1); + LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().getLast(); recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); } else { Loan loan = loanTransaction.getLoan(); @@ -1450,7 +1449,7 @@ private List findOverdueInstallmentsBeforeDate public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) { - Loan loan = ctx.getInstallments().get(0).getLoan(); + Loan loan = ctx.getInstallments().getFirst().getLoan(); if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff() && !ctx.isContractTerminated()) { @@ -1761,8 +1760,7 @@ private void handleAccelerateMaturityDate(final LoanTransaction loanTransaction, .max(LocalDate::compareTo); if (latestDueDate.isPresent()) { - final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate - .get(installmentsUpToTransactionDate.size() - 1); + final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast(); final LoanRepaymentScheduleInstallment installmentForCharges = new LoanRepaymentScheduleInstallment(loan, lastInstallment.getInstallmentNumber() + 1, currentInstallment.getDueDate(), latestDueDate.get(), diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddBuyDownFeeCommandHandler.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddBuyDownFeeCommandHandler.java new file mode 100644 index 00000000000..6fde5230f9a --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddBuyDownFeeCommandHandler.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.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "BUYDOWNFEE") +public class AddBuyDownFeeCommandHandler implements NewCommandSourceHandler { + + private final BuyDownFeePlatformService buyDownFeePlatformService; + private final DataIntegrityErrorHandler dataIntegrityErrorHandler; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + try { + return this.buyDownFeePlatformService.makeLoanBuyDownFee(command.getLoanId(), command); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.buy.down.fee", + "Buy Down Fee"); + return CommandProcessingResult.empty(); + } + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java new file mode 100644 index 00000000000..8ef5620ab63 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.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.portfolio.loanaccount.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.transaction.annotation.Transactional; + +public interface BuyDownFeePlatformService { + + @Transactional + CommandProcessingResult makeLoanBuyDownFee(Long loanId, JsonCommand command); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java new file mode 100644 index 00000000000..fdcc56aa15e --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java @@ -0,0 +1,123 @@ +/** + * 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.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformService { + + private final ProgressiveLoanTransactionValidator loanTransactionValidator; + private final LoanAssembler loanAssembler; + private final LoanTransactionRepository loanTransactionRepository; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final LoanJournalEntryPoster loanJournalEntryPoster; + private final NoteWritePlatformService noteWritePlatformService; + private final ExternalIdFactory externalIdFactory; + + @Transactional + @Override + public CommandProcessingResult makeLoanBuyDownFee(final Long loanId, final JsonCommand command) { + + this.loanTransactionValidator.validateBuyDownFee(command, loanId); + + final Loan loan = this.loanAssembler.assembleFrom(loanId); + checkClientOrGroupActive(loan); + + final List existingTransactionIds = new ArrayList<>(loanTransactionRepository.findTransactionIdsByLoan(loan)); + final List existingReversedTransactionIds = new ArrayList<>(loanTransactionRepository.findReversedTransactionIdsByLoan(loan)); + final Map changes = new LinkedHashMap<>(); + + // Create payment details + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + // Extract transaction details + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId"); + + // Create buy down fee transaction + final Money buyDownFeeAmount = Money.of(loan.getCurrency(), transactionAmount); // FLAT calculation + final LoanTransaction buyDownFeeTransaction = LoanTransaction.buyDownFee(loan, buyDownFeeAmount, paymentDetail, transactionDate, + txnExternalId); + + // Add to loan (NO schedule recalculation as per requirements) + loan.addLoanTransaction(buyDownFeeTransaction); + + // Save transaction + loanTransactionRepository.saveAndFlush(buyDownFeeTransaction); + + // Update loan derived fields + loan.updateLoanScheduleDependentDerivedFields(); + + // Add note if provided + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + noteWritePlatformService.createLoanTransactionNote(buyDownFeeTransaction.getId(), noteText); + } + + // Post journal entries + loanJournalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withClientId(loan.getClientId()) // + .withOfficeId(loan.getOfficeId()) // + .withLoanId(loan.getId()) // + .withEntityId(buyDownFeeTransaction.getId()) // + .withEntityExternalId(buyDownFeeTransaction.getExternalId()) // + .build(); + } + + private void checkClientOrGroupActive(final Loan loan) { + final Client client = loan.client(); + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + final Group group = loan.group(); + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java index f9a7d6133f7..840fd690acd 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -148,7 +148,7 @@ public CommandProcessingResult capitalizedIncomeAdjustment(final Long loanId, fi LoanTransaction savedCapitalizedIncomeAdjustment = loanTransactionRepository.saveAndFlush(capitalizedIncomeAdjustment); // Update outstanding loan balances - loan.updateLoanOutstandingBalances(); + loanBalanceService.updateLoanOutstandingBalances(loan); // Create a note if provided final String noteText = command.stringValueOfParameterNamed("note"); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java index f7bbec6356a..60f8cd2630e 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java @@ -40,6 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.springframework.beans.factory.InitializingBean; @@ -56,9 +57,8 @@ public class InternalProgressiveLoanApiResource implements InitializingBean { private final LoanRepositoryWrapper loanRepository; private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; - private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; private final InterestScheduleModelRepositoryWrapper writePlatformService; - private final LoanTransactionService loanTransactionService; + private final LoanTransactionRepository loanTransactionRepository; @Override @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") @@ -87,7 +87,8 @@ public ProgressiveLoanInterestScheduleModel fetchModel(@PathParam("loanId") @Par } private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(final Loan loan) { - final List transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); + final List transactionsToReprocess = loanTransactionRepository + .findNonReversedTransactionsForReprocessingByLoan(loan); final LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate(); final Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor .reprocessProgressiveLoanTransactionsTransactional(loan.getDisbursementDate(), businessDate, transactionsToReprocess, diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java index 067d66b011d..1b7d41ff8ac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java @@ -31,8 +31,8 @@ import org.apache.fineract.infrastructure.core.serialization.gson.LocalDateAdapter; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.serialization.gson.MoneyDeserializer; -import org.apache.fineract.organisation.monetary.serialization.gson.MoneySerializer; +import org.apache.fineract.organisation.monetary.serialization.MoneyDeserializer; +import org.apache.fineract.organisation.monetary.serialization.MoneySerializer; import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java index 071859a01ed..825a6e85fdd 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java @@ -28,4 +28,6 @@ public interface ProgressiveLoanTransactionValidator extends LoanTransactionVali void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId); void validateContractTerminationUndo(JsonCommand command, Long loanId); + + void validateBuyDownFee(JsonCommand command, Long loanId); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java index 842788b39eb..73480a7a445 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java @@ -23,6 +23,7 @@ import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -33,9 +34,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -53,6 +56,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionProcessingException; import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -269,6 +273,60 @@ public void validateContractTerminationUndo(final JsonCommand command, final Lon }); } + private static final List BUY_DOWN_FEE_TRANSACTION_SUPPORTED_PARAMETERS = List + .of(new String[] { "transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId" }); + + @Override + public void validateBuyDownFee(JsonCommand command, Long loanId) { + final String json = command.json(); + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, BUY_DOWN_FEE_TRANSACTION_SUPPORTED_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.transaction.buyDownFee"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + + if (!loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("buy.down.fee.not.enabled", + "Buy down fee is not enabled for this loan product"); + } + + // Basic validation + validateBuyDownFeeEligibility(loan); + + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + // Validate transaction date is on or after first disbursement + if (transactionDate != null) { + final LocalDate firstDisbursementDate = loan.getDisbursementDate(); + if (firstDisbursementDate != null && transactionDate.isBefore(firstDisbursementDate)) { + baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.before.first.disbursement.date"); + } + } + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateBuyDownFeeEligibility(Loan loan) { + if (!loan.getStatus().isActive()) { + throw new LoanTransactionProcessingException("Buy Down fees can only be added to active loans"); + } + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } + // Delegates @Override public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) { diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 7efc6ad18c6..af7516f3bd5 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -67,6 +67,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; @@ -74,7 +75,6 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; @@ -120,7 +120,7 @@ public static void destruct() { public void setUp() { underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class), Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class), - Mockito.mock(LoanTransactionService.class), Mockito.mock(LoanChargeValidator.class), + Mockito.mock(LoanTransactionRepository.class), Mockito.mock(LoanChargeValidator.class), Mockito.mock(LoanBalanceService.class)); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle index d5b885cc35e..179758d1351 100644 --- a/fineract-provider/build.gradle +++ b/fineract-provider/build.gradle @@ -241,30 +241,6 @@ bootJar { dependsOn resolve } -// Task to copy files from resources/main to the compiled output directory -tasks.register('injectDefaultResources', Copy) { - dependsOn bootJar, jar - from layout.buildDirectory.dir("resources/main") - into "$buildDir/classes/java/main" -} -tasks.injectDefaultResources.dependsOn(bootJar) - -// Ensure this runs before the Jib build -tasks.named("jib").configure { - dependsOn injectDefaultResources - finalizedBy removeMergedClasses -} - -tasks.named("jibDockerBuild").configure { - dependsOn injectDefaultResources - finalizedBy removeMergedClasses -} - -// Task to delete files from the merged output directory -tasks.register('removeMergedClasses', Delete) { - delete layout.buildDirectory.dir("classes/java/main") -} - jib { from { image = 'azul/zulu-openjdk-alpine:21' @@ -305,19 +281,6 @@ jib { implementation 'org.mariadb.jdbc:mariadb-java-client' implementation 'org.postgresql:postgresql' } - - pluginExtensions { - pluginExtension { - implementation = 'com.google.cloud.tools.jib.gradle.extension.layerfilter.JibLayerFilterExtension' - configuration { - filters { - filter { - glob = '/app/resources/**' - } - } - } - } - } } tasks.register('migrateDatabase') { @@ -375,7 +338,6 @@ rat.dependsOn prepareInputYaml spotbugsTest.dependsOn resolve compileTestJava.dependsOn ':fineract-client:processResources', ':fineract-avro-schemas:processResources' resolveMainClassName.dependsOn resolve -processResources.dependsOn compileJava javadoc { dependsOn resolve diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java index 7aa7816d976..cc0aa3b0c9a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java @@ -22,7 +22,6 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; -import org.apache.fineract.organisation.monetary.api.CurrenciesApiResourceSwagger.CurrencyItem; import org.apache.fineract.portfolio.note.data.NoteData; import org.apache.fineract.portfolio.paymenttype.api.PaymentTypeApiResourceSwagger.GetPaymentTypesResponse; @@ -69,6 +68,26 @@ private PostJournalEntriesTransactionIdResponse() { public Long officeId; } + public static final class CurrencyItem { + + private CurrencyItem() {} + + @Schema(example = "USD") + public String code; + @Schema(example = "US Dollar") + public String name; + @Schema(example = "2") + public Integer decimalPlaces; + @Schema(example = "100") + public Integer inMultiplesOf; + @Schema(example = "$") + public String displaySymbol; + @Schema(example = "currency.USD") + public String nameCode; + @Schema(example = "US Dollar ($)") + public String displayLabel; + } + static final class EnumOptionType { private EnumOptionType() {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java index 124da74b2d2..52ed3def884 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/ChargePaymentDTO.java @@ -21,6 +21,7 @@ import java.math.BigDecimal; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; @RequiredArgsConstructor @Getter @@ -29,4 +30,6 @@ public class ChargePaymentDTO { private final Long chargeId; private final BigDecimal amount; private final Long loanChargeId; + @Setter + private boolean accrualRecognized; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java index acbcb164c03..0a943e016e9 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsTransactionDTO.java @@ -46,6 +46,7 @@ public class SavingsTransactionDTO { private final BigDecimal overdraftAmount; private final boolean isAccountTransfer; private final List taxPayments; + private final Boolean isNegativeBalance; public boolean isOverdraftTransaction() { return this.overdraftAmount != null && this.overdraftAmount.doubleValue() > 0; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java index 8816d58f050..00ec3a78ff9 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java @@ -22,5 +22,5 @@ public interface AccountingProcessorForSavings { - void createJournalEntriesForSavings(SavingsDTO savingsDTO); + void createJournalEntriesForSavings(SavingsDTO savingsDTO, boolean isNegativeBalance); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 300ba5e47d6..bccb9694c40 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -199,6 +199,7 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting final boolean reversed = (Boolean) map.get("reversed"); final Long paymentTypeId = (Long) map.get("paymentTypeId"); final BigDecimal overdraftAmount = (BigDecimal) map.get("overdraftAmount"); + final Boolean isNegativeBalance = (Boolean) map.get("isNegativeBalance"); final List feePayments = new ArrayList<>(); final List penaltyPayments = new ArrayList<>(); @@ -206,12 +207,15 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting if (map.containsKey("savingsChargesPaid")) { @SuppressWarnings("unchecked") final List> savingsChargesPaidData = (List>) map.get("savingsChargesPaid"); - for (final Map loanChargePaid : savingsChargesPaidData) { - final Long chargeId = (Long) loanChargePaid.get("chargeId"); - final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); - final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); - final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); - final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + for (final Map savingsChargesPaid : savingsChargesPaidData) { + final Long chargeId = (Long) savingsChargesPaid.get("chargeId"); + final Long savingsChargeId = (Long) savingsChargesPaid.get("savingsChargeId"); + final boolean isPenalty = (Boolean) savingsChargesPaid.get("isPenalty"); + final boolean accrualRecognized = (Boolean) savingsChargesPaid.get("accrualRecognized"); + + final BigDecimal chargeAmountPaid = (BigDecimal) savingsChargesPaid.get("amount"); + ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, savingsChargeId); + chargePaymentDTO.setAccrualRecognized(accrualRecognized); if (isPenalty) { penaltyPayments.add(chargePaymentDTO); } else { @@ -238,7 +242,7 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting } final SavingsTransactionDTO transaction = new SavingsTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, transactionDate, transactionType, amount, reversed, feePayments, penaltyPayments, overdraftAmount, isAccountTransfer, - taxPayments); + taxPayments, isNegativeBalance); newSavingsTransactions.add(transaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index 72a5c4e3a82..5397617ab9b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -145,6 +145,10 @@ else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterest if (transactionType.isCapitalizedIncomeAmortizationAdjustment()) { createJournalEntriesForCapitalizedIncomeAmortizationAdjustment(loanDTO, loanTransactionDTO, office); } + // Handle Buy Down Fee + if (transactionType.isBuyDownFee()) { + createJournalEntriesForBuyDownFee(loanDTO, loanTransactionDTO, office); + } } } @@ -412,6 +416,26 @@ private void createJournalEntriesForCapitalizedIncomeAmortizationAdjustment(fina } } + private void createJournalEntriesForBuyDownFee(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office) { + // loan properties + final Long loanProductId = loanDTO.getLoanProductId(); + final Long loanId = loanDTO.getLoanId(); + final String currencyCode = loanDTO.getCurrencyCode(); + + // transaction properties + final String transactionId = loanTransactionDTO.getTransactionId(); + final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final BigDecimal amount = loanTransactionDTO.getAmount(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + + if (MathUtil.isGreaterThanZero(amount)) { + this.helper.createJournalEntriesForLoan(office, currencyCode, AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), loanProductId, paymentTypeId, loanId, transactionId, + transactionDate, amount); + } + } + private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { final Long loanProductId = loanDTO.getLoanProductId(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java index 4aa1b935bd8..ca54fbc17de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -38,7 +38,7 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc private final AccountingProcessorHelper helper; @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO, boolean isNegativeBalance) { final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsId = savingsDTO.getSavingsId(); @@ -162,9 +162,9 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savin transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); } } } @@ -182,9 +182,21 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { else if (savingsTransactionDTO.getTransactionType().isAccrual()) { // Post journal entry for Accrual Recognition if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + if (feePayments.size() > 0 || penaltyPayments.size() > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(), AccrualAccountsForSavings.INCOME_FROM_FEES.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getIsNegativeBalance()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } } } @@ -205,10 +217,15 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, penaltyPayments); if (isPositive) { + final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0); + AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeDebited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal, penaltyPayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, penaltyPayments); } } else { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, @@ -216,10 +233,15 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, feePayments); if (isPositive) { + final ChargePaymentDTO chargePaymentDTO = feePayments.get(0); + AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeDebited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - feePayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, feePayments); } } } @@ -227,13 +249,23 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { // Is the Charge a penalty? if (penaltyPayments.size() > 0) { + final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0); + AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeCredited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, penaltyPayments); } else { + final ChargePaymentDTO chargePaymentDTO = feePayments.get(0); + AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES; + if (chargePaymentDTO.isAccrualRecognized()) { + accountTypeToBeCredited = AccrualAccountsForSavings.FEES_RECEIVABLE; + } this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, feePayments); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java index 6b5f7f52e40..99e3e55ba81 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java @@ -38,7 +38,7 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess private final AccountingProcessorHelper helper; @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO, boolean isNegativeBalance) { final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsId = savingsDTO.getSavingsId(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 1aec8128059..4a0ead3f75e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -500,13 +500,16 @@ public void createJournalEntriesForSavings(final Map accountingB final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled"); final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); + final boolean isNegativeBalance = (accountingBridgeData.get("isNegativeBalance") != null) + ? (Boolean) accountingBridgeData.get("isNegativeBalance") + : false; if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory .determineProcessor(savingsDTO); - accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO); + accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO, isNegativeBalance); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index 224742d5970..2267b86a6b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -229,6 +229,7 @@ public void createLoanProductToGLAccountMapping(final Long loanProductId, final private void saveSavingsBaseAccountMapping(final Long savingProductId, final DepositAccountType accountType, final JsonCommand command, final JsonElement element) { // asset + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), savingProductId, CashAccountsForSavings.SAVINGS_REFERENCE.getValue()); @@ -303,6 +304,10 @@ public void createSavingProductToGLAccountMapping(final Long savingProductId, fi case ACCRUAL_PERIODIC: saveSavingsBaseAccountMapping(savingProductId, accountType, command, element); // assets + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, + SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), savingProductId, + AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue()); + this.savingsProductToGLAccountMappingHelper.saveSavingsToAssetAccountMapping(element, SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), savingProductId, AccrualAccountsForSavings.FEES_RECEIVABLE.getValue()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 30f98464e21..6159fa96e6c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -142,6 +142,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_READ", "READ_EXTERNAL_EVENT_CONFIGURATION") .requestMatchers(antMatcher(HttpMethod.PUT, "/api/*/externalevents/configuration")) .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_WRITE", "UPDATE_EXTERNAL_EVENT_CONFIGURATION") + // cache + .requestMatchers(antMatcher(HttpMethod.GET, "/api/*/caches")) + .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_READ", "READ_CACHE") + .requestMatchers(antMatcher(HttpMethod.PUT, "/api/*/caches")) + .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_WRITE", "UPDATE_CACHE") + // currency + .requestMatchers(antMatcher(HttpMethod.GET, "/api/*/currencies")) + .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_READ", "READ_CURRENCY") + .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/currencies")) + .hasAnyAuthority("ALL_FUNCTIONS", "ALL_FUNCTIONS_WRITE", "UPDATE_CURRENCY") // ... .requestMatchers(antMatcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() // .requestMatchers(antMatcher("/api/*/twofactor")).fullyAuthenticated() // diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java index 5050512d74d..ae8a14ea131 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java @@ -61,7 +61,7 @@ public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { String externalId = loan.getExternalId().getValue(); MonetaryCurrency loanCurrency = loan.getCurrency(); CurrencyDataV1 currency = CurrencyDataV1.newBuilder().setCode(loanCurrency.getCode()) - .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getCurrencyInMultiplesOf()).build(); + .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getInMultiplesOf()).build(); RepaymentDueDataV1 repaymentDue = getRepaymentDueData(repaymentInstallment, loanCurrency); diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java deleted file mode 100644 index fa02ea61748..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 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.organisation.monetary.api; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -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.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.data.ApplicationCurrencyConfigurationData; -import org.apache.fineract.organisation.monetary.data.request.CurrencyRequest; -import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService; -import org.springframework.stereotype.Component; - -@Path("/v1/currencies") -@Component -@Tag(name = "Currency", description = "Application related configuration around viewing/updating the currencies permitted for use within the MFI.") -@RequiredArgsConstructor -public class CurrenciesApiResource { - - private static final String RESOURCE_NAME_FOR_PERMISSIONS = "CURRENCY"; - - private final PlatformSecurityContext context; - private final OrganisationCurrencyReadPlatformService readPlatformService; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Currency Configuration", description = "Returns the list of currencies permitted for use AND the list of currencies not selected (but available for selection).\n" - + "\n" + "Example Requests:\n" + "\n" + "currencies\n" + "\n" + "\n" + "currencies?fields=selectedCurrencyOptions") - public ApplicationCurrencyConfigurationData retrieveCurrencies() { - - this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); - - return this.readPlatformService.retrieveCurrencyConfiguration(); - } - - @PUT - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = CurrencyRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CurrenciesApiResourceSwagger.PutCurrenciesResponse.class))) }) - public CommandProcessingResult updateCurrencies(@Parameter(hidden = true) CurrencyRequest currencyRequest) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .updateCurrencies() // - .withJson(toApiJsonSerializer.serialize(currencyRequest)) // - .build(); - - return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java deleted file mode 100644 index 67ac7f7e649..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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.organisation.monetary.api; - -import io.swagger.v3.oas.annotations.media.Schema; - -/** - * Created by sanyam on 14/8/17. - */ -public final class CurrenciesApiResourceSwagger { - - private CurrenciesApiResourceSwagger() { - - } - - public static final class CurrencyItem { - - private CurrencyItem() {} - - @Schema(example = "USD") - public String code; - @Schema(example = "US Dollar") - public String name; - @Schema(example = "2") - public Integer decimalPlaces; - @Schema(example = "100") - public Integer inMultiplesOf; - @Schema(example = "$") - public String displaySymbol; - @Schema(example = "currency.USD") - public String nameCode; - @Schema(example = "US Dollar ($)") - public String displayLabel; - } - - @Schema(description = "PutCurrenciesResponse") - public static final class PutCurrenciesResponse { - - private PutCurrenciesResponse() { - - } - - @Schema(example = "[\"KES\",\n" + " \"BND\",\n" + " \"LBP\",\n" + " \"GHC\",\n" + " \"USD\",\n" - + " \"XOF\",\n" + " \"AED\",\n" + " \"AMD\"]") - public String[] currencies; - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformServiceJpaRepositoryImpl.java index c42ea2e90e3..20d7f300f12 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/service/CurrencyWritePlatformServiceJpaRepositoryImpl.java @@ -20,21 +20,16 @@ import java.util.ArrayList; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.OrganisationCurrency; import org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepository; import org.apache.fineract.organisation.monetary.exception.CurrencyInUseException; -import org.apache.fineract.organisation.monetary.serialization.CurrencyCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService; @@ -43,30 +38,22 @@ @RequiredArgsConstructor public class CurrencyWritePlatformServiceJpaRepositoryImpl implements CurrencyWritePlatformService { - private final PlatformSecurityContext context; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final OrganisationCurrencyRepository organisationCurrencyRepository; - private final CurrencyCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final LoanProductReadPlatformService loanProductService; private final SavingsProductReadPlatformService savingsProductService; private final ChargeReadPlatformService chargeService; @Transactional @Override - public CommandProcessingResult updateAllowedCurrencies(final JsonCommand command) { + public CurrencyUpdateResponse updateAllowedCurrencies(final CurrencyUpdateRequest request) { + final var currencies = request.getCurrencies(); - this.context.authenticatedUser(); - - this.fromApiJsonDeserializer.validateForUpdate(command.json()); - - final String[] currencies = command.arrayValueOfParameterNamed("currencies"); - - final Map changes = new LinkedHashMap<>(); final List allowedCurrencyCodes = new ArrayList<>(); final Set allowedCurrencies = new HashSet<>(); for (final String currencyCode : currencies) { - final ApplicationCurrency currency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currencyCode); + final ApplicationCurrency currency = applicationCurrencyRepository.findOneWithNotFoundDetection(currencyCode); final OrganisationCurrency allowedCurrency = currency.toOrganisationCurrency(); @@ -74,9 +61,9 @@ public CommandProcessingResult updateAllowedCurrencies(final JsonCommand command allowedCurrencies.add(allowedCurrency); } - for (OrganisationCurrency priorCurrency : this.organisationCurrencyRepository.findAll()) { + for (OrganisationCurrency priorCurrency : organisationCurrencyRepository.findAll()) { if (!allowedCurrencyCodes.contains(priorCurrency.getCode())) { - // Check if it's safe to remove this currency. + // check if it's safe to remove this currency. if (!loanProductService.retrieveAllLoanProductsForCurrency(priorCurrency.getCode()).isEmpty() || !savingsProductService.retrieveAllForCurrency(priorCurrency.getCode()).isEmpty() || !chargeService.retrieveAllChargesForCurrency(priorCurrency.getCode()).isEmpty()) { @@ -85,14 +72,9 @@ public CommandProcessingResult updateAllowedCurrencies(final JsonCommand command } } - changes.put("currencies", allowedCurrencyCodes.toArray(new String[allowedCurrencyCodes.size()])); - - this.organisationCurrencyRepository.deleteAll(); - this.organisationCurrencyRepository.saveAll(allowedCurrencies); + organisationCurrencyRepository.deleteAll(); + organisationCurrencyRepository.saveAll(allowedCurrencies); - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .with(changes) // - .build(); + return CurrencyUpdateResponse.builder().currencies(allowedCurrencyCodes).build(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/starter/OrganisationMonetaryConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/starter/OrganisationMonetaryConfiguration.java index c35818e4744..0db76dd7b7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/starter/OrganisationMonetaryConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/starter/OrganisationMonetaryConfiguration.java @@ -18,10 +18,8 @@ */ package org.apache.fineract.organisation.monetary.starter; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepository; -import org.apache.fineract.organisation.monetary.serialization.CurrencyCommandFromApiJsonDeserializer; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformServiceImpl; import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService; @@ -41,19 +39,17 @@ public class OrganisationMonetaryConfiguration { @Bean @ConditionalOnMissingBean(CurrencyReadPlatformService.class) - public CurrencyReadPlatformService currencyReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate) { - return new CurrencyReadPlatformServiceImpl(context, jdbcTemplate); + public CurrencyReadPlatformService currencyReadPlatformService(JdbcTemplate jdbcTemplate) { + return new CurrencyReadPlatformServiceImpl(jdbcTemplate); } @Bean @ConditionalOnMissingBean(CurrencyWritePlatformService.class) - public CurrencyWritePlatformService currencyWritePlatformService(PlatformSecurityContext context, - ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, - OrganisationCurrencyRepository organisationCurrencyRepository, CurrencyCommandFromApiJsonDeserializer fromApiJsonDeserializer, - LoanProductReadPlatformService loanProductService, SavingsProductReadPlatformService savingsProductService, - ChargeReadPlatformService chargeService) { - return new CurrencyWritePlatformServiceJpaRepositoryImpl(context, applicationCurrencyRepository, organisationCurrencyRepository, - fromApiJsonDeserializer, loanProductService, savingsProductService, chargeService); + public CurrencyWritePlatformService currencyWritePlatformService(ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, + OrganisationCurrencyRepository organisationCurrencyRepository, LoanProductReadPlatformService loanProductService, + SavingsProductReadPlatformService savingsProductService, ChargeReadPlatformService chargeService) { + return new CurrencyWritePlatformServiceJpaRepositoryImpl(applicationCurrencyRepository, organisationCurrencyRepository, + loanProductService, savingsProductService, chargeService); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java index 7e395b20311..23256d51757 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferRepository.java @@ -39,4 +39,5 @@ public interface AccountTransferRepository @Query("select att from AccountTransferTransaction att where att.fromLoanTransaction.id IN :loanTransactions and att.reversed=false") List findByFromLoanTransactions(@Param("loanTransactions") Collection loanTransactions); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java similarity index 66% rename from fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java index 3421a4eff37..eb6e397d91c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AdjustAccountTransferCommandHandler.java @@ -16,32 +16,28 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.organisation.monetary.handler; +package org.apache.fineract.portfolio.account.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@CommandType(entity = "CURRENCY", action = "UPDATE") -public class UpdateCurrencyCommandHandler implements NewCommandSourceHandler { +@RequiredArgsConstructor +@CommandType(entity = "ACCOUNTTRANSFER", action = "ADJUST") +public class AdjustAccountTransferCommandHandler implements NewCommandSourceHandler { - private final CurrencyWritePlatformService writePlatformService; - - @Autowired - public UpdateCurrencyCommandHandler(final CurrencyWritePlatformService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final AccountTransfersWritePlatformService writePlatformService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.updateAllowedCurrencies(command); + return this.writePlatformService.adjust(command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java index 20210dd767c..e13a0701200 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformService.java @@ -29,6 +29,8 @@ public interface AccountTransfersWritePlatformService { CommandProcessingResult create(JsonCommand command); + CommandProcessingResult adjust(JsonCommand command); + void reverseTransfersWithFromAccountType(Long accountNumber, PortfolioAccountType accountTypeId); Long transferFunds(AccountTransferDTO accountTransferDTO); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index 7eea9969e62..5e909cf08e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -33,7 +33,9 @@ import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -71,6 +73,7 @@ import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.springframework.transaction.annotation.Transactional; +@Slf4j @RequiredArgsConstructor public class AccountTransfersWritePlatformServiceImpl implements AccountTransfersWritePlatformService { @@ -208,6 +211,37 @@ public CommandProcessingResult create(final JsonCommand command) { return builder.build(); } + @Transactional + @Override + public CommandProcessingResult adjust(JsonCommand command) { + + final Long accountTransferId = command.entityId(); + + Optional optAccountTransfer = this.accountTransferRepository.findById(accountTransferId); + if (!optAccountTransfer.isPresent()) { + throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.was.not.found", "Account transfer was not found"); + } + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); + + AccountTransferTransaction accountTransfer = optAccountTransfer.get(); + if (accountTransfer.getToSavingsTransaction() != null) { + log.debug("Reverse savings transfer to {} {}", accountTransfer.getToSavingsTransaction().getSavingsAccount().getAccountNumber(), + accountTransfer.getToSavingsTransaction().getId()); + savingsAccountDomainService.reverseTransfer(accountTransfer.getToSavingsTransaction(), backdatedTxnsAllowedTill); + } + if (accountTransfer.getFromSavingsTransaction() != null) { + log.debug("Reverse savings transfer from {} {}", + accountTransfer.getFromSavingsTransaction().getSavingsAccount().getAccountNumber(), + accountTransfer.getFromSavingsTransaction().getId()); + savingsAccountDomainService.reverseTransfer(accountTransfer.getFromSavingsTransaction(), backdatedTxnsAllowedTill); + } + + accountTransfer.reverse(); + this.accountTransferRepository.save(accountTransfer); + + return new CommandProcessingResultBuilder().withEntityId(accountTransferId).build(); + } + @Override @Transactional public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 7e5f7b8748d..25a9cf58a8d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -294,6 +294,10 @@ public String savingsProductChargeSchema() { return chargeSchema() + " join m_savings_product_charge spc on spc.charge_id = c.id"; } + public String savingsProductAccrualChargeSchema() { + return chargeSchema() + " join m_savings_product_accrual_charge spc on spc.charge_id = c.id"; + } + public String shareProductChargeSchema() { return chargeSchema() + " join m_share_product_charge mspc on mspc.charge_id = c.id"; } @@ -422,6 +426,17 @@ public List retrieveSavingsProductCharges(final Long savingsProductI return this.jdbcTemplate.query(sql, rm, new Object[] { savingsProductId }); // NOSONAR } + @Override + public Collection retrieveSavingsProductAccrualCharges(final Long savingsProductId) { + final ChargeMapper rm = new ChargeMapper(); + + String sql = "select " + rm.savingsProductAccrualChargeSchema() + + " where c.is_deleted=false and c.is_active=true and spc.savings_product_id=? "; + sql += addInClauseToSQL_toLimitChargesMappedToOffice_ifOfficeSpecificProductsEnabled(); + + return this.jdbcTemplate.query(sql, rm, new Object[] { savingsProductId }); // NOSONAR + } + @Override public List retrieveShareProductCharges(final Long shareProductId) { final ChargeMapper rm = new ChargeMapper(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupSummary.java deleted file mode 100644 index 7fc58a490ff..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupSummary.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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.group.data; - -import java.util.Collection; -import org.apache.fineract.organisation.monetary.data.MoneyData; - -public class GroupSummary { - - private final Long totalActiveClients; - private final Long totalChildGroups; - private final Collection totalLoanPortfolio; - private final Collection totalSavings; - - public GroupSummary(final Long totalActiveClients, final Long totalChildGroups, final Collection totalLoanPortfolio, - final Collection totalSavings) { - this.totalActiveClients = totalActiveClients; - this.totalChildGroups = totalChildGroups; - this.totalLoanPortfolio = totalLoanPortfolio; - this.totalSavings = totalSavings; - } - - public Long getTotalActiveClients() { - return this.totalActiveClients; - } - - public Long getTotalChildGroups() { - return this.totalChildGroups; - } - - public Collection getTotalLoanPortfolio() { - return this.totalLoanPortfolio; - } - - public Collection getTotalSavings() { - return this.totalSavings; - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index 813a8f272af..60441e2a788 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -596,6 +596,8 @@ private String executeTransaction(final Long loanId, final String loanExternalId commandRequest = builder.undoReAmortize(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) { commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) { + commandRequest = builder.makeLoanBuyDownFee(resolvedLoanId).build(); } if (commandRequest == null) { @@ -684,6 +686,8 @@ private String retrieveTransactionTemplate(Long loanId, String loanExternalIdStr } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) { transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId, LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId); + } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) { + transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId); } else { throw new UnrecognizedQueryParamException("command", commandParam); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java index 685ef6c0031..08fd930dfe8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java @@ -21,6 +21,7 @@ import lombok.Data; import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.mapstruct.Mapping; @@ -50,7 +51,7 @@ public class LoanPointInTimeData { private Long loanProductId; private String loanProductName; - @org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyData.Mapper.class, + @org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyMapper.class, LoanPrincipalData.Mapper.class, LoanInterestData.Mapper.class, LoanFeeData.Mapper.class, LoanPenaltyData.Mapper.class, LoanTotalAmountData.Mapper.class }) public interface Mapper { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index c546060ab1e..b36039fdb10 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -134,7 +134,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final ConfigurationDomainService configurationDomainService; private final HolidayRepository holidayRepository; private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final NoteRepository noteRepository; private final BusinessEventNotifierService businessEventNotifierService; @@ -464,7 +463,7 @@ private void handlePayDisbursementTransaction(final Loan loan, final Long charge chargesPayment.updateComponents(zero, zero, charge.getAmount(loan.getCurrency()), zero); chargesPayment.updateLoan(loan); loan.addLoanTransaction(chargesPayment); - loan.updateLoanOutstandingBalances(); + loanBalanceService.updateLoanOutstandingBalances(loan); charge.markAsFullyPaid(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java index d069b352f71..3e28eb854ef 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -177,7 +177,7 @@ public void processAccrualActivityForLoanReopen(final @NonNull Loan loan) { } // grab the latest AccrualActivityTransaction // it does not matter if it is on an installment due date or not because it was posted due to loan close - final Optional lastAccrualActivityMarkedToReverse = loanTransactionRepository + Optional lastAccrualActivityMarkedToReverse = loanTransactionRepository .findNonReversedByLoanAndType(loan, LoanTransactionType.ACCRUAL_ACTIVITY, PageRequest.of(0, 1)) // .stream().findFirst(); @@ -195,9 +195,10 @@ public void processAccrualActivityForLoanReopen(final @NonNull Loan loan) { return isDueBefore && isAfterOrEqualToLastAccrualDate; }).sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList(); - installments.forEach(installment -> { + for (LoanRepaymentScheduleInstallment installment : installments) { makeOrReplayActivity(loan, installment, lastAccrualActivityMarkedToReverse.orElse(null)); - }); + lastAccrualActivityMarkedToReverse = Optional.empty(); + } if (installments.isEmpty()) { lastAccrualActivityMarkedToReverse.ifPresent(this::reverseAccrualActivityTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index c3534e4411c..4f3aeb9b043 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -931,7 +931,7 @@ private void addUpdateIncomeAndAccrualTransaction(Loan loan, LoanInterestRecalcu createUpdateIncomePostingTransaction(loan, compoundingDetail, interest, fee, penalties, externalId); createUpdateAccrualTransaction(loan, compoundingDetail, interest, fee, penalties, feeDetails, externalId); - loan.updateLoanOutstandingBalances(); + loanBalanceService.updateLoanOutstandingBalances(loan); } private void createUpdateIncomePostingTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, @@ -1000,7 +1000,7 @@ private void processIncomeAndAccrualTransactionOnLoanClosure(Loan loan) { createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost); } - loan.updateLoanOutstandingBalances(); + loanBalanceService.updateLoanOutstandingBalances(loan); } private void determineCumulativeIncomeFromInstallments(final Loan loan, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 23d1f21e046..af94f04d306 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -261,20 +261,20 @@ public Loan assembleFrom(final JsonCommand command) { loanOfficer, loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, - isEnableInstallmentLevelDelinquency, submittedOnDate); + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, isEnableInstallmentLevelDelinquency, + submittedOnDate); } else if (group != null) { loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanAccountType, loanProduct, fund, loanOfficer, loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, - loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate); + isEnableInstallmentLevelDelinquency, submittedOnDate); } else if (client != null) { loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanAccountType, loanProduct, fund, loanOfficer, loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, - loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate); + isEnableInstallmentLevelDelinquency, submittedOnDate); } else { throw new IllegalStateException("No loan application exists for either a client or group (or both)."); } @@ -568,7 +568,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { changes.put(LoanApiConstants.productIdParameterName, newValue); loan.updateLoanProduct(loanProduct); final MonetaryCurrency currency = new MonetaryCurrency(loanProduct.getCurrency().getCode(), - loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getCurrencyInMultiplesOf()); + loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getInMultiplesOf()); loan.getLoanRepaymentScheduleDetail().setCurrency(currency); if (!changes.containsKey(LoanApiConstants.interestRateFrequencyTypeParameterName)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java index 8daee2dc691..428f0d4b2ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java @@ -63,6 +63,7 @@ public class LoanDisbursementService { private final LoanDisbursementValidator loanDisbursementValidator; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanChargeService loanChargeService; + private final LoanBalanceService loanBalanceService; public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map actualChanges) { final List disbursementList = loan.fetchDisbursementIds(); @@ -217,7 +218,7 @@ public void handleDisbursementTransaction(final Loan loan, final LocalDate disbu chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); chargesPayment.updateLoan(loan); loan.addLoanTransaction(chargesPayment); - loan.updateLoanOutstandingBalances(); + loanBalanceService.updateLoanOutstandingBalances(loan); } final LocalDate expectedDate = loan.getExpectedFirstRepaymentOnDate(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java index 1d99f0d56db..bc0479b4b74 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java @@ -55,7 +55,7 @@ public Map updateLoanRepaymentSchedule(final LoanProductRelatedD String currencyCode = loanRepaymentScheduleDetail.getCurrency().getCode(); Integer digitsAfterDecimal = loanRepaymentScheduleDetail.getCurrency().getDigitsAfterDecimal(); - Integer inMultiplesOf = loanRepaymentScheduleDetail.getCurrency().getCurrencyInMultiplesOf(); + Integer inMultiplesOf = loanRepaymentScheduleDetail.getCurrency().getInMultiplesOf(); final String digitsAfterDecimalParamName = "digitsAfterDecimal"; if (command.isChangeInIntegerParameterNamed(digitsAfterDecimalParamName, digitsAfterDecimal)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java index a8a358a3f82..825880c7ed5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java @@ -61,8 +61,8 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final LoanTermVariationsMapper loanMapper; private final InterestScheduleModelRepositoryWrapper modelRepository; - private final LoanTransactionService loanTransactionService; private final LoanBalanceService loanBalanceService; + private final LoanTransactionService loanTransactionService; @Override public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 43b64bf748a..c78483d5079 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -594,8 +594,8 @@ private void disburseLoan(JsonCommand command, boolean isPaymentTypeApplicableFo } if (loan.getLoanProduct().isMultiDisburseLoan() || loan.isProgressiveSchedule()) { - final List allNonContraTransactionsPostDisbursement = loanTransactionService - .retrieveListOfTransactionsForReprocessing(loan); + final List allNonContraTransactionsPostDisbursement = loanTransactionRepository + .findNonReversedTransactionsForReprocessingByLoan(loan); if (!allNonContraTransactionsPostDisbursement.isEmpty()) { reprocessLoanTransactionsService.reprocessTransactions(loan); } @@ -2246,7 +2246,7 @@ private void validateMultiDisbursementData(final JsonCommand command, LocalDate throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); } } else { - if (disbursementDataArray == null || disbursementDataArray.size() == 0) { + if (disbursementDataArray == null || disbursementDataArray.isEmpty()) { final String errorMessage = "For this loan product, disbursement details must be provided"; throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); } @@ -2821,7 +2821,7 @@ public CommandProcessingResult chargeOff(JsonCommand command) { final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null); loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } - final List loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); + final List loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); loanTransactions.add(chargeOffTransaction); reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions); loan.addLoanTransaction(chargeOffTransaction); @@ -3031,7 +3031,7 @@ private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loa final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; throw new LoanMultiDisbursementException(errorMessage); } - Integer trancheDisbursedCount = 0; + int trancheDisbursedCount = 0; for (LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()) { if (disbursementDetails.actualDisbursementDate() != null) { trancheDisbursedCount++; @@ -3076,6 +3076,7 @@ private Map undoDisbursal(final Loan loan, final ScheduleGenerat final boolean isScheduleRegenerateRequired = loan.isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate); loan.setActualDisbursementDate(null); loan.setDisbursedBy(null); + loan.setLastClosedBusinessDate(null); final boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(loan.getApprovedPrincipal(), loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount()); loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getApprovedPrincipal()); @@ -3361,13 +3362,13 @@ private Map undoLastDisbursal(final ScheduleGeneratorDTO schedul final Map actualChanges = new LinkedHashMap<>(); List loanTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.DISBURSEMENT); loanTransactions.sort(Comparator.comparing(LoanTransaction::getId)); - final LoanTransaction lastDisbursalTransaction = loanTransactions.get(loanTransactions.size() - 1); + final LoanTransaction lastDisbursalTransaction = loanTransactions.getLast(); final LocalDate lastTransactionDate = lastDisbursalTransaction.getTransactionDate(); existingTransactionIds.addAll(loanTransactionRepository.findTransactionIdsByLoan(loan)); existingReversedTransactionIds.addAll(loanTransactionRepository.findReversedTransactionIdsByLoan(loan)); - loanTransactions = loan.retrieveListOfTransactionsExcludeAccruals(); + loanTransactions = loanTransactionRepository.findNonReversedMonetaryTransactionsByLoan(loan); Collections.reverse(loanTransactions); for (final LoanTransaction previousTransaction : loanTransactions) { if (DateUtils.isBefore(lastTransactionDate, previousTransaction.getTransactionDate()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index 3cfcd1cef3c..2606174fdc2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; @@ -53,7 +54,7 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final EMICalculator emiCalculator; private final LoanRepositoryWrapper loanRepository; - private final LoanTransactionService loanTransactionService; + private final LoanTransactionRepository loanTransactionRepository; private final InterestScheduleModelRepositoryWrapper modelRepository; @Override @@ -81,8 +82,8 @@ private Optional getRelatedRepaymentScheduleIn } private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) { - final List transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan) - .stream().filter(t -> !t.isAccrualActivity()).toList(); + final List transactionsToReprocess = loanTransactionRepository + .findNonReversedTransactionsForReprocessingByLoan(loan).stream().filter(t -> !t.isAccrualActivity()).toList(); Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index 5092c3a62d0..acf391aab52 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -22,8 +22,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -37,7 +35,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; @@ -55,26 +52,18 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper; private final LoanBalanceService loanBalanceService; private final LoanTransactionRepository loanTransactionRepository; + private final LoanTransactionService loanTransactionService; @Override public void reprocessTransactions(final Loan loan) { - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(loan); + final List allNonContraTransactionsPostDisbursement = loanTransactionService + .retrieveListOfTransactionsForReprocessing(loan); + final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, allNonContraTransactionsPostDisbursement); handleChangedDetail(changedTransactionDetail); } - private List retrieveListOfTransactionsForReprocessing(final Loan loan) { - return loan.getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()) - .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); - } - - private Predicate loanTransactionForReprocessingPredicate() { - return transaction -> transaction.isNotReversed() - && (transaction.isChargeOff() || transaction.isReAge() || transaction.isAccrualActivity() || transaction.isReAmortize() - || !transaction.isNonMonetaryTransaction() || transaction.isContractTermination()); - } - @Override public void reprocessParticularTransactions(final Loan loan, final List loanTransactions) { final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions); @@ -83,7 +72,7 @@ public void reprocessParticularTransactions(final Loan loan, final List transactions = retrieveListOfTransactionsForReprocessing(loan); + final List transactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, transactions); handleChangedDetail(changedTransactionDetail); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/contracttermination/LoanContractTerminationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/contracttermination/LoanContractTerminationServiceImpl.java index 28ee5ba502d..b9cb5279760 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/contracttermination/LoanContractTerminationServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/contracttermination/LoanContractTerminationServiceImpl.java @@ -70,10 +70,10 @@ public class LoanContractTerminationServiceImpl { private final LoanUtilService loanUtilService; private final ExternalIdFactory externalIdFactory; private final BusinessEventNotifierService businessEventNotifierService; - private final LoanTransactionService loanTransactionService; private final LoanScheduleService loanScheduleService; private final LoanChargeValidator loanChargeValidator; private final ProgressiveLoanTransactionValidator loanTransactionValidator; + private final LoanTransactionService loanTransactionService; public CommandProcessingResult applyContractTermination(final JsonCommand command) { final Loan loan = loanAssembler.assembleFrom(command.getLoanId()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 04dd3aecb8e..e3f52a4d73d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -22,6 +22,7 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; @@ -35,7 +36,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; -import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -138,9 +138,9 @@ public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransa final LoanRepositoryWrapper loanRepositoryWrapper, final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule, - final LoanTransactionService loanTransactionService, final LoanChargeValidator loanChargeValidator, + final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService, - externalIdFactory, loanSchedule, loanTransactionService, loanChargeValidator, loanBalanceService); + externalIdFactory, loanSchedule, loanTransactionRepository, loanChargeValidator, loanBalanceService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index 15a96513efc..0e1fafb3856 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -103,6 +103,8 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeeWritePlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService; @@ -149,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; @@ -158,6 +161,7 @@ import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.portfolio.rate.service.RateAssembler; @@ -372,6 +376,16 @@ public LoanUtilService loanUtilService(ApplicationCurrencyRepositoryWrapper appl workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, calendarReadPlatformService, noteRepository); } + @Bean + @ConditionalOnMissingBean(BuyDownFeePlatformService.class) + public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator, + LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository, + PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster, + NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory) { + return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, + paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory); + } + @Bean @ConditionalOnMissingBean(LoanWritePlatformService.class) public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context, @@ -407,7 +421,7 @@ public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService, LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper, LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService, - LoanTransactionService loanTransactionService) { + LoanTransactionService loanTransactionService, BuyDownFeePlatformService buyDownFeePlatformService) { return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, @@ -457,9 +471,9 @@ public LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler(FromJso @ConditionalOnMissingBean(LoanDisbursementService.class) public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator, LoanDisbursementValidator loanDisbursementValidator, ReprocessLoanTransactionsService reprocessLoanTransactionsService, - LoanChargeService loanChargeService) { + LoanChargeService loanChargeService, LoanBalanceService loanBalanceService) { return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator, reprocessLoanTransactionsService, - loanChargeService); + loanChargeService, loanBalanceService); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index a8c62d09176..786a1fa7b6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -175,7 +175,7 @@ public class LoanProductsApiResource { @Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n" + "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n" + "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n" - + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n" + + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType,enableBuyDownFee\n\n" + "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n" diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index e55b18b8dd7..b791acf3538 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -200,7 +200,8 @@ public final class LoanProductDataValidator { LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_TYPE_PARAM_NAME, LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME, - LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME)); + LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME, + LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue())); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java index 7d0eec08f82..5ba28228f3c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java @@ -573,6 +573,9 @@ private String handleCommands(Long accountId, String externalId, String commandP } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT)) { final CommandWrapper commandRequest = builder.unblockSavingsAccount(accountId).build(); result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_ADD_ACCRUAL_TRANSACTION)) { + final CommandWrapper commandRequest = builder.addAccrualsToSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); } if (result == null) { @@ -582,7 +585,8 @@ private String handleCommands(Long accountId, String externalId, String commandP "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, - SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); + SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT, + SavingsApiConstants.COMMAND_ADD_ACCRUAL_TRANSACTION }); } return toApiJsonSerializer.serialize(result); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java index b814180e67e..b44fb8ce43a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java @@ -173,8 +173,9 @@ public String retrieveOne(@PathParam("productId") @Parameter(description = "prod SavingsProductData savingProductData = this.savingProductReadPlatformService.retrieveOne(productId); final Collection charges = this.chargeReadPlatformService.retrieveSavingsProductCharges(productId); + final Collection accrualCharges = this.chargeReadPlatformService.retrieveSavingsProductAccrualCharges(productId); - savingProductData = SavingsProductData.withCharges(savingProductData, charges); + savingProductData = SavingsProductData.withCharges(savingProductData, charges, accrualCharges); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java index 724a781d069..b5e74628ce2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java @@ -69,9 +69,19 @@ private PostSavingsCharges() {} public Integer interestCalculationDaysInYearType; @Schema(example = "1") public Integer accountingRule; - public Set charges; + public List charges; @Schema(example = "accountMappingForPayment") public String accountMappingForPayment; + @Schema(example = "false") + public Boolean withdrawalFeeForTransfers; + @Schema(example = "false") + public Boolean enforceMinRequiredBalance; + @Schema(example = "false") + public Boolean allowOverdraft; + @Schema(example = "false") + public Boolean withHoldTax; + @Schema(example = "false") + public Boolean isDormancyTrackingActive; } @Schema(description = "PostSavingsProductsResponse") @@ -317,7 +327,21 @@ private GetSavingsProductsFeeToIncomeAccountMappingsIncomeAccount() {} } public GetSavingsProductsFeeToIncomeAccountMappingsCharge charge; - public GetSavingsProductsFeeToIncomeAccountMappingsIncomeAccount incomeAccount; + public GetSavingsProductsGlAccount incomeAccount; + } + + static final class GetSavingsProductsCharge { + + private GetSavingsProductsCharge() {} + + @Schema(example = "12") + public Integer id; + @Schema(example = "12.34") + public BigDecimal amount; + @Schema(example = "Annual Fee") + public String name; + @Schema(example = "false") + public Boolean active; } static final class GetSavingsProductsPenaltyToIncomeAccountMappings { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index af3b000cfa8..efbe31a6f8a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -369,6 +369,12 @@ public SavingsAccount assembleFrom(final Long savingsId, DepositAccountType depo return account; } + public SavingsAccount getClientSavingAccount(final Long clientId, DepositAccountType depositAccountType) { + final SavingsAccount account = this.savingsAccountRepository.findSavingId(clientId, depositAccountType); + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + return account; + } + public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) { savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 7fe875bedd7..40f3b72a893 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -80,12 +80,12 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi @Autowired public DepositAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, - final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountNumberGenerator accountNumberGenerator, - final DepositAccountAssembler depositAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, - final AccountTransfersWritePlatformService accountTransfersWritePlatformService, - final ConfigurationDomainService configurationDomainService, - final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - final CalendarInstanceRepository calendarInstanceRepository) { + final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountNumberGenerator accountNumberGenerator, + final DepositAccountAssembler depositAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, + final AccountTransfersWritePlatformService accountTransfersWritePlatformService, + final ConfigurationDomainService configurationDomainService, + final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, + final CalendarInstanceRepository calendarInstanceRepository) { this.savingsAccountRepository = savingsAccountRepository; this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.accountNumberGenerator = accountNumberGenerator; @@ -100,8 +100,8 @@ public DepositAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savi @Transactional @Override public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean applyWithdrawFee, final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean applyWithdrawFee, final boolean isRegularTransaction) { boolean isAccountTransfer = false; boolean isInterestTransfer = false; boolean isWithdrawBalance = false; @@ -115,7 +115,7 @@ public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, @Transactional @Override public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) { boolean isAccountTransfer = false; boolean isRegularTransaction = false; final boolean backdatedTxnsAllowedTill = false; @@ -126,8 +126,8 @@ public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount accou @Transactional @Override public SavingsAccountTransaction handleRDDeposit(final RecurringDepositAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isRegularTransaction) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); @@ -159,8 +159,8 @@ public SavingsAccountTransaction handleRDDeposit(final RecurringDepositAccount a @Transactional @Override public SavingsAccountTransaction handleSavingDeposit(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isRegularTransaction) { boolean isAccountTransfer = false; final boolean backdatedTxnsAllowedTill = false; final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, @@ -185,7 +185,7 @@ private boolean isAnyActivationChargesDue(final RecurringDepositAccount account) @Transactional @Override public Long handleFDAccountClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); @@ -252,8 +252,8 @@ public Long handleFDAccountClosure(final FixedDepositAccount account, final Paym @Transactional @Override public Long handleFDAccountMaturityClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final DateTimeFormatter fmt, final LocalDate closedDate, final Integer onAccountClosureId, final Long toSavingsId, - final String transferDescription, Map changes) { + final DateTimeFormatter fmt, final LocalDate closedDate, final Integer onAccountClosureId, final Long toSavingsId, + final String transferDescription, Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -330,7 +330,7 @@ public Long handleFDAccountMaturityClosure(final FixedDepositAccount account, fi @Transactional @Override public Long handleRDAccountClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -443,7 +443,7 @@ private void autoGenerateAccountNumber(final SavingsAccount account) { @Transactional @Override public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -498,7 +498,7 @@ public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, f @Transactional @Override public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail, - final AppUser user, final JsonCommand command, final Map changes) { + final AppUser user, final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -550,13 +550,13 @@ public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount accoun } private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { + Set existingReversedTransactionIds) { existingTransactionIds.addAll(account.findExistingTransactionIds()); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer) { + final Set existingReversedTransactionIds, boolean isAccountTransfer) { final boolean backdatedTxnsAllowedTill = false; @@ -577,4 +577,4 @@ private void updateAlreadyPostedTransactions(final Set existingTransaction } } } -} +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 2855bc1e566..fde99a79011 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -149,7 +149,7 @@ public void modifyApplication(final JsonCommand command, final Map existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill, + final boolean isNegativeBalance) { final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + accountingBridgeData.put("isNegativeBalance", isNegativeBalance); this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); } @Transactional @Override public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds, - final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill, boolean isNegativeBalance) { final boolean isAccountTransfer = false; - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill, + isNegativeBalance); } @Override @@ -312,8 +313,8 @@ public SavingsAccountTransaction handleReversal(SavingsAccount account, List postingPeriods = account.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + log.debug("postInterest {}", postingPeriods.size()); + + MonetaryCurrency currency = account.getCurrency(); + Money interestPostedToDate = Money.zero(currency); + + if (backdatedTxnsAllowedTill) { + interestPostedToDate = Money.of(currency, account.getSummary().getTotalInterestPosted()); + } + + boolean recalucateDailyBalanceDetails = false; + boolean applyWithHoldTax = account.isWithHoldTaxApplicableForInterestPosting(); + final List withholdTransactions = new ArrayList<>(); + + if (backdatedTxnsAllowedTill) { + withholdTransactions.addAll(account.findWithHoldSavingsTransactionsWithPivotConfig()); + } else { + withholdTransactions.addAll(account.findWithHoldTransactions()); + } + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + log.debug(" period: {}", interestPostingPeriod.dateOfPostingTransaction()); + + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + log.debug(" interestEarnedToBePostedForPeriod: {}", interestEarnedToBePostedForPeriod.toString()); + + if (!interestPostingTransactionDate.isAfter(interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + SavingsAccountTransaction postingTransaction = null; + if (backdatedTxnsAllowedTill) { + postingTransaction = account.findInterestPostingSavingsTransactionWithPivotConfig(interestPostingTransactionDate); + } else { + postingTransaction = account.findInterestPostingTransactionFor(interestPostingTransactionDate); + } + if (postingTransaction == null) { + SavingsAccountTransaction newPostingTransaction = null; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + if (interestEarnedToBePostedForPeriod.isGreaterThan(Money.zero(currency))) { + newPostingTransaction = SavingsAccountTransaction.interestPosting(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + if (newPostingTransaction != null) { + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(newPostingTransaction); + } else { + account.addTransaction(newPostingTransaction); + } + if (account.savingsProduct().isAccrualBasedAccountingEnabled()) { + if (MathUtil.isGreaterThanZero(interestEarnedToBePostedForPeriod)) { + SavingsAccountTransaction accrualTransaction = SavingsAccountTransaction.accrual(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting(), false); + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(accrualTransaction); + } else { + account.addTransaction(accrualTransaction); + } + } else { + log.info("Accrual for Overdraft interest"); + } + } + if (applyWithHoldTax) { + account.createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + } + recalucateDailyBalanceDetails = true; + } else { + boolean correctionRequired = false; + if (postingTransaction.isInterestPostingAndNotReversed()) { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + } else { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); + } + log.debug(" correctionRequired {}", correctionRequired); + if (correctionRequired) { + boolean applyWithHoldTaxForOldTransaction = false; + postingTransaction.reverse(); + SavingsAccountTransaction reversal = null; + if (postReversals) { + reversal = SavingsAccountTransaction.reversal(postingTransaction); + } + final SavingsAccountTransaction withholdTransaction = account.findTransactionFor(interestPostingTransactionDate, + withholdTransactions); + if (withholdTransaction != null) { + withholdTransaction.reverse(); + applyWithHoldTaxForOldTransaction = true; + } + SavingsAccountTransaction newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + newPostingTransaction = SavingsAccountTransaction.interestPosting(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(newPostingTransaction); + if (reversal != null) { + account.addTransactionToExisting(reversal); + } + } else { + account.addTransaction(newPostingTransaction); + if (reversal != null) { + account.addTransaction(reversal); + } + } + if (account.savingsProduct().isAccrualBasedAccountingEnabled() + && MathUtil.isGreaterThanZero(interestEarnedToBePostedForPeriod)) { + log.info("TX2: {}", interestEarnedToBePostedForPeriod.getAmount()); + SavingsAccountTransaction accrualTransaction = SavingsAccountTransaction.accrual(account, account.office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting(), false); + if (backdatedTxnsAllowedTill) { + account.addTransactionToExisting(accrualTransaction); + } else { + account.addTransaction(accrualTransaction); + } + } else { + log.info("Accrual for Overdraft2 interest"); + } + if (applyWithHoldTaxForOldTransaction) { + account.createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + recalucateDailyBalanceDetails = true; + } + } + } + } + + if (recalucateDailyBalanceDetails) { + // no openingBalance concept supported yet but probably will to + // allow + // for migrations. + Money openingAccountBalance = Money.zero(currency); + + if (backdatedTxnsAllowedTill) { + if (account.getSummary().getLastInterestCalculationDate() == null) { + openingAccountBalance = Money.zero(currency); + } else { + openingAccountBalance = Money.of(currency, account.getSummary().getRunningBalanceOnPivotDate()); + } + } + + // update existing transactions so derived balance fields are + // correct. + account.recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); + } + + if (!backdatedTxnsAllowedTill) { + account.getSummary().updateSummary(currency, account.savingsAccountTransactionSummaryWrapper, account.getTransactions()); + } else { + account.getSummary().updateSummaryWithPivotConfig(currency, account.savingsAccountTransactionSummaryWrapper, null, + account.savingsAccountTransactions); + } + } + + @Override + public void reverseTransfer(SavingsAccountTransaction savingsAccountTransaction, boolean backdatedTxnsAllowedTill) { + final SavingsAccount account = savingsAccountTransaction.getSavingsAccount(); + account.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper); + + undoTransaction(account, savingsAccountTransaction); + } + + @Override + public void undoTransaction(SavingsAccount account, SavingsAccountTransaction savingsAccountTransaction) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Long savingsId = account.getId(); + final Long transactionId = savingsAccountTransaction.getId(); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), + account); + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + if (account.isNotActive()) { + throwValidationExceptionForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + + // undoing transaction is withdrawal then undo withdrawal fee transaction if any + if (savingsAccountTransaction.isWithdrawal()) { + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + } + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + checkClientOrGroupActive(account); + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + postInterest(account, mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + savingsAccountRepository.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, false); + } + + private void throwValidationExceptionForActiveStatus(final String actionName) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + @Override + public void checkClientOrGroupActive(final SavingsAccount account) { + final Client client = account.getClient(); + if (client != null) { + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + final Group group = account.group(); + if (group != null) { + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.java new file mode 100644 index 00000000000..25d83e436de --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/AddAccrualTransactionsToSavingsAccountCommandHandler.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.portfolio.savings.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SAVINGSACCOUNT", action = "ADD_ACCRUALS") +@RequiredArgsConstructor +public class AddAccrualTransactionsToSavingsAccountCommandHandler implements NewCommandSourceHandler { + + private final SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) throws MultiException { + + return savingsAccrualWritePlatformService.addAccrualEntries(command.getSavingsId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java new file mode 100644 index 00000000000..b9b61ccea86 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java @@ -0,0 +1,60 @@ +/** + * 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.savings.jobs.addaccrualtransactionforsavings; + +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class AddAccrualTransactionForSavingsConfig { + + @Autowired + private JobRepository jobRepository; + @Autowired + private PlatformTransactionManager transactionManager; + @Autowired + private SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Bean + protected Step addAccrualTransactionForSavingsStep() { + return new StepBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository) + .tasklet(addAccrualTransactionForSavingsTasklet(), transactionManager).build(); + } + + @Bean + public Job addAccrualTransactionForSavingsJob() { + return new JobBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name(), jobRepository) + .start(addAccrualTransactionForSavingsStep()).incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public AddAccrualTransactionForSavingsTasklet addAccrualTransactionForSavingsTasklet() { + return new AddAccrualTransactionForSavingsTasklet(savingsAccrualWritePlatformService); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java new file mode 100644 index 00000000000..5638221996b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java @@ -0,0 +1,50 @@ +/** + * 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.savings.jobs.addaccrualtransactionforsavings; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.savings.service.SavingsAccrualWritePlatformService; +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.repeat.RepeatStatus; + +@RequiredArgsConstructor +public class AddAccrualTransactionForSavingsTasklet implements Tasklet { + + private final SavingsAccrualWritePlatformService savingsAccrualWritePlatformService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + try { + addPeriodicAccruals(DateUtils.getBusinessLocalDate()); + } catch (MultiException e) { + throw new JobExecutionException(e); + } + return RepeatStatus.FINISHED; + } + + private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException { + savingsAccrualWritePlatformService.addAccrualEntries(tilldate); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 79b94094a0e..3f2afc2de50 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -441,7 +441,7 @@ private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransact @Transactional @Override public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { boolean isRegularTransaction = true; @@ -543,14 +543,14 @@ private void postInterest(final SavingsAccount account) { @Override public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final boolean allowAccountTransferModification) { + @SuppressWarnings("unused") final boolean allowAccountTransferModification) { throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); } @Override public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { + final boolean allowAccountTransferModification) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -629,7 +629,7 @@ public CommandProcessingResult undoRDTransaction(final Long savingsId, final Lon @Override public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final JsonCommand command) { + @SuppressWarnings("unused") final JsonCommand command) { throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); } @@ -903,7 +903,7 @@ public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, fin @Override public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -935,7 +935,7 @@ public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, f @Override public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -976,7 +976,7 @@ public void rejectSavingsTransfer(final Long accountId, final DepositAccountType @Override public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { + final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1131,7 +1131,7 @@ public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand comm @Transactional @Override public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1188,7 +1188,7 @@ public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Lo @Transactional @Override public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { this.context.authenticatedUser(); final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); @@ -1210,7 +1210,7 @@ public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAcco @Override public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { this.context.authenticatedUser(); @@ -1260,7 +1260,7 @@ public CommandProcessingResult payCharge(final Long savingsAccountId, final Long @Transactional @Override public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); // always use current date as transaction date for batch job @@ -1274,7 +1274,7 @@ public void applyChargeDue(final Long savingsAccountChargeId, final Long account @Transactional private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, - final DateTimeFormatter formatter) { + final DateTimeFormatter formatter) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1363,13 +1363,13 @@ public void updateMaturityDetails(Long depositAccountId, DepositAccountType depo } private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { + Set existingReversedTransactionIds) { existingTransactionIds.addAll(account.findExistingTransactionIds()); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { + final Set existingReversedTransactionIds) { boolean isAccountTransfer = false; final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), @@ -1405,4 +1405,4 @@ public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAcc isRegularTransaction); } -} +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 02bc5f34009..77f57a18441 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -234,6 +234,17 @@ public CommandProcessingResult submitRDApplication(final JsonCommand command) { account.updateAccountNo(this.accountNumberGenerator.generate(account, accountNumberFormat)); } + final Long savingsAccountId = command.longValueOfParameterNamed(DepositsApiConstants.linkedAccountParamName); + if (savingsAccountId != null) { + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, + DepositAccountType.SAVINGS_DEPOSIT); + this.depositAccountDataValidator.validatelinkedSavingsAccount(savingsAccount, account); + boolean isActive = true; + final AccountAssociations accountAssociations = AccountAssociations.associateSavingsAccount(account, savingsAccount, + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); + this.accountAssociationsRepository.save(accountAssociations); + } + final Long savingsId = account.getId(); final CalendarInstance calendarInstance = getCalendarInstance(command, account); this.calendarInstanceRepository.save(calendarInstance); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java index 59d7e04da25..ab73f9238de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java @@ -21,6 +21,7 @@ import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; +import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -33,6 +34,7 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.DepositAccountType; @@ -60,28 +62,63 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) { Money interestPostedToDate = Money.zero(savingsAccountData.getCurrency()); LocalDate startInterestDate = getStartInterestCalculationDate(savingsAccountData); - if (backdatedTxnsAllowedTill && savingsAccountData.getSummary().getInterestPostedTillDate() != null) { interestPostedToDate = Money.of(savingsAccountData.getCurrency(), savingsAccountData.getSummary().getTotalInterestPosted()); savingsAccountData.setStartInterestCalculationDate(savingsAccountData.getSummary().getInterestPostedTillDate()); } else { savingsAccountData.setStartInterestCalculationDate(startInterestDate); } - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, savingsAccountData); - boolean recalucateDailyBalanceDetails = false; boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(savingsAccountData); final List withholdTransactions = new ArrayList<>(); withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig(savingsAccountData)); + Boolean flagValidationInterest = false; + Boolean flagValidationOverdraft = false; for (final PostingPeriod interestPostingPeriod : postingPeriods) { final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, + savingsAccountData); + + if (postingTransaction == null) { + SavingsAccountTransactionData newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.getCurrency()))) { + flagValidationInterest = true; + } else { + flagValidationOverdraft = true; + } + } else { + boolean correctionRequired = false; + if (postingTransaction.isInterestPostingAndNotReversed()) { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + } else { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); + } + if (DateUtils.isBefore(interestPostingTransactionDate, interestPostingUpToDate)) { + correctionRequired = false; + } + if (correctionRequired) { + if (interestEarnedToBePostedForPeriod.isGreaterThanZero() || interestEarnedToBePostedForPeriod.isLessThanZero()) { + flagValidationInterest = true; + } + } + } + } + } + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + final Boolean isNegativeBalance = MathUtil.isLessThanZero(interestPostingPeriod.getInterestEarned()); + if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, @@ -92,14 +129,21 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.getCurrency()))) { newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); } else { newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), - interestPostingPeriod.isUserPosting()); - } + interestPostingPeriod.isUserPosting(), isNegativeBalance); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + } savingsAccountData.updateTransactions(newPostingTransaction); - + if (savingsAccountData.getSavingsProductData().isAccrualBasedAccountingEnabled()) { + savingsAccountData.updateTransactions(SavingsAccountTransactionData.accrual(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting())); + } if (applyWithHoldTax) { createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, savingsAccountData); @@ -112,9 +156,11 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int } else { correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); } + if (DateUtils.isBefore(interestPostingTransactionDate, interestPostingUpToDate)) { + correctionRequired = false; + } if (correctionRequired) { boolean applyWithHoldTaxForOldTransaction = false; - postingTransaction.reverse(); final SavingsAccountTransactionData withholdTransaction = findTransactionFor(interestPostingTransactionDate, withholdTransactions); @@ -128,14 +174,24 @@ public SavingsAccountData postInterest(final MathContext mc, final LocalDate int newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); + } else { newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), - interestPostingPeriod.isUserPosting()); + interestPostingPeriod.isUserPosting(), isNegativeBalance); + newPostingTransaction.setFlagValidationOverdraft(flagValidationOverdraft); + newPostingTransaction.setFlagValidationInterest(flagValidationInterest); } - savingsAccountData.updateTransactions(newPostingTransaction); + if (savingsAccountData.getSavingsProductData().isAccrualBasedAccountingEnabled()) { + savingsAccountData.updateTransactions( + SavingsAccountTransactionData.accrual(savingsAccountData, interestPostingTransactionDate, + interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting())); + } if (applyWithHoldTaxForOldTransaction) { createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, savingsAccountData); @@ -232,7 +288,6 @@ public List calculateInterestUsing(final MathContext mc, final Lo final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( savingsAccountData.getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, postedAsOnDates); - final List allPostingPeriods = new ArrayList<>(); Money periodStartingBalance; @@ -268,16 +323,73 @@ public List calculateInterestUsing(final MathContext mc, final Lo if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { isUserPosting = true; } - final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, - retreiveOrderedNonInterestPostingTransactions(savingsAccountData), monetaryCurrency, compoundingPeriodType, - interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, - interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, - isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, - isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft()); + List listOfTransactions = retreiveOrderedNonInterestPostingTransactions(savingsAccountData); + + List listOfTransactionsNegative = new ArrayList<>(); + List listOfTransactionsPositive = new ArrayList<>(); + boolean firstIsNegative = false; + boolean firstIsSet = false; + + for (SavingsAccountTransactionData lists : listOfTransactions) { + + if (MathUtil.isLessThanZero(lists.getRunningBalance()) + && periodInterval.startDate().getMonth() == lists.getDate().getMonth()) { + listOfTransactionsNegative.add(lists); + if (!firstIsSet) { + firstIsNegative = true; + firstIsSet = true; + } + } else if (periodInterval.startDate().getMonth() == lists.getDate().getMonth()) { + BigDecimal bd = new BigDecimal(String.valueOf(lists.getRunningBalance())).setScale(2, RoundingMode.DOWN); + if (!MathUtil.isZero(bd)) { + listOfTransactionsPositive.add(lists); + if (!firstIsSet) { + firstIsNegative = false; + firstIsSet = true; + } + } + } + + } + List firstList = null; + List secondList = null; - periodStartingBalance = postingPeriod.closingBalance(); + if (firstIsNegative) { + firstList = listOfTransactionsNegative; + secondList = listOfTransactionsPositive; + } else { + firstList = listOfTransactionsPositive; + secondList = listOfTransactionsNegative; + } + Boolean flagIntroduce = false; + + if (!firstList.isEmpty()) { + final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, firstList, + monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), + upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, + isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), flagIntroduce); + flagIntroduce = postingPeriod.getEndTransaction(); + periodStartingBalance = postingPeriod.closingBalance(); + if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount()) + && MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) { + allPostingPeriods.add(postingPeriod); + } + } - allPostingPeriods.add(postingPeriod); + if (!secondList.isEmpty()) { + final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, secondList, + monetaryCurrency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), + upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, + isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isUserPosting, financialYearBeginningMonth, savingsAccountData.isAllowOverdraft(), flagIntroduce); + + periodStartingBalance = postingPeriod.closingBalance(); + if (!(MathUtil.isZero(postingPeriod.getOpeningBalance().getAmount()) + && MathUtil.isZero(postingPeriod.closingBalance().getAmount()))) { + allPostingPeriods.add(postingPeriod); + } + } } this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, allPostingPeriods, @@ -428,7 +540,8 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final } if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) { transaction.updateOverdraftAmount(overdraftAmount.getAmount()); - } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount()))) { + } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount())) + && MathUtil.isGreaterThanZero(transaction.getRunningBalance())) { SavingsAccountTransactionData accountTransaction = SavingsAccountTransactionData.copyTransaction(transaction); if (transaction.isChargeTransaction()) { Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); @@ -444,6 +557,26 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final accountTransaction.updateRunningBalance(runningBalance); addTransactionToExisting(accountTransaction, savingsAccountData); + isTransactionsModified = true; + } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.getCurrency(), transaction.getOverdraftAmount())) + && MathUtil.isLessThanZero(transaction.getRunningBalance())) { + SavingsAccountTransactionData accountTransaction = transaction; + if (transaction.isChargeTransaction()) { + Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); + final Set newChargePaidBy = new HashSet<>(); + chargesPaidBy.forEach( + x -> newChargePaidBy.add(SavingsAccountChargesPaidByData.instance(x.getChargeId(), x.getAmount()))); + transaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy); + } + // if (MathUtil.isGreaterThanZero(savingsAccountData.getSummary().getAccountBalance())){ + // transaction.reverse(); + // } + if (overdraftAmount.isGreaterThanZero()) { + transaction.updateOverdraftAmount(overdraftAmount.getAmount()); + } + transaction.updateRunningBalance(runningBalance); + // addTransactionToExisting(accountTransaction, savingsAccountData); + isTransactionsModified = true; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index f84962affc8..8606522e5aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -31,6 +31,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.glaccount.data.GLAccountData; @@ -42,12 +44,17 @@ import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksReadService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.group.data.GroupGeneralData; +import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.savings.DepositAccountType; @@ -64,7 +71,9 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; @@ -80,12 +89,15 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +@Slf4j public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountReadPlatformService { private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; private final DatabaseSpecificSQLGenerator sqlGenerator; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; // mappers private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; @@ -104,8 +116,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, - final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { + final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.context = context; this.jdbcTemplate = jdbcTemplate; this.sqlGenerator = sqlGenerator; @@ -118,6 +130,7 @@ public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte this.paginationHelper = paginationHelper; this.savingAccountMapperForInterestPosting = new SavingAccountMapperForInterestPosting(); this.savingAccountAssembler = savingAccountAssembler; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } @Override @@ -251,6 +264,7 @@ public List retrieveAllSavingsDataForInterestPosting(final b new Object[] { maxSavingsId, status, pageSize, yesterday }); for (SavingsAccountData savingsAccountData : savingsAccountDataList) { this.savingAccountAssembler.assembleSavings(savingsAccountData); + log.debug(" to process {} as {}", savingsAccountData.getAccountNo(), savingsAccountData.getDepositType().getValue()); } return savingsAccountDataList; } @@ -315,7 +329,7 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); - sqlBuilder.append("sp.id as productId, "); + sqlBuilder.append("sp.id as productId, sp.name as productName, "); sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); @@ -334,7 +348,10 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append( "msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, "); sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, "); - sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, "); + sqlBuilder.append( + "apm2.gl_account_id as glAccountIdForInterestReceivableNegative, apm1.gl_account_id as glAccountIdForInterestOnSavings, apm.gl_account_id as glAccountIdForSavingsControl, apm3.gl_account_id as glAccountIdForOverdraftPorfolioNegative, "); + sqlBuilder.append( + "apm4.gl_account_id as glAccountIdForSavingsControlAcountPositiveInterestNegative, apm5.gl_account_id as glAccountIdForInterestReceivablePositiveInterestNegative, "); sqlBuilder.append( "mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage "); sqlBuilder.append("from m_savings_account sa "); @@ -351,9 +368,13 @@ private static final class SavingAccountMapperForInterestPosting implements Resu sqlBuilder.append("left join m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = tr.id "); sqlBuilder.append("left join m_tax_component mtc on mtc.id = txd.tax_component_id "); sqlBuilder.append( - "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=3 "); + "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=2 "); sqlBuilder.append( - "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=2 "); + "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=17 "); + sqlBuilder.append("left join acc_product_mapping apm2 on apm2.product_id = sp.id and apm2.financial_account_type=18 "); + sqlBuilder.append("left join acc_product_mapping apm3 on apm3.product_id = sp.id and apm3.financial_account_type = 11 "); + sqlBuilder.append("left join acc_product_mapping apm4 on apm4.product_id = sp.id and apm4.financial_account_type = 2 "); + sqlBuilder.append("left join acc_product_mapping apm5 on apm5.product_id = sp.id and apm5.financial_account_type = 18 "); this.schemaSql = sqlBuilder.toString(); } @@ -407,12 +428,21 @@ public List extractData(final ResultSet rs) throws SQLExcept final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings"); final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl"); + final Long glAccountIdForOverdraftPorfolioNegative = rs.getLong("glAccountIdForOverdraftPorfolioNegative"); + final Long glAccountIdForInterestReceivableNegative = rs.getLong("glAccountIdForInterestReceivableNegative"); + + final Long glAccountIdForSavingsControlAcountPositiveInterestNegative = rs + .getLong("glAccountIdForSavingsControlAcountPositiveInterestNegative"); + final Long glAccountIdForInterestReceivablePositiveInterestNegative = rs + .getLong("glAccountIdForInterestReceivablePositiveInterestNegative"); + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); final Integer accountType = rs.getInt("accountingType"); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType); final EnumOptionData enumOptionDataForAccounting = new EnumOptionData(accountType.longValue(), accountingRuleType.getCode(), accountingRuleType.getValue().toString()); - final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, + final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, productName, enumOptionDataForAccounting); final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); @@ -563,6 +593,15 @@ public List extractData(final ResultSet rs) throws SQLExcept savingsAccountData.setClientData(clientData); savingsAccountData.setGroupGeneralData(groupGeneralData); savingsAccountData.setSavingsProduct(savingsProductData); + + savingsAccountData.setGlAccountIdForInterestReceivableNegative(glAccountIdForInterestReceivableNegative); + savingsAccountData.setGlAccountIdForOverdraftPorfolioNegative(glAccountIdForOverdraftPorfolioNegative); + + savingsAccountData.setGlAccountIdForSavingsControlAcountPositiveInterestNegative( + glAccountIdForSavingsControlAcountPositiveInterestNegative); + savingsAccountData.setGlAccountIdForInterestReceivablePositiveInterestNegative( + glAccountIdForInterestReceivablePositiveInterestNegative); + savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); } @@ -1386,4 +1425,107 @@ public List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long public Long retrieveAccountIdByExternalId(final ExternalId externalId) { return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); } + + @Override + public Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) { + final SavingAccrualMapper mapper = new SavingAccrualMapper(); + final StringBuilder sqlBuilder = new StringBuilder(400); + Map paramMap = new HashMap<>(3); + sqlBuilder.append(" select " + mapper.schema() + " where "); + + sqlBuilder.append(" savings.status_enum = :active "); + sqlBuilder.append(" and (savings.nominal_annual_interest_rate is not null and savings.nominal_annual_interest_rate > 0) "); + sqlBuilder.append(" and msp.accounting_type = :type "); + sqlBuilder.append(" and (savings.closedon_date <= :tillDate or savings.closedon_date is null) "); + sqlBuilder.append(" and (savings.accrued_till_date <= :tillDate or savings.accrued_till_date is null) "); + if (savings != null) { + sqlBuilder.append(" and savings.id = " + savings.getId()); + } + sqlBuilder.append(" order by savings.id "); + paramMap.put("active", SavingsAccountStatusType.ACTIVE.getValue()); + paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); + paramMap.put("tillDate", tillDate); + try { + return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); + } catch (EmptyResultDataAccessException e) { + return new ArrayList<>(); + } + } + + private static final class SavingAccrualMapper implements RowMapper { + + private final String schemaSql; + + SavingAccrualMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append( + " savings.id as savingsId, savings.status_enum as status, (CASE WHEN savings.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId, "); + sqlBuilder.append( + " savings.accrued_till_date as accruedTill, savings.product_id as productId, savings.deposit_type_enum as depositType, "); + sqlBuilder.append(" savings.account_no as accountNo, savings.nominal_annual_interest_rate as nominalAnnualIterestRate, "); + sqlBuilder.append(" savings.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append(" savings.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append(" savings.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append(" savings.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append(" savings.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + sqlBuilder.append(" savings.interest_posted_till_date as postedTill, tg.id as taxGroupId, "); + sqlBuilder.append( + " savings.currency_code as currencyCode, savings.currency_digits as currencyDigits, savings.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append( + " curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode "); + sqlBuilder.append(" from m_savings_account savings "); + sqlBuilder.append(" left join m_savings_product msp on msp.id = savings.product_id "); + sqlBuilder.append(" left join m_client mc on mc.id = savings.client_id "); + sqlBuilder.append(" left join m_group mg on mg.id = savings.group_id "); + sqlBuilder.append(" left join m_currency curr on curr.code = savings.currency_code "); + sqlBuilder.append(" left join m_tax_group tg on tg.id = savings.tax_group_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccrualData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long savingsId = rs.getLong("savingsId"); + final String accountNo = rs.getString("accountNo"); + final Long productId = rs.getLong("productId"); + final Long officeId = rs.getLong("officeId"); + final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); + final LocalDate postedTill = JdbcSupport.getLocalDate(rs, "postedTill"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualIterestRate = rs.getBigDecimal("nominalAnnualIterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + return new SavingsAccrualData(savingsId, accountNo, depositType, null, productId, officeId, accruedTill, postedTill, currency, + nominalAnnualIterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, BigDecimal.ZERO); + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java new file mode 100644 index 00000000000..65b6a1e0a16 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.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.portfolio.savings.service; + +import java.time.LocalDate; +import java.util.Collection; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; + +public interface SavingsAccrualWritePlatformService { + + void addAccrualEntries(LocalDate tillDate) throws MultiException; + + boolean isChargeToBeRecognizedAsAccrual(Collection chargeIds, SavingsAccountCharge savingsAccountCharge); + + SavingsAccountTransaction addSavingsChargeAccrualTransaction(SavingsAccount savingsAccount, SavingsAccountCharge savingsAccountCharge, + LocalDate transactionDate); + + CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws MultiException; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java new file mode 100644 index 00000000000..e495bb29e65 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java @@ -0,0 +1,278 @@ +/** + * 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.savings.service; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; +import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargePaidBy; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsHelper; +import org.apache.fineract.portfolio.savings.domain.interest.CompoundInterestValues; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; +import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SavingsAccrualWritePlatformServiceImpl implements SavingsAccrualWritePlatformService { + + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final SavingsHelper savingsHelper; + private final ConfigurationDomainService configurationDomainService; + private final SavingsAccountDomainService savingsAccountDomainService; + + @Transactional + @Override + public void addAccrualEntries(LocalDate tillDate) throws JobExecutionException { + final Collection savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, + null); + final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final MathContext mc = MoneyHelper.getMathContext(); + + List errors = new ArrayList<>(); + for (SavingsAccrualData savingsAccrual : savingsAccrualData) { + try { + SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccrual.getId(), false); + LocalDate fromDate = savingsAccrual.getAccruedTill(); + if (fromDate == null) { + fromDate = savingsAccount.getActivationDate(); + } + log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate); + addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth, + isSavingsInterestPostingAtCurrentPeriodEnd, mc); + } catch (Exception e) { + log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage()); + errors.add(e.getCause()); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + } + + @Transactional + @Override + public CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws JobExecutionException { + SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccountId, false); + final LocalDate tillDate = DateUtils.getBusinessLocalDate(); + final Collection savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, + savingsAccount); + final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final MathContext mc = MoneyHelper.getMathContext(); + + List errors = new ArrayList<>(); + for (SavingsAccrualData savingsAccrual : savingsAccrualData) { + try { + LocalDate fromDate = savingsAccrual.getAccruedTill(); + if (fromDate == null) { + fromDate = savingsAccount.getActivationDate(); + } + log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate); + addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth, + isSavingsInterestPostingAtCurrentPeriodEnd, mc); + } catch (Exception e) { + log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage()); + errors.add(e.getCause()); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + + return CommandProcessingResult.empty(); + } + + @Override + public boolean isChargeToBeRecognizedAsAccrual(final Collection chargeIds, final SavingsAccountCharge savingsAccountCharge) { + if (chargeIds.isEmpty()) { + return false; + } + return chargeIds.contains(savingsAccountCharge.getCharge().getId()); + } + + @Transactional + @Override + public SavingsAccountTransaction addSavingsChargeAccrualTransaction(SavingsAccount savingsAccount, + SavingsAccountCharge savingsAccountCharge, LocalDate transactionDate) { + final MonetaryCurrency currency = savingsAccount.getCurrency(); + final Money chargeAmount = savingsAccountCharge.getAmount(currency); + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, savingsAccount.office(), + transactionDate, chargeAmount, false, false); + final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(savingsAccountTransaction, savingsAccountCharge, + savingsAccountTransaction.getAmount(currency).getAmount()); + savingsAccountTransaction.getSavingsAccountChargesPaid().add(chargePaidBy); + + savingsAccount.addTransaction(savingsAccountTransaction); + return savingsAccountTransaction; + } + + private void addAccrualTransactions(SavingsAccount savingsAccount, final LocalDate fromDate, final LocalDate tillDate, + final Integer financialYearBeginningMonth, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final MathContext mc) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + Boolean isNegativeBalance = false; + existingTransactionIds.addAll(savingsAccount.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(savingsAccount.findExistingReversedTransactionIds()); + + List postedAsOnTransactionDates = savingsAccount.getManualPostingDates(); + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType + .fromInt(savingsAccount.getInterestCalculationType()); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(savingsAccount.getInterestPostingPeriodType()); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(savingsAccount.getInterestCalculationDaysInYearType()); + + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(fromDate, tillDate, + postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates); + + final List allPostingPeriods = new ArrayList<>(); + final MonetaryCurrency currency = savingsAccount.getCurrency(); + Money periodStartingBalance = Money.zero(currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(savingsAccount.getInterestCalculationType()); + final BigDecimal interestRateAsFraction = savingsAccount.getEffectiveInterestRateAsFraction(mc, tillDate); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(savingsAccount.getId()); + boolean isInterestTransfer = false; + final Money minBalanceForInterestCalculation = Money.of(currency, savingsAccount.getMinBalanceForInterestCalculation()); + List savingsAccountTransactionDetailsForPostingPeriodList = savingsAccount + .toSavingsAccountTransactionDetailsForPostingPeriodList(); + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + if (DateUtils.isDateInTheFuture(periodInterval.endDate())) { + continue; + } + final boolean isUserPosting = (postedAsOnTransactionDates.contains(periodInterval.endDate())); + + final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriodList, currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), tillDate, interestPostTransactions, isInterestTransfer, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, + financialYearBeginningMonth); + + postingPeriod.setOverdraftInterestRateAsFraction(savingsAccount.getNominalAnnualInterestRateOverdraft()); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + BigDecimal compoundedInterest = BigDecimal.ZERO; + BigDecimal unCompoundedInterest = BigDecimal.ZERO; + final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest); + + final List accrualTransactionDates = savingsAccount.retreiveOrderedAccrualTransactions().stream() + .map(transaction -> transaction.getTransactionDate()).toList(); + + final List accrualTransactionDatesReverse = savingsAccount.retreiveOrderedAccrualTransactions().stream() + .filter(transaction -> transaction.isReversed()).map(transaction -> transaction.getTransactionDate()).toList(); + + LocalDate accruedTillDate = fromDate; + for (PostingPeriod period : allPostingPeriods) { + LocalDate valueDate = period.getPeriodInterval().endDate(); + List foundDate = accrualTransactionDates.stream().filter(date -> date.equals(valueDate)).toList(); + List foundDateReverse = accrualTransactionDatesReverse.stream().filter(date -> date.equals(valueDate)).toList(); + if (MathUtil.isGreaterThanZero(period.closingBalance())) { + isNegativeBalance = false; + period.setAccrual(true); + period.setNegative(MathUtil.isLessThanZero(savingsAccount.getSummary().getAccountBalance())); + period.calculateInterest(compoundInterestValues); + log.debug(" period {} {} : {}", period.getPeriodInterval().startDate(), period.getPeriodInterval().endDate(), + period.getInterestEarned()); + if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate())) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, false); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } else if (accrualTransactionDatesReverse.contains(period.getPeriodInterval().endDate())) { + if (foundDate.size() == foundDateReverse.size()) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, false); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } + } + } else { + + isNegativeBalance = true; + period.setAccrual(true); + period.setNegative(MathUtil.isLessThanZero(savingsAccount.getSummary().getAccountBalance())); + period.calculateInterest(compoundInterestValues); + log.debug(" period {} {} : {}", period.getPeriodInterval().startDate(), period.getPeriodInterval().endDate(), + period.getInterestEarned()); + if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate()) + && !MathUtil.isZero(period.getInterestEarned().getAmount())) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, true); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } else if (accrualTransactionDatesReverse.contains(period.getPeriodInterval().endDate())) { + if (foundDate.size() == foundDateReverse.size()) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false, true); + savingsAccountTransaction.setRunningBalance(period.getClosingBalance()); + savingsAccount.addTransaction(savingsAccountTransaction); + } + } + + } + } + + savingsAccount.setAccruedTillDate(accruedTillDate); + savingsAccountRepository.saveAndFlush(savingsAccount); + + savingsAccountDomainService.postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false, + isNegativeBalance); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index c7fb7d249e6..d45f3c4e6b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -678,7 +678,7 @@ public CommandProcessingResult createActiveApplication(final SavingsAccountDataD generateAccountNumber(account); // post journal entries for activation charges - this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + this.savingsAccountDomainService.postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, false); return new CommandProcessingResultBuilder() // .withSavingsId(account.getId()) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java index 07ab31de7b3..d43c5264237 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java @@ -20,6 +20,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; @@ -27,7 +28,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -42,7 +42,6 @@ import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType; import org.apache.fineract.infrastructure.entityaccess.service.FineractEntityAccessUtil; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsProductDataValidator; @@ -140,13 +139,11 @@ public CommandProcessingResult update(final Long productId, final JsonCommand co final Map changes = product.update(command); - if (changes.containsKey(chargesParamName)) { - final Set savingsProductCharges = this.savingsProductAssembler.assembleListOfSavingsProductCharges(command, - product.currency().getCode()); - final boolean updated = product.update(savingsProductCharges); - if (!updated) { - changes.remove(chargesParamName); - } + if (changes.containsKey(chargesParamName) || changes.containsKey(accrualChargesParamName)) { + product.setCharges(savingsProductAssembler.assembleListOfSavingsProductCharges(command, product.currency().getCode(), + chargesParamName)); + product.setAccrualCharges(savingsProductAssembler.assembleListOfSavingsProductCharges(command, product.currency().getCode(), + accrualChargesParamName)); } if (changes.containsKey(taxGroupIdParamName)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index e1bc759a7de..3d553a364ae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -147,6 +147,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @Configuration public class SavingsConfiguration { @@ -154,8 +155,8 @@ public class SavingsConfiguration { @Bean @ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class) public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context, - GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, DatatableReadService datatableService, - DataTableValidator dataTableValidator, JdbcTemplate jdbcTemplate, SearchUtil searchUtil) { + GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, DatatableReadService datatableService, + DataTableValidator dataTableValidator, JdbcTemplate jdbcTemplate, SearchUtil searchUtil) { return new SavingsAccountTransactionsSearchServiceImpl(context, genericDataService, sqlGenerator, datatableService, dataTableValidator, jdbcTemplate, searchUtil); } @@ -181,7 +182,7 @@ public DepositAccountInterestRateChartReadPlatformService depositAccountInterest @Bean @ConditionalOnMissingBean(DepositAccountOnHoldTransactionReadPlatformService.class) public DepositAccountOnHoldTransactionReadPlatformService depositAccountOnHoldTransactionReadPlatformService(JdbcTemplate jdbcTemplate, - ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { + ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { return new DepositAccountOnHoldTransactionReadPlatformServiceImpl(jdbcTemplate, sqlGenerator, columnValidator, paginationHelper); } @@ -199,15 +200,15 @@ public DepositAccountPreMatureCalculationPlatformService depositAccountPreMature @Bean @ConditionalOnMissingBean(DepositAccountReadPlatformService.class) public DepositAccountReadPlatformService depositAccountReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - DepositAccountInterestRateChartReadPlatformService chartReadPlatformService, - InterestRateChartReadPlatformService productChartReadPlatformService, - PaginationParametersDataValidator paginationParametersDataValidator, DatabaseSpecificSQLGenerator sqlGenerator, - PaginationHelper paginationHelper, ClientReadPlatformService clientReadPlatformService, - GroupReadPlatformService groupReadPlatformService, DepositProductReadPlatformService depositProductReadPlatformService, - SavingsDropdownReadPlatformService savingsDropdownReadPlatformService, ChargeReadPlatformService chargeReadPlatformService, - StaffReadPlatformService staffReadPlatformService, DepositsDropdownReadPlatformService depositsDropdownReadPlatformService, - SavingsAccountReadPlatformService savingsAccountReadPlatformService, DropdownReadPlatformService dropdownReadPlatformService, - CalendarReadPlatformService calendarReadPlatformService, PaymentTypeReadPlatformService paymentTypeReadPlatformService) { + DepositAccountInterestRateChartReadPlatformService chartReadPlatformService, + InterestRateChartReadPlatformService productChartReadPlatformService, + PaginationParametersDataValidator paginationParametersDataValidator, DatabaseSpecificSQLGenerator sqlGenerator, + PaginationHelper paginationHelper, ClientReadPlatformService clientReadPlatformService, + GroupReadPlatformService groupReadPlatformService, DepositProductReadPlatformService depositProductReadPlatformService, + SavingsDropdownReadPlatformService savingsDropdownReadPlatformService, ChargeReadPlatformService chargeReadPlatformService, + StaffReadPlatformService staffReadPlatformService, DepositsDropdownReadPlatformService depositsDropdownReadPlatformService, + SavingsAccountReadPlatformService savingsAccountReadPlatformService, DropdownReadPlatformService dropdownReadPlatformService, + CalendarReadPlatformService calendarReadPlatformService, PaymentTypeReadPlatformService paymentTypeReadPlatformService) { return new DepositAccountReadPlatformServiceImpl(context, jdbcTemplate, chartReadPlatformService, productChartReadPlatformService, paginationParametersDataValidator, sqlGenerator, paginationHelper, clientReadPlatformService, groupReadPlatformService, depositProductReadPlatformService, savingsDropdownReadPlatformService, chargeReadPlatformService, staffReadPlatformService, @@ -218,21 +219,21 @@ public DepositAccountReadPlatformService depositAccountReadPlatformService(Platf @Bean @ConditionalOnMissingBean(DepositAccountWritePlatformService.class) public DepositAccountWritePlatformService depositAccountWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper, - SavingsAccountTransactionRepository savingsAccountTransactionRepository, DepositAccountAssembler depositAccountAssembler, - DepositAccountTransactionDataValidator depositAccountTransactionDataValidator, - SavingsAccountChargeDataValidator savingsAccountChargeDataValidator, - PaymentDetailWritePlatformService paymentDetailWritePlatformService, - ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, - JournalEntryWritePlatformService journalEntryWritePlatformService, DepositAccountDomainService depositAccountDomainService, - NoteRepository noteRepository, AccountTransfersReadPlatformService accountTransfersReadPlatformService, - ChargeRepositoryWrapper chargeRepository, SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository, - AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, - AccountTransfersWritePlatformService accountTransfersWritePlatformService, - DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, - ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, - WorkingDaysRepositoryWrapper workingDaysRepository, - DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository + SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper, + SavingsAccountTransactionRepository savingsAccountTransactionRepository, DepositAccountAssembler depositAccountAssembler, + DepositAccountTransactionDataValidator depositAccountTransactionDataValidator, + SavingsAccountChargeDataValidator savingsAccountChargeDataValidator, + PaymentDetailWritePlatformService paymentDetailWritePlatformService, + ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, + JournalEntryWritePlatformService journalEntryWritePlatformService, DepositAccountDomainService depositAccountDomainService, + NoteRepository noteRepository, AccountTransfersReadPlatformService accountTransfersReadPlatformService, + ChargeRepositoryWrapper chargeRepository, SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository, + AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, + AccountTransfersWritePlatformService accountTransfersWritePlatformService, + DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, + ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, + WorkingDaysRepositoryWrapper workingDaysRepository, + DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository ) { return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper, @@ -247,16 +248,16 @@ public DepositAccountWritePlatformService depositAccountWritePlatformService(Pla @Bean @ConditionalOnMissingBean(DepositApplicationProcessWritePlatformService.class) public DepositApplicationProcessWritePlatformService depositApplicationProcessWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepository, FixedDepositAccountRepository fixedDepositAccountRepository, - RecurringDepositAccountRepository recurringDepositAccountRepository, DepositAccountAssembler depositAccountAssembler, - DepositAccountDataValidator depositAccountDataValidator, AccountNumberGenerator accountNumberGenerator, - ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, - NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, - SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, - SavingsAccountChargeAssembler savingsAccountChargeAssembler, AccountAssociationsRepository accountAssociationsRepository, - FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository, - ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - BusinessEventNotifierService businessEventNotifierService) { + SavingsAccountRepositoryWrapper savingAccountRepository, FixedDepositAccountRepository fixedDepositAccountRepository, + RecurringDepositAccountRepository recurringDepositAccountRepository, DepositAccountAssembler depositAccountAssembler, + DepositAccountDataValidator depositAccountDataValidator, AccountNumberGenerator accountNumberGenerator, + ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, + NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, + SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, + SavingsAccountChargeAssembler savingsAccountChargeAssembler, AccountAssociationsRepository accountAssociationsRepository, + FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository, + ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, + BusinessEventNotifierService businessEventNotifierService) { return new DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, fixedDepositAccountRepository, recurringDepositAccountRepository, depositAccountAssembler, depositAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository, @@ -268,7 +269,7 @@ public DepositApplicationProcessWritePlatformService depositApplicationProcessWr @Bean @ConditionalOnMissingBean(DepositProductReadPlatformService.class) public DepositProductReadPlatformService depositProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - InterestRateChartReadPlatformService interestRateChartReadPlatformService) { + InterestRateChartReadPlatformService interestRateChartReadPlatformService) { return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService); } @@ -281,9 +282,9 @@ public DepositsDropdownReadPlatformService depositsDropdownReadPlatformService() @Bean @ConditionalOnMissingBean(FixedDepositProductWritePlatformService.class) public FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService(PlatformSecurityContext context, - FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, - DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, + DepositProductAssembler depositProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository, fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); } @@ -298,16 +299,16 @@ public GroupSavingsIndividualMonitoringWritePlatformService groupSavingsIndividu @Bean @ConditionalOnMissingBean(GSIMReadPlatformService.class) public GSIMReadPlatformService gsimReadPlatformService(JdbcTemplate jdbcTemplate, PlatformSecurityContext context, - ColumnValidator columnValidator) { + ColumnValidator columnValidator) { return new GSIMReadPlatformServiceImpl(jdbcTemplate, context, columnValidator); } @Bean @ConditionalOnMissingBean(RecurringDepositProductWritePlatformService.class) public RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService(PlatformSecurityContext context, - RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, - DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, + DepositProductAssembler depositProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository, fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); } @@ -322,8 +323,8 @@ public SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplica @Bean @ConditionalOnMissingBean(SavingsAccountChargeReadPlatformService.class) public SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService(PlatformSecurityContext context, - ChargeDropdownReadPlatformService chargeDropdownReadPlatformService, JdbcTemplate jdbcTemplate, - DropdownReadPlatformService dropdownReadPlatformService, DatabaseSpecificSQLGenerator sqlGenerator) { + ChargeDropdownReadPlatformService chargeDropdownReadPlatformService, JdbcTemplate jdbcTemplate, + DropdownReadPlatformService dropdownReadPlatformService, DatabaseSpecificSQLGenerator sqlGenerator) { return new SavingsAccountChargeReadPlatformServiceImpl(context, chargeDropdownReadPlatformService, jdbcTemplate, dropdownReadPlatformService, sqlGenerator); } @@ -337,20 +338,20 @@ public SavingsAccountInterestPostingService savingsAccountInterestPostingService @Bean @ConditionalOnMissingBean(SavingsAccountReadPlatformService.class) public SavingsAccountReadPlatformService savingsAccountReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, - SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator) { + SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, + SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { return new SavingsAccountReadPlatformServiceImpl(context, jdbcTemplate, savingAccountAssembler, paginationHelper, columnValidator, - sqlGenerator, savingsAccountRepositoryWrapper); + sqlGenerator, savingsAccountRepositoryWrapper, namedParameterJdbcTemplate); } @Bean @ConditionalOnMissingBean(SavingsAccountTemplateReadPlatformService.class) public SavingsAccountTemplateReadPlatformService savingsAccountTemplateReadPlatformService(PlatformSecurityContext context, - JdbcTemplate jdbcTemplate, ClientReadPlatformService clientReadPlatformService, - GroupReadPlatformService groupReadPlatformService, SavingsProductReadPlatformService savingProductReadPlatformService, - StaffReadPlatformService staffReadPlatformService, SavingsDropdownReadPlatformService dropdownReadPlatformService, - ChargeReadPlatformService chargeReadPlatformService, EntityDatatableChecksReadService entityDatatableChecksReadService, - ColumnValidator columnValidator) { + JdbcTemplate jdbcTemplate, ClientReadPlatformService clientReadPlatformService, + GroupReadPlatformService groupReadPlatformService, SavingsProductReadPlatformService savingProductReadPlatformService, + StaffReadPlatformService staffReadPlatformService, SavingsDropdownReadPlatformService dropdownReadPlatformService, + ChargeReadPlatformService chargeReadPlatformService, EntityDatatableChecksReadService entityDatatableChecksReadService, + ColumnValidator columnValidator) { return new SavingsAccountTemplateReadPlatformServiceImpl(context, jdbcTemplate, clientReadPlatformService, groupReadPlatformService, savingProductReadPlatformService, staffReadPlatformService, dropdownReadPlatformService, chargeReadPlatformService, entityDatatableChecksReadService, columnValidator); @@ -387,16 +388,16 @@ public SavingsAccountWritePlatformService savingsAccountWritePlatformService(Pla @Bean @ConditionalOnMissingBean(SavingsApplicationProcessWritePlatformService.class) public SavingsApplicationProcessWritePlatformService savingsApplicationProcessWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepository, SavingsAccountAssembler savingAccountAssembler, - SavingsAccountDataValidator savingsAccountDataValidator, AccountNumberGenerator accountNumberGenerator, - ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, - NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, - SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, - SavingsAccountChargeAssembler savingsAccountChargeAssembler, CommandProcessingService commandProcessingService, - SavingsAccountDomainService savingsAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, - AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService, - EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository, - GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService) { + SavingsAccountRepositoryWrapper savingAccountRepository, SavingsAccountAssembler savingAccountAssembler, + SavingsAccountDataValidator savingsAccountDataValidator, AccountNumberGenerator accountNumberGenerator, + ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, + NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, + SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, + SavingsAccountChargeAssembler savingsAccountChargeAssembler, CommandProcessingService commandProcessingService, + SavingsAccountDomainService savingsAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, + AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService, + EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository, + GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService) { return new SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, savingAccountAssembler, savingsAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository, savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler, @@ -414,17 +415,17 @@ public SavingsDropdownReadPlatformService savingsDropdownReadPlatformService() { @Bean @ConditionalOnMissingBean(SavingsProductReadPlatformService.class) public SavingsProductReadPlatformService savingsProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - FineractEntityAccessUtil fineractEntityAccessUtil) { + FineractEntityAccessUtil fineractEntityAccessUtil) { return new SavingsProductReadPlatformServiceImpl(context, jdbcTemplate, fineractEntityAccessUtil); } @Bean @ConditionalOnMissingBean(SavingsProductWritePlatformService.class) public SavingsProductWritePlatformService savingsProductWritePlatformService(PlatformSecurityContext context, - SavingsProductRepository savingProductRepository, SavingsProductDataValidator fromApiJsonDataValidator, - SavingsProductAssembler savingsProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, - FineractEntityAccessUtil fineractEntityAccessUtil) { + SavingsProductRepository savingProductRepository, SavingsProductDataValidator fromApiJsonDataValidator, + SavingsProductAssembler savingsProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, + FineractEntityAccessUtil fineractEntityAccessUtil) { return new SavingsProductWritePlatformServiceJpaRepositoryImpl(context, savingProductRepository, fromApiJsonDataValidator, savingsProductAssembler, accountMappingWritePlatformService, fineractEntityAccessUtil); } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 33642742e4d..99cadca29fc 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -468,4 +468,4 @@ resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POS fineract.command.enabled=true fineract.command.executor=sync fineract.command.ring-buffer-size=1024 -fineract.command.producer-type=single +fineract.command.producer-type=single \ No newline at end of file diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 42b41ea10f8..e05afb8900d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -204,4 +204,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0186_add_buy_down_fee_to_loan_product.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0186_add_buy_down_fee_to_loan_product.xml new file mode 100644 index 00000000000..e845a9975a7 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0186_add_buy_down_fee_to_loan_product.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java index 4f45f832667..f6425fe0f25 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java @@ -54,6 +54,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException; +import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.useradministration.domain.AppUser; @@ -153,7 +154,7 @@ public void teardown() { } @Test - public void testExecuteCommandSuccessAfter2Fails() { + public void testExecuteCommandSuccessAfter2Fails() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); long commandId = 1L; @@ -198,7 +199,7 @@ public void testExecuteCommandSuccessAfter2Fails() { * stays in the same status therefor it should fail after reaching max retry count. */ @Test - public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnderProcessingException() { + public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnderProcessingException() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -254,7 +255,7 @@ public void executeCommandShouldFailAfterRetriesWithIdempotentCommandProcessUnde * processable. */ @Test - public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUnderProcessingException() { + public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUnderProcessingException() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -312,7 +313,7 @@ public void executeCommandShouldPassAfter1retryFailsByIdempotentCommandProcessUn * fail, status should be still the same. On 3rd try it should result no error. */ @Test - public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckStatus() { + public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckStatus() throws MultiException { CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class); when(commandWrapper.isDatatableResource()).thenReturn(false); when(commandWrapper.isNoteResource()).thenReturn(false); @@ -375,7 +376,7 @@ public void executeCommandShouldPassAfter2RetriesOnRetryExceptionAndWithStuckSta } @Test - public void testExecuteCommandSuccess() { + public void testExecuteCommandSuccess() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); long commandId = 1L; @@ -415,7 +416,7 @@ public void testExecuteCommandSuccess() { } @Test - public void testExecuteCommandFails() { + public void testExecuteCommandFails() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); Long commandId = jsonCommand.commandId(); @@ -482,7 +483,7 @@ public void publishHookEventHandlesInvalidJson() { private static final class RetryException extends RuntimeException {} @Test - public void testExecuteCommandWithRetry() { + public void testExecuteCommandWithRetry() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); when(commandWrapper.isInterestPauseResource()).thenReturn(false); @@ -540,7 +541,7 @@ public void testExecuteCommandWithRetry() { } @Test - public void testExecuteCommandWithMaxRetryFailure() { + public void testExecuteCommandWithMaxRetryFailure() throws MultiException { CommandWrapper commandWrapper = getCommandWrapper(); when(commandWrapper.isInterestPauseResource()).thenReturn(false); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 7b6e646212f..9d41557b70f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -173,8 +173,8 @@ delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), when(loanAccountData.getAccountNo()).thenReturn("0001"); when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10)); - when(loanAccountData.getCurrency()).thenAnswer(a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), - loanCurrency.getCurrencyInMultiplesOf())); + when(loanAccountData.getCurrency()).thenAnswer( + a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf())); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(false); when(delinquentData.getDelinquentDate()).thenReturn(delinquentDate); @@ -238,8 +238,8 @@ delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), when(loanAccountData.getAccountNo()).thenReturn("0001"); when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10)); - when(loanAccountData.getCurrency()).thenAnswer(a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), - loanCurrency.getCurrencyInMultiplesOf())); + when(loanAccountData.getCurrency()).thenAnswer( + a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf())); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); when(delinquentData.getDelinquentDate()).thenReturn(delinquentDate); @@ -363,7 +363,7 @@ public void testLastRepaymentInCollectionData() { DelinquencyReadPlatformService delinquencyReadPlatformService = new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, - delinquencyEffectivePauseHelper, configurationDomainService); + delinquencyEffectivePauseHelper, configurationDomainService, Mockito.mock(LoanTransactionRepository.class)); LoanProduct loanProduct = Mockito.mock(LoanProduct.class); when(loanProduct.isMultiDisburseLoan()).thenReturn(false); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java index fa780795747..fc5a7209425 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java @@ -40,7 +40,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -80,7 +79,6 @@ public class LoanBuilder { private BigDecimal fixedPrincipalPercentagePerInstallment; private ExternalId externalId = ExternalId.empty(); private LoanApplicationTerms loanApplicationTerms = mock(LoanApplicationTerms.class); - private LoanScheduleModel loanScheduleModel = mock(LoanScheduleModel.class); private Boolean enableInstallmentLevelDelinquency = false; private LocalDate submittedOnDate = LocalDate.now(ZoneId.systemDefault()); private LocalDate approvedOnDate; @@ -116,8 +114,8 @@ public Loan build() { Loan loan = Loan.newIndividualLoanApplication(accountNo, client, loanType, loanProduct, fund, loanOfficer, loanPurpose, transactionProcessor, loanRepaymentScheduleDetail, charges, collateral, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, - rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, - enableInstallmentLevelDelinquency, submittedOnDate); + rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency, + submittedOnDate); if (id != null) { loan.setId(id); @@ -335,11 +333,6 @@ public LoanBuilder withLoanApplicationTerms(LoanApplicationTerms loanApplication return this; } - public LoanBuilder withLoanScheduleModel(LoanScheduleModel loanScheduleModel) { - this.loanScheduleModel = loanScheduleModel; - return this; - } - public LoanBuilder withEnableInstallmentLevelDelinquency(Boolean enableInstallmentLevelDelinquency) { this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; return this; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java new file mode 100644 index 00000000000..9da50c0b5c1 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java @@ -0,0 +1,52 @@ +/** + * 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.savings.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.tax.data.TaxGroupData; + +@Data +@RequiredArgsConstructor +public class SavingsAccrualData { + + private final Long id; + private final String accountNo; + private final EnumOptionData depositType; + private final SavingsAccountStatusEnumData status; + private final Long savingsProductId; + private final Long officeId; + private final LocalDate accruedTill; + private final LocalDate postedTill; + private final CurrencyData currencyData; + private final BigDecimal nominalAnnualInterestRate; + private final EnumOptionData interestCompoundingPeriodType; + private final EnumOptionData interestPostingPeriodType; + private final EnumOptionData interestCalculationType; + private final EnumOptionData interestCalculationDaysInYearType; + + private final BigDecimal accruedInterestIncome; + private LocalDate interestCalculatedFrom; + private TaxGroupData taxGroup; + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java index 87685317d5e..442540e72e3 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java @@ -89,7 +89,7 @@ public class SavingsProductDataValidator { private final SavingsProductAccountingDataValidator savingsProductAccountingDataValidator; private static final Set SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( SavingsApiConstants.localeParamName, SavingsApiConstants.monthDayFormatParamName, nameParamName, shortNameParamName, - descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, + "interestReceivableAccountId", descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, SavingsApiConstants.withdrawalFeeAmountParamName, @@ -98,8 +98,9 @@ public class SavingsProductDataValidator { SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), - SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), - SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), + SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), + SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), @@ -109,7 +110,8 @@ public class SavingsProductDataValidator { nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, SavingsApiConstants.minRequiredBalanceParamName, SavingsApiConstants.enforceMinRequiredBalanceParamName, SavingsApiConstants.maxAllowedLienLimitParamName, SavingsApiConstants.lienAllowedParamName, - minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName)); + minBalanceForInterestCalculationParamName, withHoldTaxParamName, taxGroupIdParamName, + SavingsApiConstants.accrualChargesParamName)); public void validateForCreate(final String json) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java index 4e2e1389f02..1cb23e7c27b 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountInterestRateChart.java @@ -156,4 +156,12 @@ public BigDecimal getApplicableInterestRate(final BigDecimal depositAmount, fina public boolean isPrimaryGroupingByAmount() { return this.chartFields.isPrimaryGroupingByAmount(); } + + public SavingsAccount getAccount() { + return account; + } + + public void setAccount(SavingsAccount account) { + this.account = account; + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java index 360e64ada17..51e498bf3eb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositTermDetail.java @@ -166,6 +166,10 @@ public Integer inMultiplesOfDepositTermType() { return this.inMultiplesOfDepositTermType; } + public Integer getDepositPeriodInDays(final Integer depositPeriod, final SavingsPeriodFrequencyType depositPeriodFrequencyType) { + return this.convertToSafeDays(depositPeriod, depositPeriodFrequencyType); + } + public boolean isDepositBetweenMinAndMax(LocalDate depositStartDate, LocalDate depositEndDate) { return isEqualOrGreaterThanMin(depositStartDate, depositEndDate) && isEqualOrLessThanMax(depositStartDate, depositEndDate); } @@ -263,4 +267,5 @@ public DepositTermDetail copy() { return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType); } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java index 216770856c3..07d956a3c6c 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java @@ -113,7 +113,7 @@ protected FixedDepositProduct(final String name, final String shortName, final S super(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, - minBalanceForInterestCalculation, withHoldTax, taxGroup); + minBalanceForInterestCalculation, withHoldTax, taxGroup, null); if (charts != null) { this.charts = charts; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 5db6e32dc14..a987560adba 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -71,6 +71,8 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; @@ -117,17 +119,18 @@ import org.apache.fineract.portfolio.savings.exception.SavingsActivityPriorToClientTransferException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentDateException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsAlreadyUndoneException; import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsCannotBeUndoneException; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.domain.TaxComponent; import org.apache.fineract.portfolio.tax.domain.TaxGroup; import org.apache.fineract.portfolio.tax.service.TaxUtils; import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; @Entity +@Getter +@Setter @Table(name = "m_savings_account", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "sa_account_no_UNIQUE"), @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @@ -136,8 +139,6 @@ @SuppressWarnings({ "MemberName" }) public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom { - private static final Logger LOG = LoggerFactory.getLogger(SavingsAccount.class); - @Version int version; @@ -337,12 +338,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @JoinColumn(name = "tax_group_id") private TaxGroup taxGroup; + @Column(name = "accrued_till_date") + protected LocalDate accruedTillDate; + @Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true) private BigDecimal savingsOnHoldAmount; @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) protected List identifiers = new ArrayList<>(); public transient ConfigurationDomainService configurationDomainService; + public transient SavingsAccountTransaction newTransaction; protected SavingsAccount() { // @@ -501,10 +506,6 @@ public boolean isClosed() { return SavingsAccountStatusType.fromInt(this.status).isClosed(); } - public List getIdentifiers() { - return identifiers; - } - public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { @@ -642,6 +643,10 @@ public void postInterest(final MathContext mc, final LocalDate interestPostingUp } } + public List getIdentifiers() { + return identifiers; + } + protected List findWithHoldTransactions() { final List withholdTransactions = new ArrayList<>(); List trans = getTransactions(); @@ -664,7 +669,7 @@ protected List findWithHoldSavingsTransactionsWithPiv return withholdTransactions; } - private boolean isWithHoldTaxApplicableForInterestPosting() { + public boolean isWithHoldTaxApplicableForInterestPosting() { return this.withHoldTax() && this.depositAccountType().isSavingsDeposit(); } @@ -846,6 +851,7 @@ public List calculateInterestUsing(final MathContext mc, final Lo if (postInterestOnDate != null) { postedAsOnDates.add(postInterestOnDate); } + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, postedAsOnDates); @@ -926,8 +932,7 @@ private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc); } - @SuppressWarnings("unused") - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { + public BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); } @@ -939,6 +944,20 @@ private boolean hasOverdraftInterestCalculation() { return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft); } + public List retreiveOrderedAccrualTransactions() { + final List listOfTransactionsSorted = retrieveListOfTransactions(); + + final List orderedAccrualTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (transaction.isAccrual()) { + orderedAccrualTransactions.add(transaction); + } + } + orderedAccrualTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedAccrualTransactions; + } + protected List retreiveOrderedNonInterestPostingTransactions() { final List listOfTransactionsSorted = retrieveListOfTransactions(); @@ -946,7 +965,7 @@ protected List retreiveOrderedNonInterestPostingTrans for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isNotReversed() && !transaction.isReversalTransaction()) { + && transaction.isNotReversed() && !transaction.isReversalTransaction() && !transaction.isAccrual()) { orderedNonInterestPostingTransactions.add(transaction); } } @@ -989,6 +1008,7 @@ protected List retrieveListOfTransactions() { protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, final boolean backdatedTxnsAllowedTill, boolean postReversals) { Money runningBalance = openingAccountBalance; + BigDecimal previewBalance = BigDecimal.ZERO; boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation(); List accountTransactionsSorted = null; @@ -1001,7 +1021,9 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final boolean isTransactionsModified = false; for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { + boolean typeTransaccionValidation = transaction.getTransactionType() == SavingsAccountTransactionType.ACCRUAL; if (transaction.isReversed() || transaction.isReversalTransaction()) { + transaction.setNegativeBalance(MathUtil.isLessThanZero(transaction.getRunningBalance())); transaction.zeroBalanceFields(); } else { Money overdraftAmount = Money.zero(this.currency); @@ -1022,6 +1044,12 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final } transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); } + if (typeTransaccionValidation && this.newTransaction != null + && (transaction.getDateOf().isAfter(this.newTransaction.getDateOf()) + || transaction.getDateOf().isEqual(this.newTransaction.getDateOf()))) { + transaction.setNegativeBalance(MathUtil.isLessThanZero(transaction.getRunningBalance())); + transaction.reverse(); + } runningBalance = runningBalance.plus(transactionAmount); transaction.setRunningBalance(runningBalance); @@ -1029,7 +1057,7 @@ protected void recalculateDailyBalances(final Money openingAccountBalance, final if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { overdraftAmount = runningBalance.negated(); } - if (!calculateInterest || transaction.getId() == null) { + if (!calculateInterest || transaction.getId() == null || transaction.getOverdraftAmount(this.currency).isZero()) { transaction.setOverdraftAmount(overdraftAmount); } else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency))) { SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction); @@ -1110,15 +1138,17 @@ public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO tran final Long relaxingDaysConfigForPivotDate, final String refNo) { final String resourceTypeName = depositAccountType().resourceName(); if (isNotActive()) { - final String defaultUserMessage = "Transaction is not allowed. Account is not active."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", - transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + if (!SavingsAccountStatusType.fromInt(this.status).isMatured()) { + final String defaultUserMessage = "Transaction is not allowed. Account is not active."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", + transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); - throw new PlatformApiDataValidationException(dataValidationErrors); + throw new PlatformApiDataValidationException(dataValidationErrors); + } } if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { @@ -1158,6 +1188,7 @@ public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO tran if (backdatedTxnsAllowedTill) { addTransactionToExisting(transaction); } else { + addTransactionNew(transaction); addTransaction(transaction); } @@ -1293,6 +1324,7 @@ public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO tra if (backdatedTxnsAllowedTill) { addTransactionToExisting(transaction); } else { + addTransactionNew(transaction); addTransaction(transaction); } @@ -1976,11 +2008,15 @@ public SavingsProduct savingsProduct() { return this.product; } - private Boolean isCashBasedAccountingEnabledOnSavingsProduct() { + public Boolean isCashBasedAccountingEnabledOnSavingsProduct() { return this.product.isCashBasedAccountingEnabled(); } - private Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { + public Boolean isPeriodicAccrualAccounting() { + return this.product.isPeriodicAccrualAccounting(); + } + + public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { return this.product.isAccrualBasedAccountingEnabled(); } @@ -2375,6 +2411,50 @@ public void undoTransaction(final Long transactionId) { } } + protected Map undoActivate() { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.undoActivateAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = null; + this.activatedBy = null; + this.lockedInUntilDate = null; + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNOD_ACTIVATE, businessDate); + + // Undo Transactions + for (SavingsAccountTransaction transaction : getTransactions()) { + if (!transaction.isReversed()) { + undoTransaction(transaction); + } + } + + return actualChanges; + } + public void undoSavingsTransaction(final Long transactionId) { SavingsAccountTransaction transactionToUndo = null; @@ -2385,7 +2465,7 @@ public void undoSavingsTransaction(final Long transactionId) { } if (transactionToUndo == null) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); } validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); @@ -2408,7 +2488,7 @@ public void undoSavingsTransaction(final Long transactionId) { public void undoTransaction(final SavingsAccountTransaction transactionToUndo) { if (transactionToUndo.isReversed()) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); + throw new SavingsTransferTransactionsAlreadyUndoneException(getAccountNumber(), transactionToUndo.getId()); } validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); @@ -2680,6 +2760,98 @@ public Map activate(final AppUser currentUser, final JsonCommand return actualChanges; } + protected Map undoActivate(final AppUser currentUser, final JsonCommand command, final LocalDate operationDate) { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = null; + this.activatedBy = null; + this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); + + if (this.client != null && this.client.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (this.group != null && this.group.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate approvalDate = getApprovedOnDate(); + if (operationDate.isBefore(approvalDate)) { + + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(approvalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_ACTIVATE, operationDate); + + updateSavingsToApprovedState(); + + return actualChanges; + } + + protected void updateSavingsToApprovedState() { + reverseExistingTransactions(); + + for (SavingsAccountCharge charge : this.charges()) { + charge.resetToOriginal(currency); + } + } + + protected void reverseExistingTransactions() { + Collection retainTransactions = new ArrayList<>(); + for (final SavingsAccountTransaction transaction : this.transactions) { + transaction.reverse(); + if (transaction.getId() != null) { + retainTransactions.add(transaction); + } + } + this.transactions.retainAll(retainTransactions); + } + public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { // update annual fee due date @@ -2842,7 +3014,6 @@ private LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLo lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency); break; case WHOLE_TERM: - LOG.error("TODO Implement calculateDateAccountIsLockedUntil for WHOLE_TERM"); break; } @@ -2883,6 +3054,10 @@ public void addTransaction(final SavingsAccountTransaction transaction) { this.transactions.add(transaction); } + public void addTransactionNew(final SavingsAccountTransaction transaction) { + this.newTransaction = transaction; + } + public void addTransactionToExisting(final SavingsAccountTransaction transaction) { this.savingsAccountTransactions.add(transaction); } @@ -3205,6 +3380,38 @@ private void handleChargeTransactions(final SavingsAccountCharge savingsAccountC } else { this.transactions.add(transaction); } + + // Charge Accrual Recognition + final SavingsAccountTransaction savingsAccountAccrualTransaction = handleAccruedChargeAppliedTransaction( + transaction.getTransactionDate(), savingsAccountCharge); + if (savingsAccountAccrualTransaction != null) { + savingsAccountAccrualTransaction.getSavingsAccountChargesPaid().add(chargePaidBy); + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactions.add(savingsAccountAccrualTransaction); + } else { + this.transactions.add(savingsAccountAccrualTransaction); + } + } + } + + private SavingsAccountTransaction handleAccruedChargeAppliedTransaction(final LocalDate transactionDate, + final SavingsAccountCharge savingsAccountCharge) { + SavingsAccountTransaction savingsAccountAccrualTransaction = null; + if (isPeriodicAccrualAccounting()) { + if (isChargeToBeRecognizedAsAccrual(savingsAccountCharge)) { + savingsAccountAccrualTransaction = SavingsAccountTransaction.accrual(this, office(), transactionDate, + savingsAccountCharge.getAmount(getCurrency()), false, false); + } + } + return savingsAccountAccrualTransaction; + } + + private boolean isChargeToBeRecognizedAsAccrual(final SavingsAccountCharge savingsAccountCharge) { + final Collection chargeIds = savingsProduct().accrualChargeIds(); + if (chargeIds.isEmpty()) { + return false; + } + return chargeIds.contains(savingsAccountCharge.getCharge().getId()); } private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { @@ -3446,7 +3653,7 @@ protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPos if (withholdTransaction == null && this.withHoldTax()) { boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate, backdatedTxnsAllowedTill); recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; - } else { + } else if (withholdTransaction != null) { boolean isWithholdTaxAdded = updateWithHoldTransaction(totalInterestPosted, withholdTransaction); recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; } @@ -3847,4 +4054,11 @@ public List toSavingsAccountTr .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) .toList(); } + + public List toSavingsAccountTransactionDetailsForPostingPeriodList() { + return retreiveOrderedNonInterestPostingTransactions().stream() + .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) + .toList(); + } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java index aaece884cdb..aa96dd51814 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java @@ -35,6 +35,10 @@ public interface SavingsAccountRepository extends JpaRepository findSavingAccountByClientId(@Param("clientId") Long clientId); + @Query("select s_acc from SavingsAccount s_acc where s_acc.client.id = :clientId and s_acc.depositType = :depositAccountTypeId") + SavingsAccount findSavingAccountByClientIdAndDepositAccountType(@Param("clientId") Long clientId, + @Param("depositAccountTypeId") Integer depositAccountTypeId); + @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select sa from SavingsAccount sa where sa.id = :savingsId") SavingsAccount findOneLocked(@Param("savingsId") Long id); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index 3a406335698..461beb135e2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -90,6 +90,17 @@ public SavingsAccount findOneWithNotFoundDetection(final Long savingsId, final D return account; } + @Transactional(readOnly = true) + public SavingsAccount findSavingId(final Long clientId, final DepositAccountType depositAccountType) { + final SavingsAccount account = this.repository.findSavingAccountByClientIdAndDepositAccountType(clientId, + depositAccountType.getValue()); + if (account == null) { + throw new SavingsAccountNotFoundException(clientId); + } + account.loadLazyCollections(); + return account; + } + @Transactional(readOnly = true) public List findSavingAccountByClientId(@Param("clientId") Long clientId) { List accounts = this.repository.findSavingAccountByClientId(clientId); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index 96aef542849..be7f8756902 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -28,6 +28,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -41,8 +42,10 @@ import java.util.Optional; import java.util.Set; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; @@ -138,11 +141,14 @@ public final class SavingsAccountTransaction extends AbstractAuditableWithUTCDat @Column(name = "ref_no", nullable = true) private String refNo; + @Transient + private Boolean isNegativeBalance; + SavingsAccountTransaction() {} private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final PaymentDetail paymentDetail, final Integer typeOf, final LocalDate transactionLocalDate, final BigDecimal amount, final boolean isReversed, - final boolean isManualTransaction, final Boolean lienTransaction, final String refNo) { + final boolean isManualTransaction, final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { this.savingsAccount = savingsAccount; this.office = office; this.typeOf = typeOf; @@ -155,19 +161,21 @@ private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Off this.isManualTransaction = isManualTransaction; this.lienTransaction = lienTransaction; this.refNo = refNo; + this.isNegativeBalance = isNegativeBalance; } private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final Integer typeOf, final LocalDate transactionLocalDate, final Money amount, final boolean isReversed, final boolean isManualTransaction, - final Boolean lienTransaction, final String refNo) { - this(savingsAccount, office, null, typeOf, transactionLocalDate, amount, isReversed, isManualTransaction, lienTransaction, refNo); + final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { + this(savingsAccount, office, null, typeOf, transactionLocalDate, amount, isReversed, isManualTransaction, lienTransaction, refNo, + isNegativeBalance); } private SavingsAccountTransaction(final SavingsAccount savingsAccount, final Office office, final PaymentDetail paymentDetail, final Integer typeOf, final LocalDate transactionLocalDate, final Money amount, final boolean isReversed, - final boolean isManualTransaction, final Boolean lienTransaction, final String refNo) { + final boolean isManualTransaction, final Boolean lienTransaction, final String refNo, final Boolean isNegativeBalance) { this(savingsAccount, office, paymentDetail, typeOf, transactionLocalDate, amount.getAmount(), isReversed, isManualTransaction, - lienTransaction, refNo); + lienTransaction, refNo, isNegativeBalance); } public static SavingsAccountTransaction deposit(final SavingsAccount savingsAccount, final Office office, @@ -176,7 +184,7 @@ public static SavingsAccountTransaction deposit(final SavingsAccount savingsAcco final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.DEPOSIT.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction deposit(final SavingsAccount savingsAccount, final Office office, @@ -186,7 +194,7 @@ public static SavingsAccountTransaction deposit(final SavingsAccount savingsAcco final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, savingsAccountTransactionType.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawal(final SavingsAccount savingsAccount, final Office office, @@ -195,7 +203,16 @@ public static SavingsAccountTransaction withdrawal(final SavingsAccount savingsA final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.WITHDRAWAL.getValue(), - date, amount, isReversed, isManualTransaction, lienTransaction, refNo); + date, amount, isReversed, isManualTransaction, lienTransaction, refNo, false); + } + + public static SavingsAccountTransaction accrual(final SavingsAccount savingsAccount, final Office office, final LocalDate date, + final Money amount, final boolean isManualTransaction, final Boolean isNegativeBalance) { + final boolean isReversed = false; + final Boolean lienTransaction = false; + final String refNo = ExternalId.generate().getValue(); + return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount, + isReversed, isManualTransaction, lienTransaction, refNo, isNegativeBalance); } public static SavingsAccountTransaction interestPosting(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -204,7 +221,7 @@ public static SavingsAccountTransaction interestPosting(final SavingsAccount sav final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.INTEREST_POSTING.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction overdraftInterest(final SavingsAccount savingsAccount, final Office office, @@ -213,7 +230,7 @@ public static SavingsAccountTransaction overdraftInterest(final SavingsAccount s final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.OVERDRAFT_INTEREST.getValue(), date, - amount, isReversed, isManualTransaction, lienTransaction, refNo); + amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawalFee(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -222,7 +239,7 @@ public static SavingsAccountTransaction withdrawalFee(final SavingsAccount savin final boolean isManualTransaction = false; final Boolean lienTransaction = false; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WITHDRAWAL_FEE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction annualFee(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -232,7 +249,7 @@ public static SavingsAccountTransaction annualFee(final SavingsAccount savingsAc final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ANNUAL_FEE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction charge(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -242,7 +259,7 @@ public static SavingsAccountTransaction charge(final SavingsAccount savingsAccou final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.PAY_CHARGE.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction waiver(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -252,7 +269,7 @@ public static SavingsAccountTransaction waiver(final SavingsAccount savingsAccou final Boolean lienTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WAIVE_CHARGES.getValue(), date, amount, - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction initiateTransfer(final SavingsAccount savingsAccount, final Office office, @@ -264,7 +281,7 @@ public static SavingsAccountTransaction initiateTransfer(final SavingsAccount sa final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.INITIATE_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction approveTransfer(final SavingsAccount savingsAccount, final Office office, @@ -276,7 +293,7 @@ public static SavingsAccountTransaction approveTransfer(final SavingsAccount sav final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.APPROVE_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withdrawTransfer(final SavingsAccount savingsAccount, final Office office, @@ -288,7 +305,7 @@ public static SavingsAccountTransaction withdrawTransfer(final SavingsAccount sa final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.WITHDRAW_TRANSFER.getValue(), date, savingsAccount.getSummary().getAccountBalance(), - isReversed, isManualTransaction, lienTransaction, refNo); + isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction withHoldTax(final SavingsAccount savingsAccount, final Office office, final LocalDate date, @@ -299,7 +316,7 @@ public static SavingsAccountTransaction withHoldTax(final SavingsAccount savings final String refNo = null; SavingsAccountTransaction accountTransaction = new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.WITHHOLD_TAX.getValue(), date, amount, isReversed, isManualTransaction, lienTransaction, - refNo); + refNo, false); updateTaxDetails(taxDetails, accountTransaction); return accountTransaction; } @@ -312,13 +329,13 @@ public static SavingsAccountTransaction escheat(final SavingsAccount savingsAcco final String refNo = null; return new SavingsAccountTransaction(savingsAccount, savingsAccount.office(), paymentDetail, SavingsAccountTransactionType.ESCHEAT.getValue(), date, savingsAccount.getSummary().getAccountBalance(), isReversed, - accountTransaction, lienTransaction, refNo); + accountTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction copyTransaction(SavingsAccountTransaction accountTransaction) { return new SavingsAccountTransaction(accountTransaction.savingsAccount, accountTransaction.office, accountTransaction.paymentDetail, accountTransaction.typeOf, accountTransaction.getTransactionDate(), accountTransaction.amount, accountTransaction.reversed, - accountTransaction.isManualTransaction, accountTransaction.lienTransaction, accountTransaction.refNo); + accountTransaction.isManualTransaction, accountTransaction.lienTransaction, accountTransaction.refNo, false); } public static SavingsAccountTransaction holdAmount(final SavingsAccount savingsAccount, final Office office, @@ -327,14 +344,14 @@ public static SavingsAccountTransaction holdAmount(final SavingsAccount savingsA final boolean isManualTransaction = false; final String refNo = null; return new SavingsAccountTransaction(savingsAccount, office, paymentDetail, SavingsAccountTransactionType.AMOUNT_HOLD.getValue(), - date, amount, isReversed, isManualTransaction, lienTransaction, refNo); + date, amount, isReversed, isManualTransaction, lienTransaction, refNo, false); } public static SavingsAccountTransaction releaseAmount(SavingsAccountTransaction accountTransaction, LocalDate transactionDate) { return new SavingsAccountTransaction(accountTransaction.savingsAccount, accountTransaction.office, accountTransaction.paymentDetail, SavingsAccountTransactionType.AMOUNT_RELEASE.getValue(), transactionDate, accountTransaction.amount, accountTransaction.reversed, accountTransaction.isManualTransaction, accountTransaction.lienTransaction, - accountTransaction.refNo); + accountTransaction.refNo, false); } public static SavingsAccountTransaction reversal(SavingsAccountTransaction accountTransaction) { @@ -559,6 +576,10 @@ public boolean isTransferRelatedTransaction() { return isTransferInitiation() || isTransferApproval() || isTransferRejection() || isTransferWithdrawal(); } + public boolean isAccrual() { + return getTransactionType().isAccrual(); + } + public void zeroBalanceFields() { this.runningBalance = null; this.cumulativeBalance = null; @@ -603,6 +624,8 @@ public Map toMapData(final String currencyCode) { thisTransactionData.put("currencyCode", currencyCode); thisTransactionData.put("amount", this.amount); thisTransactionData.put("overdraftAmount", this.overdraftAmount); + thisTransactionData.put("isNegativeBalance", + this.isNegativeBalance != null ? this.isNegativeBalance : MathUtil.isLessThanZero(runningBalance)); if (this.paymentDetail != null) { thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId()); @@ -667,7 +690,7 @@ public EndOfDayBalance toEndOfDayBalance(final LocalDateInterval periodInterval, numberOfDays = newInterval.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(balanceDate, openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final Money openingBalance, final LocalDate nextTransactionDate) { @@ -683,7 +706,7 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance, final Local if (!openingBalance.isEqualTo(endOfDayBalance) && numberOfDays > 1) { numberOfDays = numberOfDays - 1; } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, numberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, numberOfDays, currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { @@ -700,7 +723,8 @@ public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { } } - return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + return EndOfDayBalance.from(getTransactionDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays, + currency.getDigitsAfterDecimal()); } public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, final LocalDateInterval boundedBy) { @@ -738,7 +762,8 @@ public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, fi numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); } - return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance, + currency.getDigitsAfterDecimal()); } public boolean isBalanceInExistencesForOneDayOrMore() { @@ -881,4 +906,8 @@ public SavingsAccountTransactionDetailsForPostingPeriod toSavingsAccountTransact this.amount, currency, this.balanceNumberOfDays, isDeposit(), isWithdrawal(), isAllowOverDraft, isChargeTransactionAndNotReversed(), isDividendPayoutAndNotReversed()); } + + public void setNegativeBalance(Boolean negativeBalance) { + isNegativeBalance = negativeBalance; + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java index ca5779939ef..1191b278917 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java @@ -28,6 +28,8 @@ public enum SavingsEvent { SAVINGS_APPLICATION_APPROVED("application.approval"), // SAVINGS_APPLICATION_APPROVAL_UNDO("application.approval.undo"), // SAVINGS_ACTIVATE("activate"), // + SAVINGS_UNOD_ACTIVATE("activate.undo"), // + SAVINGS_UNDO_ACTIVATE("undo.activate"), // SAVINGS_DEPOSIT("deposit"), // SAVINGS_WITHDRAWAL("withdraw"), // SAVINGS_POST_INTEREST("interest.post"), // diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java index 917b377023a..8e3ca453301 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java @@ -20,6 +20,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; @@ -163,6 +164,10 @@ public class SavingsProduct extends AbstractPersistableCustom { @JoinTable(name = "m_savings_product_charge", joinColumns = @JoinColumn(name = "savings_product_id"), inverseJoinColumns = @JoinColumn(name = "charge_id")) protected Set charges; + @ManyToMany + @JoinTable(name = "m_savings_product_accrual_charge", joinColumns = @JoinColumn(name = "savings_product_id"), inverseJoinColumns = @JoinColumn(name = "charge_id")) + protected Set accrualCharges; + @Column(name = "allow_overdraft") private boolean allowOverdraft; @@ -220,14 +225,15 @@ public static SavingsProduct createNew(final String name, final String shortName final BigDecimal minRequiredBalance, final boolean lienAllowed, final BigDecimal maxAllowedLienLimit, final BigDecimal minBalanceForInterestCalculation, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, boolean withHoldTax, TaxGroup taxGroup, - final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat) { + final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, + final Set accrualCharges) { return new SavingsProduct(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, lienAllowed, maxAllowedLienLimit, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, - taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat); + taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accrualCharges); } protected SavingsProduct() { @@ -242,11 +248,12 @@ protected SavingsProduct(final String name, final String shortName, final String final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final boolean withdrawalFeeApplicableForTransfer, final AccountingRuleType accountingRuleType, final Set charges, final boolean allowOverdraft, final BigDecimal overdraftLimit, BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, - TaxGroup taxGroup) { + TaxGroup taxGroup, final Set accrualCharges) { this(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, - false, null, false, null, minBalanceForInterestCalculation, null, null, withHoldTax, taxGroup, null, null, null, null); + false, null, false, null, minBalanceForInterestCalculation, null, null, withHoldTax, taxGroup, null, null, null, null, + accrualCharges); } protected SavingsProduct(final String name, final String shortName, final String description, final MonetaryCurrency currency, @@ -259,7 +266,8 @@ protected SavingsProduct(final String name, final String shortName, final String final BigDecimal minRequiredBalance, final boolean lienAllowed, final BigDecimal maxAllowedLienLimit, BigDecimal minBalanceForInterestCalculation, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, final boolean withHoldTax, final TaxGroup taxGroup, - final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat) { + final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat, + final Set accrualCharges) { this.name = name; this.shortName = shortName; @@ -291,6 +299,10 @@ protected SavingsProduct(final String name, final String shortName, final String this.charges = charges; } + if (accrualCharges != null) { + this.accrualCharges = accrualCharges; + } + validateLockinDetails(); this.allowOverdraft = allowOverdraft; this.overdraftLimit = overdraftLimit; @@ -402,7 +414,7 @@ public Map update(final JsonCommand command) { actualChanges.put(digitsAfterDecimalParamName, newValue); actualChanges.put(localeParamName, localeAsInput); digitsAfterDecimal = newValue; - this.currency = new MonetaryCurrency(this.currency.getCode(), digitsAfterDecimal, this.currency.getCurrencyInMultiplesOf()); + this.currency = new MonetaryCurrency(this.currency.getCode(), digitsAfterDecimal, this.currency.getInMultiplesOf()); } String currencyCode = this.currency.getCode(); @@ -410,11 +422,10 @@ public Map update(final JsonCommand command) { final String newValue = command.stringValueOfParameterNamed(currencyCodeParamName); actualChanges.put(currencyCodeParamName, newValue); currencyCode = newValue; - this.currency = new MonetaryCurrency(currencyCode, this.currency.getDigitsAfterDecimal(), - this.currency.getCurrencyInMultiplesOf()); + this.currency = new MonetaryCurrency(currencyCode, this.currency.getDigitsAfterDecimal(), this.currency.getInMultiplesOf()); } - Integer inMultiplesOf = this.currency.getCurrencyInMultiplesOf(); + Integer inMultiplesOf = this.currency.getInMultiplesOf(); if (command.isChangeInIntegerParameterNamed(inMultiplesOfParamName, inMultiplesOf)) { final Integer newValue = command.integerValueOfParameterNamed(inMultiplesOfParamName); actualChanges.put(inMultiplesOfParamName, newValue); @@ -475,7 +486,6 @@ public Map update(final JsonCommand command) { this.lockinPeriodFrequencyType = newValue != null ? SavingsPeriodFrequencyType.fromInt(newValue).getValue() : newValue; } - // set period type to null if frequency is null if (this.lockinPeriodFrequency == null) { this.lockinPeriodFrequencyType = null; } @@ -492,12 +502,18 @@ public Map update(final JsonCommand command) { this.accountingRule = newValue; } - // charges if (command.hasParameter(chargesParamName)) { - final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); + JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); if (jsonArray != null) { actualChanges.put(chargesParamName, command.jsonFragment(chargesParamName)); } + + if (command.hasParameter(accrualChargesParamName)) { + jsonArray = command.arrayOfParameterNamed(accrualChargesParamName); + if (jsonArray != null) { + actualChanges.put(accrualChargesParamName, command.jsonFragment(accrualChargesParamName)); + } + } } if (command.isChangeInBooleanParameterNamed(allowOverdraftParamName, this.allowOverdraft)) { @@ -654,7 +670,6 @@ public boolean isCashBasedAccountingEnabled() { } // TODO this entire block is currently unnecessary as Savings does not have - // accrual accounting public boolean isAccrualBasedAccountingEnabled() { return isUpfrontAccrualAccounting() || isPeriodicAccrualAccounting(); } @@ -724,6 +739,14 @@ public Set charges() { return this.charges; } + public Set accrualCharges() { + return this.accrualCharges; + } + + public List accrualChargeIds() { + return accrualCharges.stream().map(Charge::getId).toList(); + } + public InterestRateChart applicableChart(@SuppressWarnings("unused") final LocalDate target) { return null; } @@ -780,4 +803,12 @@ public Long getDaysToEscheat() { return this.daysToEscheat; } + public void setCharges(Set charges) { + this.charges = charges; + } + + public void setAccrualCharges(Set accrualCharges) { + this.accrualCharges = accrualCharges; + } + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java index c47cb76ee3a..b8660e4cfb5 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.savings.domain; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; @@ -27,7 +28,6 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.digitsAfterDecimalParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.idParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.inMultiplesOfParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationDaysInYearTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; @@ -47,22 +47,16 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import java.math.BigDecimal; -import java.util.HashSet; import java.util.Set; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; @@ -74,15 +68,11 @@ import org.springframework.stereotype.Component; @Component -public class SavingsProductAssembler { - - private final ChargeRepositoryWrapper chargeRepository; - private final TaxGroupRepositoryWrapper taxGroupRepository; +public class SavingsProductAssembler extends SavingsProductBaseAssembler { @Autowired - public SavingsProductAssembler(final ChargeRepositoryWrapper chargeRepository, final TaxGroupRepositoryWrapper taxGroupRepository) { - this.chargeRepository = chargeRepository; - this.taxGroupRepository = taxGroupRepository; + public SavingsProductAssembler(ChargeRepositoryWrapper chargeRepository, TaxGroupRepositoryWrapper taxGroupRepository) { + super(chargeRepository, taxGroupRepository); } public SavingsProduct assemble(final JsonCommand command) { @@ -141,7 +131,10 @@ public SavingsProduct assemble(final JsonCommand command) { final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule")); // Savings product charges - final Set charges = assembleListOfSavingsProductCharges(command, currencyCode); + final Set charges = assembleListOfSavingsProductCharges(command, currencyCode, chargesParamName); + + // Savings product charges to be accrued + final Set accrualCharges = assembleListOfSavingsProductCharges(command, currencyCode, accrualChargesParamName); boolean allowOverdraft = false; if (command.parameterExists(allowOverdraftParamName)) { @@ -198,49 +191,7 @@ public SavingsProduct assemble(final JsonCommand command) { lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, accountingRuleType, charges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, lienAllowed, maxAllowedLienLimit, minBalanceForInterestCalculation, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, - taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat); - } - - public Set assembleListOfSavingsProductCharges(final JsonCommand command, final String savingsProductCurrencyCode) { - - final Set charges = new HashSet<>(); - - if (command.parameterExists(chargesParamName)) { - final JsonArray chargesArray = command.arrayOfParameterNamed(chargesParamName); - if (chargesArray != null) { - for (int i = 0; i < chargesArray.size(); i++) { - - final JsonObject jsonObject = chargesArray.get(i).getAsJsonObject(); - if (jsonObject.has(idParamName)) { - final Long id = jsonObject.get(idParamName).getAsLong(); - - final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(id); - - if (!charge.isSavingsCharge()) { - final String errorMessage = "Charge with identifier " + charge.getId() - + " cannot be applied to Savings product."; - throw new ChargeCannotBeAppliedToException("savings.product", errorMessage, charge.getId()); - } - - if (!savingsProductCurrencyCode.equals(charge.getCurrencyCode())) { - final String errorMessage = "Charge and Savings Product must have the same currency."; - throw new InvalidCurrencyException("charge", "attach.to.savings.product", errorMessage); - } - charges.add(charge); - } - } - } - } - - return charges; + taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accrualCharges); } - public TaxGroup assembleTaxGroup(final JsonCommand command) { - final Long taxGroupId = command.longValueOfParameterNamed(taxGroupIdParamName); - TaxGroup taxGroup = null; - if (taxGroupId != null) { - taxGroup = this.taxGroupRepository.findOneWithNotFoundDetection(taxGroupId); - } - return taxGroup; - } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java new file mode 100644 index 00000000000..69e83df2cfe --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java @@ -0,0 +1,86 @@ +/** + * 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.savings.domain; + +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.idParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; +import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.portfolio.tax.domain.TaxGroupRepositoryWrapper; + +@RequiredArgsConstructor +public class SavingsProductBaseAssembler { + + protected final ChargeRepositoryWrapper chargeRepository; + protected final TaxGroupRepositoryWrapper taxGroupRepository; + + public Set assembleListOfSavingsProductCharges(final JsonCommand command, final String savingsProductCurrencyCode, + final String chargesParameterName) { + + final Set charges = new HashSet<>(); + + if (command.parameterExists(chargesParameterName)) { + final JsonArray chargesArray = command.arrayOfParameterNamed(chargesParameterName); + if (chargesArray != null) { + for (int i = 0; i < chargesArray.size(); i++) { + + final JsonObject jsonObject = chargesArray.get(i).getAsJsonObject(); + if (jsonObject.has(idParamName)) { + final Long id = jsonObject.get(idParamName).getAsLong(); + + final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(id); + + if (!charge.isSavingsCharge()) { + final String errorMessage = "Charge with identifier " + charge.getId() + + " cannot be applied to Savings product."; + throw new ChargeCannotBeAppliedToException("savings.product", errorMessage, charge.getId()); + } + + if (!savingsProductCurrencyCode.equals(charge.getCurrencyCode())) { + final String errorMessage = "Charge and Savings Product must have the same currency."; + throw new InvalidCurrencyException("charge", "attach.to.savings.product", errorMessage); + } + charges.add(charge); + } + } + } + } + + return charges; + } + + public TaxGroup assembleTaxGroup(final JsonCommand command) { + final Long taxGroupId = command.longValueOfParameterNamed(taxGroupIdParamName); + TaxGroup taxGroup = null; + if (taxGroupId != null) { + taxGroup = this.taxGroupRepository.findOneWithNotFoundDetection(taxGroupId); + } + return taxGroup; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java index 50f7d4103bd..d934705187c 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountBlockedException.java @@ -26,4 +26,9 @@ public SavingsAccountBlockedException(final Long accountId) { super("error.msg.saving.account.blocked.transaction.not.allowed", "Any transaction to " + accountId + " is not allowed, since the account is blocked", accountId); } + + public SavingsAccountBlockedException(final String accountNo) { + super("error.msg.saving.account.blocked.transaction.not.allowed", + "Any transaction to " + accountNo + " is not allowed, since the account is blocked", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java index d54cb87745f..c4e1647925a 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountCreditsBlockedException.java @@ -26,4 +26,9 @@ public SavingsAccountCreditsBlockedException(final Long accountId) { super("error.msg.savings.account.credit.transaction.not.allowed", "Any Credit transactions to " + accountId + " is not allowed, since the account is blocked for credits", accountId); } + + public SavingsAccountCreditsBlockedException(final String accountNo) { + super("error.msg.savings.account.credit.transaction.not.allowed", + "Any Credit transactions to " + accountNo + " is not allowed, since the account is blocked for credits", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java index 8a2903b4ddf..412c547eae4 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsAccountDebitsBlockedException.java @@ -27,4 +27,8 @@ public SavingsAccountDebitsBlockedException(final Long accountId) { "Any debit transactions from " + accountId + " is not allowed, since the account is blocked for debits", accountId); } + public SavingsAccountDebitsBlockedException(final String accountNo) { + super("error.msg.savings.account.debit.transaction.not.allowed", + "Any debit transactions from " + accountNo + " is not allowed, since the account is blocked for debits", accountNo); + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java new file mode 100644 index 00000000000..8769c72ad72 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsDepositsWithActiveTransferFundsException.java @@ -0,0 +1,35 @@ +/** + * 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.savings.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown an action to transition a loan from one state to another violates + * a domain rule. + */ +public class SavingsDepositsWithActiveTransferFundsException extends AbstractPlatformDomainRuleException { + + public SavingsDepositsWithActiveTransferFundsException(final String accountNo, final Long transactionId) { + super("error.msg.savings.deposits.with.active.transfer.funds.transaction", + "Savings/Deposit account " + accountNo + " with active transfer funds transaction " + transactionId, transactionId, + accountNo); + } + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java new file mode 100644 index 00000000000..526fafc17b5 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsTransferTransactionsAlreadyUndoneException.java @@ -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. + */ +package org.apache.fineract.portfolio.savings.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * {@link AbstractPlatformDomainRuleException} thrown an action to transition a loan from one state to another violates + * a domain rule. + */ +public class SavingsTransferTransactionsAlreadyUndoneException extends AbstractPlatformDomainRuleException { + + public SavingsTransferTransactionsAlreadyUndoneException(final String accountNo, final Long transactionId) { + super("error.msg.savings.transfer.transactions.cannot.be.undone", + "Transaction related to savings account " + accountNo + " already undone", transactionId, accountNo); + } + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java index 325c9dd8a63..c3990f949e0 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.service; import java.math.BigDecimal; +import java.math.MathContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; @@ -39,7 +40,7 @@ SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatte boolean backdatedTxnsAllowedTill); void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds, - boolean backdatedTxnsAllowedTill); + boolean backdatedTxnsAllowedTill, boolean isNegativeBalance); SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, boolean backdatedTxnsAllowedTill); @@ -48,4 +49,15 @@ SavingsAccountTransaction handleReversal(SavingsAccount account, List retrieveAllSavingsDataForInterestPosting(boolean backda List retrieveAllTransactionData(List refNo); Long retrieveAccountIdByExternalId(ExternalId externalId); + + Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings); + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index 295d3f55aee..a2f4239b4f1 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -30,12 +30,13 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.savings.data.SavingsAccountData; @@ -108,33 +109,44 @@ private void batchUpdateJournalEntries(final List savingsAcc for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null) { final String key = savingsAccountTransactionData.getRefNo(); - if (savingsAccountTransactionDataHashMap.containsKey(key)) { - final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); - savingsAccountTransactionData.setId(dataFromFetch.getId()); - if (savingsAccountData.getGlAccountIdForSavingsControl() != 0 - && savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) { - OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime(); - paramsForGLInsertion.add(new Object[] { savingsAccountData.getGlAccountIdForSavingsControl(), - savingsAccountData.getOfficeId(), null, currencyCode, - SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), - savingsAccountTransactionData.getId(), null, false, null, false, - savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(), - savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(), - savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, - savingsAccountTransactionData.getTransactionDate(), null, userId, userId, - DateUtils.getBusinessLocalDate() }); - - paramsForGLInsertion.add(new Object[] { savingsAccountData.getGlAccountIdForInterestOnSavings(), - savingsAccountData.getOfficeId(), null, currencyCode, - SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), - savingsAccountTransactionData.getId(), null, false, null, false, - savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(), - savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(), - savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, - savingsAccountTransactionData.getTransactionDate(), null, userId, userId, - DateUtils.getBusinessLocalDate() }); - } + final Boolean isNegativeBalance = savingsAccountTransactionData.getIsNegativeBalance(); + final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); + savingsAccountTransactionData.setId(dataFromFetch.getId()); + if (savingsAccountData.getGlAccountIdForSavingsControl() != 0 + && savingsAccountData.getGlAccountIdForInterestOnSavings() != 0) { + OffsetDateTime auditDatetime = DateUtils.getAuditOffsetDateTime(); + paramsForGLInsertion.add(new Object[] { + isNegativeBalance + ? MathUtil.isLessThanZero(savingsAccountTransactionData.getRunningBalance()) + ? savingsAccountData.getGlAccountIdForInterestReceivableNegative() + : savingsAccountData.getGlAccountIdForInterestReceivableNegative() + : savingsAccountData.getGlAccountIdForSavingsControl(), + + savingsAccountData.getOfficeId(), null, currencyCode, + SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + savingsAccountTransactionData.getTransactionDate(), JournalEntryType.CREDIT.getValue().longValue(), + savingsAccountTransactionData.getAmount(), null, JournalEntryType.CREDIT.getValue().longValue(), + savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, + savingsAccountTransactionData.getTransactionDate(), null, userId, userId, + DateUtils.getBusinessLocalDate() }); + + paramsForGLInsertion.add(new Object[] { + isNegativeBalance + ? MathUtil.isLessThanZero(savingsAccountTransactionData.getRunningBalance()) + ? savingsAccountData.getGlAccountIdForOverdraftPorfolioNegative() + : savingsAccountData.getGlAccountIdForSavingsControlAcountPositiveInterestNegative() + : savingsAccountData.getGlAccountIdForInterestOnSavings(), + savingsAccountData.getOfficeId(), null, currencyCode, + SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + savingsAccountTransactionData.getTransactionDate(), JournalEntryType.DEBIT.getValue().longValue(), + savingsAccountTransactionData.getAmount(), null, JournalEntryType.DEBIT.getValue().longValue(), + savingsAccountData.getId(), auditDatetime, auditDatetime, false, BigDecimal.ZERO, BigDecimal.ZERO, null, + savingsAccountTransactionData.getTransactionDate(), null, userId, userId, + DateUtils.getBusinessLocalDate() }); } + } } } @@ -184,9 +196,9 @@ private void batchUpdate(final List savingsAccountDataList) List savingsAccountTransactionDataList = savingsAccountData.getSavingsAccountTransactionData(); for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null) { - UUID uuid = UUID.randomUUID(); - savingsAccountTransactionData.setRefNo(uuid.toString()); - transRefNo.add(uuid.toString()); + final ExternalId externalId = ExternalId.generate(); + savingsAccountTransactionData.setRefNo(externalId.toString()); + transRefNo.add(externalId.toString()); paramsForTransactionInsertion.add(new Object[] { savingsAccountData.getId(), savingsAccountData.getOfficeId(), savingsAccountTransactionData.isReversed(), savingsAccountTransactionData.getTransactionType().getId(), savingsAccountTransactionData.getTransactionDate(), savingsAccountTransactionData.getAmount(), diff --git a/fineract-validation/src/main/resources/fineract/validation/messages.properties b/fineract-validation/src/main/resources/fineract/validation/messages.properties index 8508fdd06d9..a4e53db4da5 100644 --- a/fineract-validation/src/main/resources/fineract/validation/messages.properties +++ b/fineract-validation/src/main/resources/fineract/validation/messages.properties @@ -32,3 +32,12 @@ org.apache.fineract.businessdate.locale.not-blank=The parameter 'locale' is mand # External Events org.apache.fineract.externalevent.configurations.not-null=The parameter 'externalEventConfigurations' is mandatory: '${validatedValue}'. + +# Cache + +org.apache.fineract.cache.cache-type.not-null=The parameter 'cacheType' is mandatory. + +# Currency + +org.apache.fineract.organisation.monetary.currencies.not-null=The parameter 'currencies' is mandatory. +org.apache.fineract.organisation.monetary.currencies.not-empty=The parameter 'currencies' cannot be empty. diff --git a/fineract-validation/src/main/resources/fineract/validation/messages_en.properties b/fineract-validation/src/main/resources/fineract/validation/messages_en.properties index 8508fdd06d9..a4e53db4da5 100644 --- a/fineract-validation/src/main/resources/fineract/validation/messages_en.properties +++ b/fineract-validation/src/main/resources/fineract/validation/messages_en.properties @@ -32,3 +32,12 @@ org.apache.fineract.businessdate.locale.not-blank=The parameter 'locale' is mand # External Events org.apache.fineract.externalevent.configurations.not-null=The parameter 'externalEventConfigurations' is mandatory: '${validatedValue}'. + +# Cache + +org.apache.fineract.cache.cache-type.not-null=The parameter 'cacheType' is mandatory. + +# Currency + +org.apache.fineract.organisation.monetary.currencies.not-null=The parameter 'currencies' is mandatory. +org.apache.fineract.organisation.monetary.currencies.not-empty=The parameter 'currencies' cannot be empty. diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java index 47a80859b34..7b4a50bde8c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java @@ -642,6 +642,15 @@ public static Integer createSavingsProduct(final String minOpenningBalance, fina return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); } + public static Integer createSavingsProductWithAccrualAccounting(final String minOpenningBalance, final Account... accounts) { + LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------"); + final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() // + .withInterestPostingPeriodTypeAsQuarterly() // + .withInterestCalculationPeriodTypeAsDailyBalance() // + .withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsAccrualBased(accounts).build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + private Integer createFixedDepositProduct(final String validFrom, final String validTo, Account... accounts) { LOG.info("------------------------------CREATING NEW FIXED DEPOSIT PRODUCT ---------------------------------------"); FixedDepositProductHelper fixedDepositProductHelper = new FixedDepositProductHelper(requestSpec, responseSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java index e903059f962..c9c7426462c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccrualsOnLoanClosureTest.java @@ -86,6 +86,9 @@ public void testAccrualCreatedOnLoanClosureWithSubmittedDate() { transaction(800.0, "Disbursement", "22 April 2024", 800.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), transaction(820.0, "Repayment", "25 April 2024", 0.0, 800.0, 0.0, 0.0, 20.0, 0.0, 0.0), transaction(20.0, "Accrual", "25 April 2024", 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0)); + + globalConfigurationHelper.updateGlobalConfiguration(CHARGE_ACCRUAL_DATE, + new PutGlobalConfigurationsRequest().stringValue("due-date")); }); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ActuatorIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ActuatorIntegrationTest.java index 824b6c2bd35..3a6437cefd3 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ActuatorIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ActuatorIntegrationTest.java @@ -25,14 +25,24 @@ import io.restassured.http.ContentType; import io.restassured.response.Response; import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.integrationtests.common.Utils; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +@Slf4j public class ActuatorIntegrationTest { private static final String INFO_URL = "/fineract-provider/actuator/info"; + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + } + @Test public void testActuatorGitBuildInfo() { + log.info(INFO_URL); Response response = RestAssured.given().headers("Content-Type", ContentType.JSON, "Accept", ContentType.JSON).when().get(INFO_URL) .then().contentType(ContentType.JSON).extract().response(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CurrenciesTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CurrenciesTest.java index 0125dfb5c09..a66b60d2417 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CurrenciesTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CurrenciesTest.java @@ -18,17 +18,20 @@ */ package org.apache.fineract.integrationtests; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; -import java.util.ArrayList; -import java.util.Collections; +import java.util.List; +import java.util.Objects; import org.apache.fineract.integrationtests.common.CurrenciesHelper; import org.apache.fineract.integrationtests.common.CurrencyDomain; import org.apache.fineract.integrationtests.common.Utils; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,43 +55,32 @@ public void testCurrencyElements() { CurrencyDomain currency = CurrenciesHelper.getCurrencybyCode(requestSpec, responseSpec, "USD"); CurrencyDomain usd = CurrencyDomain.create("USD", "US Dollar", 2, "$", "currency.USD", "US Dollar ($)").build(); - Assertions.assertTrue(currency.getDecimalPlaces() >= 0); - Assertions.assertNotNull(currency.getName()); - Assertions.assertNotNull(currency.getDisplaySymbol()); - Assertions.assertNotNull(currency.getDisplayLabel()); - Assertions.assertNotNull(currency.getNameCode()); + assertNotNull(currency); + assertTrue(currency.getDecimalPlaces() >= 0); + assertNotNull(currency.getName()); + assertNotNull(currency.getDisplaySymbol()); + assertNotNull(currency.getDisplayLabel()); + assertNotNull(currency.getNameCode()); - Assertions.assertEquals(usd, currency); + assertEquals(usd, currency); } @Test public void testUpdateCurrencySelection() { + var currenciestoUpdate = List.of("KES", "BND", "LBP", "GHC", "USD", "INR"); - // Test updation - ArrayList currenciestoUpdate = new ArrayList(); - currenciestoUpdate.add("KES"); - currenciestoUpdate.add("BND"); - currenciestoUpdate.add("LBP"); - currenciestoUpdate.add("GHC"); - currenciestoUpdate.add("USD"); - currenciestoUpdate.add("INR"); - - ArrayList currenciesOutput = CurrenciesHelper.updateSelectedCurrencies(this.requestSpec, this.responseSpec, - currenciestoUpdate); - Assertions.assertNotNull(currenciesOutput); + var currenciesOutput = CurrenciesHelper.updateSelectedCurrencies(this.requestSpec, this.responseSpec, currenciestoUpdate); - Assertions.assertEquals(currenciestoUpdate, currenciesOutput, "Verifying Do Outputed Currencies Match after Updation"); + assertNotNull(currenciesOutput); + assertEquals(currenciestoUpdate, currenciesOutput, "Verifying returned currencies match after update"); - // Test that output matches updation - ArrayList currenciesBeforeUpdate = new ArrayList(); - for (String e : currenciestoUpdate) { - currenciesBeforeUpdate.add(CurrenciesHelper.getCurrencybyCode(requestSpec, responseSpec, e)); - } - Collections.sort(currenciesBeforeUpdate); + var currenciesBeforeUpdate = currenciestoUpdate.stream() + .map(currency -> CurrenciesHelper.getCurrencybyCode(requestSpec, responseSpec, currency)).filter(Objects::nonNull).sorted() + .toList(); - ArrayList currenciesAfterUpdate = CurrenciesHelper.getSelectedCurrencies(requestSpec, responseSpec); - Assertions.assertNotNull(currenciesAfterUpdate); + var currenciesAfterUpdate = CurrenciesHelper.getSelectedCurrencies(requestSpec, responseSpec); - Assertions.assertEquals(currenciesBeforeUpdate, currenciesAfterUpdate, "Verifying Do Selected Currencies Match after Updation"); + assertNotNull(currenciesAfterUpdate); + assertEquals(currenciesBeforeUpdate, currenciesAfterUpdate, "Verifying selected currencies match after update"); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java index cbaebb6399c..1ba83b0b112 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyAndChargebackIntegrationTest.java @@ -168,9 +168,8 @@ public void testLoanClassificationStepAsPartOfCOB(LoanProductTestBuilder loanPro BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); log.info("Current Business date {}", businessDate); - // Run the Loan COB Job - final String jobName = "Loan COB"; - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have a delinquency classification getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -293,9 +292,8 @@ public void testLoanClassificationStepAsPartOfCOBRepeated(LoanProductTestBuilder BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); log.info("Current Business date {}", businessDate); - // Run the Loan COB Job - final String jobName = "Loan COB"; - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have a delinquency classification getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index f8c2522720d..6f7669bb352 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -76,7 +76,6 @@ import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; -import org.apache.fineract.integrationtests.common.loans.CobHelper; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; @@ -867,9 +866,8 @@ public void testLoanClassificationStepAsPartOfCOB() { assertTrue(jobBusinessStepConfigData.getBusinessSteps().stream().anyMatch( businessStep -> BusinessConfigurationApiTest.LOAN_DELINQUENCY_CLASSIFICATION.equals(businessStep.getStepName()))); - // Run first time the Loan COB Job - final String jobName = "Loan COB"; - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have not a delinquency classification GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -885,13 +883,11 @@ public void testLoanClassificationStepAsPartOfCOB() { } // Move the Business date to get older the loan and to have an overdue loan - LocalDate lastLoanCOBBusinessDate = bussinesLocalDate; bussinesLocalDate = bussinesLocalDate.plusDays(3); - schedulerJobHelper.fastForwardTime(lastLoanCOBBusinessDate, bussinesLocalDate, jobName, responseSpec); - log.info("Current date {}", bussinesLocalDate); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, bussinesLocalDate); - // Run Second time the Job - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have a delinquency classification getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -968,15 +964,11 @@ public void testLoanClassificationToValidateNegatives() { log.info("Loan Delinquency Range is null {}", (firstTestCase == null)); loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse); - final String jobName = "Loan COB"; - bussinesLocalDate = Utils.getDateAsLocalDate("31 January 2012"); - LocalDate lastLoanCOBBusinessDate = bussinesLocalDate.minusDays(1); - schedulerJobHelper.fastForwardTime(lastLoanCOBBusinessDate, bussinesLocalDate, jobName, responseSpec); - log.info("Current date {}", bussinesLocalDate); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, bussinesLocalDate); - // Run Second time the Job - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have a delinquency classification getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -1059,15 +1051,11 @@ public void testLoanClassificationUsingAgeingArrears() { log.info("Loan Product Arrears: {}", loanProductsProductIdResponseUpd.getInArrearsTolerance()); assertEquals(0, loanProductsProductIdResponseUpd.getInArrearsTolerance()); - final String jobName = "Loan COB"; - bussinesLocalDate = Utils.getDateAsLocalDate("31 January 2012"); - LocalDate lastLoanCOBBusinessDate = bussinesLocalDate.minusDays(1); - schedulerJobHelper.fastForwardTime(lastLoanCOBBusinessDate, bussinesLocalDate, jobName, responseSpec); - log.info("Current date {}", bussinesLocalDate); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, bussinesLocalDate); - // Run Second time the Job - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Get loan details expecting to have a delinquency classification getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); @@ -1114,24 +1102,22 @@ public void testDelinquencyWithPauseLettingPauseExpire() { log.info("Loan Account Arrears {}", getLoansLoanIdResponse.getInArrearsTolerance()); assertEquals(3, getLoansLoanIdResponse.getInArrearsTolerance()); - final String jobName = "Loan COB"; - - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "04 February 2012"); updateBusinessDate("06 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); PostLoansDelinquencyActionResponse pauseDelinquencyResponse = loanTransactionHelper .createLoanDelinquencyAction(loanId.longValue(), PAUSE, "06 February 2012", "10 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "07 February 2012"); updateBusinessDate("09 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "10 March 2012"); updateBusinessDate("12 March 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 2049.99, 36); }); } @@ -1165,28 +1151,26 @@ public void testDelinquencyWithPauseResumeBeforePauseExpires() { log.info("Loan Account Arrears {}", getLoansLoanIdResponse.getInArrearsTolerance()); assertEquals(3, getLoansLoanIdResponse.getInArrearsTolerance()); - final String jobName = "Loan COB"; - - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "04 February 2012"); updateBusinessDate("06 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); PostLoansDelinquencyActionResponse pauseDelinquencyResponse = loanTransactionHelper .createLoanDelinquencyAction(loanId.longValue(), PAUSE, "06 February 2012", "10 March 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "07 February 2012"); updateBusinessDate("09 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); bussinesLocalDate = Utils.getDateAsLocalDate("10 February 2012"); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, bussinesLocalDate); loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), RESUME, "10 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "10 March 2012"); updateBusinessDate("12 March 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 2049.99, 36); }); } @@ -1217,38 +1201,36 @@ public void testDelinquencyWithMultiplePausePeriods() { log.info("Loan Account Arrears {}", getLoansLoanIdResponse.getInArrearsTolerance()); assertEquals(3, getLoansLoanIdResponse.getInArrearsTolerance()); - final String jobName = "Loan COB"; - // delinquent days: 5 - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "04 February 2012"); updateBusinessDate("06 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); // Add delinquency pause on 06 February 2012 PostLoansDelinquencyActionResponse pauseDelinquencyResponse = loanTransactionHelper .createLoanDelinquencyAction(loanId.longValue(), PAUSE, "06 February 2012", "10 March 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "07 February 2012"); updateBusinessDate("09 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); // Add delinquency resume on 10 February 2012 updateBusinessDate("10 February 2012"); loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), RESUME, "10 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "11 February 2012"); updateBusinessDate("13 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 8); // Add new pause on 13 February 2012 pauseDelinquencyResponse = loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), PAUSE, "13 February 2012", "18 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "21 February 2012"); updateBusinessDate("23 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 13); // Add new pause on 23 February 2012 @@ -1256,9 +1238,9 @@ public void testDelinquencyWithMultiplePausePeriods() { "28 February 2012"); updateBusinessDate("25 February 2012"); loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), RESUME, "25 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "10 March 2012"); updateBusinessDate("12 March 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 2049.99, 29); }); } @@ -1299,36 +1281,34 @@ public void testDelinquencyWithMultiplePausePeriodsWithInstallmentLevelDelinquen log.info("Loan Account Arrears {}", getLoansLoanIdResponse.getInArrearsTolerance()); assertEquals(3, getLoansLoanIdResponse.getInArrearsTolerance()); - final String jobName = "Loan COB"; - // delinquent days: 5 - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "04 February 2012"); updateBusinessDate("06 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); PostLoansDelinquencyActionResponse pauseDelinquencyResponse = loanTransactionHelper .createLoanDelinquencyAction(loanId.longValue(), PAUSE, "06 February 2012", "10 March 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "07 February 2012"); updateBusinessDate("09 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 5); updateBusinessDate("10 February 2012"); loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), RESUME, "10 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "11 February 2012"); updateBusinessDate("13 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 8); pauseDelinquencyResponse = loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), PAUSE, "13 February 2012", "18 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "21 February 2012"); updateBusinessDate("23 February 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); verifyDelinquency(loanId, "01 February 2012", 1033.33, 13); pauseDelinquencyResponse = loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), PAUSE, "23 February 2012", @@ -1337,9 +1317,9 @@ public void testDelinquencyWithMultiplePausePeriodsWithInstallmentLevelDelinquen updateBusinessDate("25 February 2012"); loanTransactionHelper.createLoanDelinquencyAction(loanId.longValue(), RESUME, "25 February 2012"); - CobHelper.fastForwardLoansLastCOBDate(requestSpec, responseSpec204, loanId, "12 March 2012"); updateBusinessDate("14 March 2012"); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse); @@ -1447,10 +1427,10 @@ public void testLoanClassificationOnlyForActiveLoanWithCOB() { getLoanProductsProductResponse.getId().toString(), operationDate, null); // run cob for business date 01 January 2012 - final String jobName = "Loan COB"; bussinesLocalDate = Utils.getDateAsLocalDate(operationDate); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, bussinesLocalDate); - schedulerJobHelper.executeAndAwaitJob(jobName); + // Run the Loan inline COB Job + inlineLoanCOBHelper.executeInlineCOB(Long.valueOf(loanId)); // Loan delinquency data GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java new file mode 100644 index 00000000000..4ce6ed3085e --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java @@ -0,0 +1,264 @@ +/** + * 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.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration tests for Buy Down Fee functionality in Progressive Loans + */ +@Slf4j +@ExtendWith({ LoanTestLifecycleExtension.class }) +public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { + + private Long clientId; + private Long loanId; + + // Buy Down Fee accounts for accrual-based accounting + private final Account buyDownExpenseAccount = accountHelper.createExpenseAccount(); + private final Account deferredIncomeLiabilityAccount = accountHelper.createLiabilityAccount(); + + // Additional receivable accounts required for accrual-based accounting + private final Account interestReceivableAccount = accountHelper.createAssetAccount(); + private final Account feeReceivableAccount = accountHelper.createAssetAccount(); + private final Account penaltyReceivableAccount = accountHelper.createAssetAccount(); + + @BeforeEach + public void beforeEach() { + runAt("01 September 2024", () -> { + clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(createProgressiveLoanProductWithBuyDownFee()); + + // Apply for the loan with proper progressive loan settings + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "01 September 2024", 1000.0, 10.0, 12, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 September 2024")); + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 September 2024"); + }); + } + + @Test + public void testBuyDownFeeOnProgressiveLoan() { + runAt("02 September 2024", () -> { + // Verify loan product has buy down fee enabled + final GetLoansLoanIdResponse loanDetailsBeforeTransaction = loanTransactionHelper.getLoanDetails(loanId); + assertNotNull(loanDetailsBeforeTransaction); + log.info("Loan Product: {}", loanDetailsBeforeTransaction.getLoanProductName()); + + // Create buy down fee transaction + Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 500.0, "02 September 2024"); + + assertNotNull(buyDownFeeTransactionId); + + // Verify transaction was created in loan details + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + assertNotNull(loanDetails); + + // Find the buy down fee transaction + boolean buyDownFeeFound = false; + for (GetLoansLoanIdTransactions transaction : loanDetails.getTransactions()) { + if (transaction.getType() != null && transaction.getType().getId() != null && transaction.getType().getId().equals(40L)) { + buyDownFeeFound = true; + assertEquals(0, BigDecimal.valueOf(500.0).compareTo(transaction.getAmount())); + assertEquals(Long.valueOf(40), transaction.getType().getId()); + assertEquals("Buy Down Fee", transaction.getType().getValue()); + break; + } + } + assertTrue(buyDownFeeFound, "Buy down fee transaction should be found in loan transactions"); + }); + } + + @Test + public void testBuyDownFeeWithNote() { + runAt("03 September 2024", () -> { + String externalId = UUID.randomUUID().toString(); + String noteText = "Buy Down Fee - Test Note"; + + PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("03 September 2024").locale("en") + .transactionAmount(250.0).externalId(externalId).note(noteText)); + + assertNotNull(response.getResourceId()); + + // Verify transaction details + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + GetLoansLoanIdTransactions buyDownFeeTransaction = loanDetails.getTransactions().stream() + .filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L)) + .filter(t -> externalId.equals(t.getExternalId())).findFirst().orElse(null); + + assertNotNull(buyDownFeeTransaction, "Buy down fee transaction should exist"); + assertEquals(0, BigDecimal.valueOf(250.0).compareTo(buyDownFeeTransaction.getAmount())); + assertEquals(externalId, buyDownFeeTransaction.getExternalId()); + }); + } + + @Test + public void testMultipleBuyDownFees() { + runAt("04 September 2024", () -> { + // Add first buy down fee + Long firstBuyDownFeeId = addBuyDownFeeForLoan(loanId, 200.0, "04 September 2024"); + + // Add second buy down fee + Long secondBuyDownFeeId = addBuyDownFeeForLoan(loanId, 150.0, "04 September 2024"); + + assertNotNull(firstBuyDownFeeId); + assertNotNull(secondBuyDownFeeId); + + // Verify both transactions exist + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + long buyDownFeeCount = loanDetails.getTransactions().stream() + .filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L)).count(); + + assertEquals(2, buyDownFeeCount, "Should have 2 buy down fee transactions"); + }); + } + + @Test + public void testBuyDownFeeAccountingEntries() { + runAt("04 September 2024", () -> { + // Add Buy Down fee transaction + Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 250.0, "04 September 2024"); + assertNotNull(buyDownFeeTransactionId); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + GetLoansLoanIdTransactions buyDownFeeTransaction = loanDetails.getTransactions().stream() + .filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L)) + .filter(t -> buyDownFeeTransactionId.equals(t.getId())).findFirst().orElse(null); + + assertNotNull(buyDownFeeTransaction, "Buy down fee transaction should exist"); + assertEquals(0, BigDecimal.valueOf(250.0).compareTo(buyDownFeeTransaction.getAmount())); + + verifyTRJournalEntries(buyDownFeeTransactionId, debit(buyDownExpenseAccount, 250.0), // DR: Buy Down Expense + credit(deferredIncomeLiabilityAccount, 250.0) // CR: Deferred Income Liability + ); + + log.info("Buy Down Fee transaction created successfully (accounting validation pending client model regeneration)"); + }); + } + + @Test + public void testBuyDownFeeValidation() { + runAt("05 September 2024", () -> { + // Test with negative amount (should fail) + try { + PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 September 2024") + .locale("en").transactionAmount(-100.0).note("Invalid negative amount")); + assertTrue(false, "Buy down fee with negative amount should have failed"); + } catch (Exception e) { + // Expected: validation should prevent negative amounts + log.info("Expected validation error for negative amount: {}", e.getMessage()); + } + + // Test with zero amount (should fail) + try { + PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 September 2024") + .locale("en").transactionAmount(0.0).note("Invalid zero amount")); + assertTrue(false, "Buy down fee with zero amount should have failed"); + } catch (Exception e) { + // Expected: validation should prevent zero amounts + log.info("Expected validation error for zero amount: {}", e.getMessage()); + } + }); + } + + /** + * Creates a progressive loan product with buy down fee enabled + */ + private PostLoanProductsRequest createProgressiveLoanProductWithBuyDownFee() { + // Create a progressive loan product with accrual-based accounting and proper GL mappings + return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6)) + .shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Progressive loan product with buy down fee enabled") + .includeInBorrowerCycle(false).useBorrowerCycle(false).currencyCode("USD").digitsAfterDecimal(2).principal(1000.0) + .minPrincipal(100.0).maxPrincipal(10000.0).numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24) + .repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0) + .minInterestRatePerPeriod(0.0).maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS) + .amortizationType(AmortizationType.EQUAL_INSTALLMENTS).interestType(InterestType.DECLINING_BALANCE) + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).allowPartialPeriodInterestCalcualtion(false) + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy") + .paymentAllocation(List.of(createDefaultPaymentAllocation("NEXT_INSTALLMENT"))).creditAllocation(List.of()) + .daysInMonthType(30).daysInYearType(360).isInterestRecalculationEnabled(false).accountingRule(3) // Accrual-based + // accounting + // GL Account Mappings for Accrual-Based Accounting + .fundSourceAccountId(fundSource.getAccountID().longValue()) + .loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue()) + .transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue()) + .interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue()) + .incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue()) + .incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue()) + .incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue()) + .writeOffAccountId(writtenOffAccount.getAccountID().longValue()) + .overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue()) + // Receivable accounts required for accrual-based accounting + .receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue()) + .receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue()) + .receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue()) + .buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()) + .incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()).loanScheduleType("PROGRESSIVE") + .loanScheduleProcessingType("HORIZONTAL").enableBuyDownFee(true) + .buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT) + .buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION) + .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy"); + } + + /** + * Helper method to add buy down fee for a loan + * + * @param loanId + * the ID of the loan to add the buy down fee to + * @param amount + * the amount of the buy down fee + * @param date + * the transaction date in format specified by DATETIME_PATTERN + * @return the ID of the created buy down fee transaction + */ + private Long addBuyDownFeeForLoan(Long loanId, Double amount, String date) { + String buyDownFeeExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en") + .transactionAmount(amount).externalId(buyDownFeeExternalId).note("Buy Down Fee Transaction")); + return response.getResourceId(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java index fc81ebf76ef..c22516c13b3 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.integrationtests; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; @@ -682,6 +685,46 @@ public void testProgressiveChargeBackInterestRecalculation() { }); } + @Test + public void testRunCOBJobAfterUndoDisbursement() { + AtomicReference loanIdRef = new AtomicReference<>(); + setup(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableAccrualActivityPosting(true)); + + runAt("1 April 2025", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 March 2025", 430.0, + 26.0, 6, null); + + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(430), "1 March 2025"); + + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateTransactionsExist(loanDetails, // + transaction(9.02, "Accrual", "31 March 2025", 0.0, 0.0, 9.02, 0.0, 0.0, 0.0, 0.0)); + assertEquals(LocalDate.of(2025, 3, 31), loanDetails.getLastClosedBusinessDate()); + + undoDisbursement(loanId.intValue()); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + assertNull(loanDetails.getLastClosedBusinessDate()); + + disburseLoan(loanIdRef.get(), BigDecimal.valueOf(430), "2 March 2025"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + assertNull(loanDetails.getLastClosedBusinessDate()); + }); + + runAt("2 April 2025", () -> { + executeInlineCOB(loanIdRef.get()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get()); + validateTransactionsExist(loanDetails, // + transaction(9.02, "Accrual", "01 April 2025", 0.0, 0.0, 9.02, 0.0, 0.0, 0.0, 0.0)); + assertEquals(LocalDate.of(2025, 4, 1), loanDetails.getLastClosedBusinessDate()); + }); + } + private List chargebackCreditAllocationOrders(List allocationIds) { List creditAllocationOrders = new ArrayList<>(allocationIds.size()); for (int i = 0; i < allocationIds.size(); i++) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoRepaymentWithDownPaymentIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoRepaymentWithDownPaymentIntegrationTest.java index e584fac4690..b2d145f7e6e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoRepaymentWithDownPaymentIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoRepaymentWithDownPaymentIntegrationTest.java @@ -192,6 +192,8 @@ public void undoRepaymentWithDownPaymentAndAdvancedPaymentAllocationTest() { loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); assertEquals(500.0, Utils.getDoubleValue(loanDetails.getSummary().getTotalOutstanding())); + globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, + new PutGlobalConfigurationsRequest().enabled(false)); } private Integer createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrenciesHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrenciesHelper.java index e7556a71c2c..528bf59966f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrenciesHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/CurrenciesHelper.java @@ -24,6 +24,8 @@ import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,15 +43,13 @@ private CurrenciesHelper() { // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) - public static ArrayList getAllCurrencies(final RequestSpecification requestSpec, - final ResponseSpecification responseSpec) { - final String GET_ALL_CURRENCIES_URL = CURRENCIES_URL + "?" + Utils.TENANT_IDENTIFIER; + public static List getAllCurrencies(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { LOG.info("------------------------ RETRIEVING ALL CURRENCIES -------------------------"); - final HashMap response = Utils.performServerGet(requestSpec, responseSpec, GET_ALL_CURRENCIES_URL, ""); - ArrayList selectedCurrencyOptions = (ArrayList) response.get("selectedCurrencyOptions"); - ArrayList currencyOptions = (ArrayList) response.get("currencyOptions"); + HashMap response = Utils.performServerGet(requestSpec, responseSpec, CURRENCIES_URL + "?" + Utils.TENANT_IDENTIFIER, ""); + var selectedCurrencyOptions = (ArrayList) response.get("selectedCurrencyOptions"); + var currencyOptions = (ArrayList) response.get("currencyOptions"); currencyOptions.addAll(selectedCurrencyOptions); - final String jsonData = new Gson().toJson(new ArrayList(selectedCurrencyOptions)); + var jsonData = new Gson().toJson(selectedCurrencyOptions); return new Gson().fromJson(jsonData, new TypeToken>() {}.getType()); } @@ -57,12 +57,12 @@ public static ArrayList getAllCurrencies(final RequestSpecificat // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) - public static ArrayList getSelectedCurrencies(final RequestSpecification requestSpec, + public static List getSelectedCurrencies(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { - final String GET_ALL_SELECTED_CURRENCIES_URL = CURRENCIES_URL + "?fields=selectedCurrencyOptions" + "&" + Utils.TENANT_IDENTIFIER; LOG.info("------------------------ RETRIEVING ALL SELECTED CURRENCIES -------------------------"); - final HashMap response = Utils.performServerGet(requestSpec, responseSpec, GET_ALL_SELECTED_CURRENCIES_URL, ""); - final String jsonData = new Gson().toJson(response.get("selectedCurrencyOptions")); + HashMap response = Utils.performServerGet(requestSpec, responseSpec, + CURRENCIES_URL + "?fields=selectedCurrencyOptions" + "&" + Utils.TENANT_IDENTIFIER, ""); + var jsonData = new Gson().toJson(response.get("selectedCurrencyOptions")); return new Gson().fromJson(jsonData, new TypeToken>() {}.getType()); } @@ -72,10 +72,10 @@ public static ArrayList getSelectedCurrencies(final RequestSpeci @Deprecated(forRemoval = true) public static CurrencyDomain getCurrencybyCode(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String code) { - ArrayList currenciesList = getAllCurrencies(requestSpec, responseSpec); - for (CurrencyDomain e : currenciesList) { - if (e.getCode().equals(code)) { - return e; + var currencies = getAllCurrencies(requestSpec, responseSpec); + for (var currency : currencies) { + if (currency.getCode().equals(code)) { + return currency; } } return null; @@ -85,22 +85,22 @@ public static CurrencyDomain getCurrencybyCode(final RequestSpecification reques // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) - public static ArrayList updateSelectedCurrencies(final RequestSpecification requestSpec, - final ResponseSpecification responseSpec, final ArrayList currencies) { - final String CURRENCIES_UPDATE_URL = CURRENCIES_URL + "?" + Utils.TENANT_IDENTIFIER; - LOG.info("---------------------------------UPDATE SELECTED CURRENCIES LIST---------------------------------------------"); - HashMap hash = Utils.performServerPut(requestSpec, responseSpec, CURRENCIES_UPDATE_URL, currenciesToJSON(currencies), "changes"); - return (ArrayList) hash.get("currencies"); + public static List updateSelectedCurrencies(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final List currencies) { + LOG.info( + "---------------------------------UPDATE SELECTED CURRENCIES LIST (deprecated)---------------------------------------------"); + // TODO: this nested "changes" map makes no sense whatsover... in the future just use "currencies" (straight + // forward, no nesting, no complexity) + Map changes = Utils.performServerPut(requestSpec, responseSpec, CURRENCIES_URL + "?" + Utils.TENANT_IDENTIFIER, + currenciesToJSON(currencies), "changes"); + return (List) changes.get("currencies"); } // TODO: Rewrite to use fineract-client instead! // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) - private static String currenciesToJSON(final ArrayList currencies) { - HashMap map = new HashMap<>(); - map.put("currencies", currencies); - LOG.info("map : {}", map); - return new Gson().toJson(map); + private static String currenciesToJSON(final List currencies) { + return new Gson().toJson(Map.of("currencies", currencies)); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index e636d63460c..c16780be260 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -544,7 +544,7 @@ private static ArrayList getAllDefaultGlobalConfigurations() { assetOwnerTransferInterestOutstandingStrategy.put("name", GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY); assetOwnerTransferInterestOutstandingStrategy.put("value", 0L); - assetOwnerTransferInterestOutstandingStrategy.put("enabled", false); + assetOwnerTransferInterestOutstandingStrategy.put("enabled", true); assetOwnerTransferInterestOutstandingStrategy.put("trapDoor", false); assetOwnerTransferInterestOutstandingStrategy.put("string_value", "TOTAL_OUTSTANDING_INTEREST"); defaults.add(assetOwnerTransferInterestOutstandingStrategy); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/OfficeHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/OfficeHelper.java index 18aea876b72..8e3ec6efea4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/OfficeHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/OfficeHelper.java @@ -142,7 +142,7 @@ public static String getAsJSON(final String openingDate) { public static String getAsJSON(String externalId, final String openingDate) { final HashMap map = new HashMap<>(); map.put("parentId", "1"); - map.put("name", Utils.uniqueRandomStringGenerator("Office_", 4)); + map.put("name", Utils.uniqueRandomStringGenerator("O_", 9)); map.put("dateFormat", "dd MMMM yyyy"); map.put("locale", "en"); map.put("openingDate", openingDate); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java index 8a325476f58..093f2f0c2bf 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java @@ -272,7 +272,7 @@ public void executeAndAwaitJob(T jobParam, Consumer private void awaitJob(Instant beforeExecuteTime, Supplier>> retrieveLastRunHistory) { final Duration timeout = Duration.ofMinutes(2); - final Duration pause = Duration.ofSeconds(5); + final Duration pause = Duration.ofSeconds(1); DateTimeFormatter df = DateTimeFormatter.ISO_INSTANT; // FINERACT-926 // Await JobDetailData.lastRunHistory [JobDetailHistoryData] // jobRunStartTime >= beforeExecuteTime (or timeout) diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 847bec4ba98..0c0e0b61400 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -166,6 +166,8 @@ public class LoanProductTestBuilder { private List supportedInterestRefundTypes = null; private String chargeOffBehaviour; private boolean interestRecognitionOnDisbursementDate = false; + private Boolean enableBuyDownFee = false; + private String buyDownFeeCalculationType; public String build() { final HashMap map = build(null, null); @@ -341,6 +343,14 @@ public HashMap build(final String chargeId, final Integer delinq map.put("chargeOffBehaviour", chargeOffBehaviour); } + if (this.enableBuyDownFee != null) { + map.put("enableBuyDownFee", this.enableBuyDownFee); + } + + if (this.buyDownFeeCalculationType != null) { + map.put("buyDownFeeCalculationType", this.buyDownFeeCalculationType); + } + return map; } @@ -906,4 +916,13 @@ public Map toMap() { } } + public LoanProductTestBuilder withEnableBuyDownFee(final Boolean enableBuyDownFee) { + this.enableBuyDownFee = enableBuyDownFee; + return this; + } + + public LoanProductTestBuilder withBuyDownFeeCalculationType(final String buyDownFeeCalculationType) { + this.buyDownFeeCalculationType = buyDownFeeCalculationType; + return this; + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index c5f3b43c85b..9b060bdd6c4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -3049,6 +3049,10 @@ public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(Long loanId, Post return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "downPayment")); } + public PostLoansLoanIdTransactionsResponse makeLoanBuyDownFee(Long loanId, PostLoansLoanIdTransactionsRequest request) { + return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "buyDownFee")); + } + public List getAdvancedPaymentAllocationRules(final Integer loanId) { return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue())); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java index 29d5aec6f54..30267956adb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java @@ -109,6 +109,7 @@ baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), @Test public void initialSearchExternalAssetOwnerTransferUsingTextTest() { + saleActiveLoanToExternalAssetOwnerWithSearching(); String textToSearch = UUID.randomUUID().toString(); PagedRequestExternalAssetOwnerSearchRequest searchRequest = EXTERNAL_ASSET_OWNER_HELPER .buildExternalAssetOwnerSearchRequest(textToSearch, "", null, null, 0, 10); @@ -118,10 +119,10 @@ public void initialSearchExternalAssetOwnerTransferUsingTextTest() { // Search over the current Asset Transfers and get just the first five textToSearch = ""; - searchRequest = EXTERNAL_ASSET_OWNER_HELPER.buildExternalAssetOwnerSearchRequest(textToSearch, "", null, null, 0, 5); + searchRequest = EXTERNAL_ASSET_OWNER_HELPER.buildExternalAssetOwnerSearchRequest(textToSearch, "", null, null, 0, 1); response = EXTERNAL_ASSET_OWNER_HELPER.searchExternalAssetOwnerTransfer(searchRequest); assertNotNull(response); - assertEquals(5, response.getContent().size(), "Expecting first five results"); + assertEquals(1, response.getContent().size(), "Expecting first result"); textToSearch = response.getContent().iterator().next().getOwner().getExternalId(); searchRequest = EXTERNAL_ASSET_OWNER_HELPER.buildExternalAssetOwnerSearchRequest(textToSearch, "", null, null, 0, 5); @@ -133,6 +134,7 @@ public void initialSearchExternalAssetOwnerTransferUsingTextTest() { @Test public void initialSearchExternalAssetOwnerTransferUsingEffectiveDateTest() { + saleActiveLoanToExternalAssetOwnerWithSearching(); final String attribute = "effective"; LocalDate fromDate = Utils.getDateAsLocalDate("01 March 2023"); LocalDate toDate = fromDate.plusMonths(3); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/base/BaseSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/base/BaseSavingsIntegrationTest.java index 95873fb26a8..9de4dbbea9c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/base/BaseSavingsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/base/BaseSavingsIntegrationTest.java @@ -119,7 +119,12 @@ protected PostSavingsProductsRequest dailyInterestPostingProduct() { .accountingRule(1) // none .interestCalculationDaysInYearType(DaysInYearType.DAYS_365).interestCompoundingPeriodType(InterestPeriodType.DAILY) .interestCalculationType(InterestCalculationType.AVERAGE_DAILY_BALANCE) // - .interestPostingPeriodType(InterestPeriodType.DAILY);// + .interestPostingPeriodType(InterestPeriodType.DAILY) // + .withdrawalFeeForTransfers(false) // + .enforceMinRequiredBalance(false) // + .allowOverdraft(false) // + .withHoldTax(false) // + .isDormancyTrackingActive(false); // } protected PostSavingsProductsResponse createProduct(PostSavingsProductsRequest productsRequest) { diff --git a/scripts/split-features.sh b/scripts/split-features.sh new file mode 100755 index 00000000000..b793ce18fde --- /dev/null +++ b/scripts/split-features.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# Exit on any error +set -e + +# Get the number of shards (default to 10 if not provided) +NUM_SHARDS=${1:-10} +# Convert to 0-based index for internal calculations +SHARD_INDEX_ZERO_BASED=$((${2:-1} - 1)) +SHARD_INDEX=${2:-1} # Keep original 1-based index for output + +# Directory containing feature files +FEATURES_DIR="fineract-e2e-tests-runner/src/test/resources/features" +TEMP_FILE="/tmp/feature_scenarios_$(date +%s).txt" + +# Check if features directory exists +if [ ! -d "$FEATURES_DIR" ]; then + echo "Error: Features directory not found at $FEATURES_DIR" + exit 1 +fi + +# Function to count scenarios in a feature file +count_scenarios() { + local file="$1" + # Count scenario and scenario outline keywords, excluding commented lines + grep -v '^[[:space:]]*#' "$file" | grep -c 'Scenario\( Outline\)\?:' || echo "0" +} + +# Process each feature file and count scenarios +echo "Analyzing feature files to count scenarios..." +> "$TEMP_FILE" + +while IFS= read -r -d $'\0' file; do + # Remove the 'fineract-e2e-tests-runner/' prefix + rel_path="${file#fineract-e2e-tests-runner/}" + scenario_count=$(count_scenarios "$file") + echo "$scenario_count $rel_path" +done < <(find "$FEATURES_DIR" -type f -name '*.feature' -print0) | sort -nr > "$TEMP_FILE" + +# Read the sorted list of features +SORTED_FEATURES=() +while IFS= read -r line; do + # Extract just the file path (removing the scenario count) + path=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //') + [ -n "$path" ] && SORTED_FEATURES+=("$path") +done < "$TEMP_FILE" +TOTAL_FEATURES=${#SORTED_FEATURES[@]} + +# Check if any feature files were found +if [ $TOTAL_FEATURES -eq 0 ]; then + echo "Warning: No feature files found in $FEATURES_DIR" + # Create an empty feature list file + FEATURE_LIST_FILE="feature_shard_${SHARD_INDEX}.txt" + > "$FEATURE_LIST_FILE" + echo "Created empty feature list file: $FEATURE_LIST_FILE" + rm -f "$TEMP_FILE" + exit 0 +fi + +# Create a file to store the feature file paths +# Use the 1-based index for the filename +FEATURE_LIST_FILE="feature_shard_${SHARD_INDEX}.txt" +> "$FEATURE_LIST_FILE" + +# Distribute features to shards in a round-robin fashion to balance scenario counts +for ((i=0; i> "$FEATURE_LIST_FILE" + fi +done + +# Count scenarios in this shard +SHARD_SCENARIOS=0 +while IFS= read -r line; do + [ -z "$line" ] && continue + count=$(grep -m 1 -F "$line" "$TEMP_FILE" | awk '{print $1}') + SHARD_SCENARIOS=$((SHARD_SCENARIOS + count)) +done < "$FEATURE_LIST_FILE" + +# Get the list of features in this shard for output +SHARD_FEATURES=() +while IFS= read -r line; do + [ -n "$line" ] && SHARD_FEATURES+=("$line") +done < "$FEATURE_LIST_FILE" +NUM_FEATURES=${#SHARD_FEATURES[@]} + +# Output the shard information +echo "Shard $SHARD_INDEX (1-based): $NUM_FEATURES features with $SHARD_SCENARIOS total scenarios" +if [ $NUM_FEATURES -gt 0 ]; then + echo "First feature: ${SHARD_FEATURES[0]}" + if [ $NUM_FEATURES -gt 1 ]; then + echo "Last feature: ${SHARD_FEATURES[$((NUM_FEATURES-1))]}" + fi + echo "Features in this shard (scenario count):" + while IFS= read -r line; do + [ -z "$line" ] && continue + count=$(grep -m 1 -F "$line" "$TEMP_FILE" | awk '{print $1}') + echo " - $line ($count scenarios)" + done < "$FEATURE_LIST_FILE" +fi + +echo "Feature list written to $FEATURE_LIST_FILE" + +# Clean up temp file +rm -f "$TEMP_FILE" + +# Set output for GitHub Actions +if [ -n "$GITHUB_OUTPUT" ]; then + echo "FEATURE_LIST_FILE=$FEATURE_LIST_FILE" >> $GITHUB_OUTPUT +fi diff --git a/scripts/split-tests.sh b/scripts/split-tests.sh new file mode 100755 index 00000000000..c9a84add2b3 --- /dev/null +++ b/scripts/split-tests.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Usage: ./split-tests.sh +set -e + +TOTAL_SHARDS=$1 +SHARD_INDEX=$2 + +if [[ -z "$TOTAL_SHARDS" || -z "$SHARD_INDEX" ]]; then + echo "ERROR: You must provide and ." + exit 1 +fi + +echo "🔍 Searching for eligible JUnit test classes..." + +ALL_TESTS=$(find . -type f -path "*/src/test/java/*Test.java" \ + | while read filepath; do + filename=$(basename "$filepath") + + # Skip abstract class or interface by name + if [[ "$filename" =~ ^Abstract.*Test\.java$ || "$filename" =~ .*AbstractTest\.java$ ]]; then + echo "Skipping abstract-named file: $filename" >&2 + continue + fi + + # Skip if file doesn't contain valid JUnit annotations + if ! grep -qE "@Test|@Nested|@ParameterizedTest" "$filepath"; then + continue + fi + + # Extract module directory path (everything before /src/test/java) + module_path="${filepath%%/src/test/java/*}" + # Convert from ./custom/acme/loan/job to :custom:acme:loan:job + module_name=$(echo "$module_path" | sed 's|^\./||; s|/|:|g; s|^|:|') + + # Extract fully qualified test class name + class_name=$(echo "$filepath" | sed 's|^.*src/test/java/||; s|/|.|g; s|.java$||') + + echo "$module_name,$class_name" + done \ + | sort) + +TOTAL_COUNT=$(echo "$ALL_TESTS" | wc -l) +echo "Found $TOTAL_COUNT eligible test classes." + +SELECTED_CLASSES=$(echo "$ALL_TESTS" \ + | awk -v ts="$TOTAL_SHARDS" -v si="$SHARD_INDEX" 'NR % ts == (si - 1)') + +OUTPUT_FILE="shard-tests_${SHARD_INDEX}.txt" +echo "$SELECTED_CLASSES" > "$OUTPUT_FILE" + +echo "Selected $(wc -l < "$OUTPUT_FILE") classes for shard $SHARD_INDEX of $TOTAL_SHARDS:" +cat "$OUTPUT_FILE" From 45baa403c68a966def4fcd3d4d48fcc4fc32597f Mon Sep 17 00:00:00 2001 From: alburquerquerangelarturo Date: Thu, 3 Jul 2025 20:59:56 -0600 Subject: [PATCH 2/2] Cambios para savings --- .../savings/SavingsApiConstants.java | 1 + .../service/AccountingProcessorHelper.java | 15 +- .../domain/DepositAccountAssembler.java | 6 - .../DepositAccountDomainServiceJpa.java | 44 +++--- .../SavingsAccountDomainServiceJpa.java | 12 +- ...WritePlatformServiceJpaRepositoryImpl.java | 30 ++-- ...SavingsAccountReadPlatformServiceImpl.java | 10 +- .../SavingsAccrualWritePlatformService.java | 9 -- ...avingsAccrualWritePlatformServiceImpl.java | 26 ---- ...WritePlatformServiceJpaRepositoryImpl.java | 12 +- .../savings/starter/SavingsConfiguration.java | 139 +++++++++--------- .../src/main/resources/application.properties | 2 +- .../data/SavingsProductDataValidator.java | 3 +- .../domain/SavingsAccountRepository.java | 4 - .../SavingsAccountRepositoryWrapper.java | 11 -- .../domain/SavingsProductAssembler.java | 95 +++++++----- .../domain/SavingsProductBaseAssembler.java | 86 ----------- .../SavingsSchedularInterestPoster.java | 12 +- 18 files changed, 198 insertions(+), 319 deletions(-) delete mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index d074981bfa8..0f3547201f3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -106,6 +106,7 @@ public class SavingsApiConstants { public static final String activeParamName = "active"; public static final String nameParamName = "name"; public static final String shortNameParamName = "shortName"; + public static final String interestReceivableAccount = "interestReceivableAccountId"; public static final String descriptionParamName = "description"; public static final String currencyCodeParamName = "currencyCode"; public static final String digitsAfterDecimalParamName = "digitsAfterDecimal"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index bccb9694c40..e6be3c6033a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -207,14 +207,13 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting if (map.containsKey("savingsChargesPaid")) { @SuppressWarnings("unchecked") final List> savingsChargesPaidData = (List>) map.get("savingsChargesPaid"); - for (final Map savingsChargesPaid : savingsChargesPaidData) { - final Long chargeId = (Long) savingsChargesPaid.get("chargeId"); - final Long savingsChargeId = (Long) savingsChargesPaid.get("savingsChargeId"); - final boolean isPenalty = (Boolean) savingsChargesPaid.get("isPenalty"); - final boolean accrualRecognized = (Boolean) savingsChargesPaid.get("accrualRecognized"); - - final BigDecimal chargeAmountPaid = (BigDecimal) savingsChargesPaid.get("amount"); - ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, savingsChargeId); + for (final Map loanChargePaid : savingsChargesPaidData) { + final Long chargeId = (Long) loanChargePaid.get("chargeId"); + final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); + final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); + final boolean accrualRecognized = (Boolean) loanChargePaid.get("accrualRecognized"); + final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); + ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); chargePaymentDTO.setAccrualRecognized(accrualRecognized); if (isPenalty) { penaltyPayments.add(chargePaymentDTO); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index efbe31a6f8a..af3b000cfa8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -369,12 +369,6 @@ public SavingsAccount assembleFrom(final Long savingsId, DepositAccountType depo return account; } - public SavingsAccount getClientSavingAccount(final Long clientId, DepositAccountType depositAccountType) { - final SavingsAccount account = this.savingsAccountRepository.findSavingId(clientId, depositAccountType); - account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - return account; - } - public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) { savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 40f3b72a893..7fe875bedd7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -80,12 +80,12 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi @Autowired public DepositAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, - final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountNumberGenerator accountNumberGenerator, - final DepositAccountAssembler depositAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, - final AccountTransfersWritePlatformService accountTransfersWritePlatformService, - final ConfigurationDomainService configurationDomainService, - final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - final CalendarInstanceRepository calendarInstanceRepository) { + final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountNumberGenerator accountNumberGenerator, + final DepositAccountAssembler depositAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService, + final AccountTransfersWritePlatformService accountTransfersWritePlatformService, + final ConfigurationDomainService configurationDomainService, + final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, + final CalendarInstanceRepository calendarInstanceRepository) { this.savingsAccountRepository = savingsAccountRepository; this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.accountNumberGenerator = accountNumberGenerator; @@ -100,8 +100,8 @@ public DepositAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savi @Transactional @Override public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean applyWithdrawFee, final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean applyWithdrawFee, final boolean isRegularTransaction) { boolean isAccountTransfer = false; boolean isInterestTransfer = false; boolean isWithdrawBalance = false; @@ -115,7 +115,7 @@ public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, @Transactional @Override public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) { boolean isAccountTransfer = false; boolean isRegularTransaction = false; final boolean backdatedTxnsAllowedTill = false; @@ -126,8 +126,8 @@ public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount accou @Transactional @Override public SavingsAccountTransaction handleRDDeposit(final RecurringDepositAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isRegularTransaction) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); @@ -159,8 +159,8 @@ public SavingsAccountTransaction handleRDDeposit(final RecurringDepositAccount a @Transactional @Override public SavingsAccountTransaction handleSavingDeposit(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isRegularTransaction) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isRegularTransaction) { boolean isAccountTransfer = false; final boolean backdatedTxnsAllowedTill = false; final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, @@ -185,7 +185,7 @@ private boolean isAnyActivationChargesDue(final RecurringDepositAccount account) @Transactional @Override public Long handleFDAccountClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); @@ -252,8 +252,8 @@ public Long handleFDAccountClosure(final FixedDepositAccount account, final Paym @Transactional @Override public Long handleFDAccountMaturityClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final DateTimeFormatter fmt, final LocalDate closedDate, final Integer onAccountClosureId, final Long toSavingsId, - final String transferDescription, Map changes) { + final DateTimeFormatter fmt, final LocalDate closedDate, final Integer onAccountClosureId, final Long toSavingsId, + final String transferDescription, Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -330,7 +330,7 @@ public Long handleFDAccountMaturityClosure(final FixedDepositAccount account, fi @Transactional @Override public Long handleRDAccountClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -443,7 +443,7 @@ private void autoGenerateAccountNumber(final SavingsAccount account) { @Transactional @Override public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user, - final JsonCommand command, final Map changes) { + final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -498,7 +498,7 @@ public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, f @Transactional @Override public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail, - final AppUser user, final JsonCommand command, final Map changes) { + final AppUser user, final JsonCommand command, final Map changes) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -550,13 +550,13 @@ public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount accoun } private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { + Set existingReversedTransactionIds) { existingTransactionIds.addAll(account.findExistingTransactionIds()); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer) { + final Set existingReversedTransactionIds, boolean isAccountTransfer) { final boolean backdatedTxnsAllowedTill = false; @@ -577,4 +577,4 @@ private void updateAlreadyPostedTransactions(final Set existingTransaction } } } -} \ No newline at end of file +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java index 00211a19cfd..4769a1ea9c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java @@ -24,8 +24,12 @@ import java.math.MathContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.*; - +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; @@ -79,8 +83,8 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi @Transactional @Override public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { context.authenticatedUser(); account.validateForAccountBlock(); account.validateForDebitBlock(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 3f2afc2de50..79b94094a0e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -441,7 +441,7 @@ private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransact @Transactional @Override public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { boolean isRegularTransaction = true; @@ -543,14 +543,14 @@ private void postInterest(final SavingsAccount account) { @Override public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final boolean allowAccountTransferModification) { + @SuppressWarnings("unused") final boolean allowAccountTransferModification) { throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); } @Override public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { + final boolean allowAccountTransferModification) { final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -629,7 +629,7 @@ public CommandProcessingResult undoRDTransaction(final Long savingsId, final Lon @Override public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final JsonCommand command) { + @SuppressWarnings("unused") final JsonCommand command) { throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); } @@ -903,7 +903,7 @@ public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, fin @Override public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); @@ -935,7 +935,7 @@ public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, f @Override public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { + final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -976,7 +976,7 @@ public void rejectSavingsTransfer(final Long accountId, final DepositAccountType @Override public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { + final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1131,7 +1131,7 @@ public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand comm @Transactional @Override public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1188,7 +1188,7 @@ public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Lo @Transactional @Override public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { this.context.authenticatedUser(); final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); @@ -1210,7 +1210,7 @@ public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAcco @Override public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { this.context.authenticatedUser(); @@ -1260,7 +1260,7 @@ public CommandProcessingResult payCharge(final Long savingsAccountId, final Long @Transactional @Override public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); // always use current date as transaction date for batch job @@ -1274,7 +1274,7 @@ public void applyChargeDue(final Long savingsAccountChargeId, final Long account @Transactional private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, - final DateTimeFormatter formatter) { + final DateTimeFormatter formatter) { context.authenticatedUser(); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService @@ -1363,13 +1363,13 @@ public void updateMaturityDetails(Long depositAccountId, DepositAccountType depo } private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { + Set existingReversedTransactionIds) { existingTransactionIds.addAll(account.findExistingTransactionIds()); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { + final Set existingReversedTransactionIds) { boolean isAccountTransfer = false; final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), @@ -1405,4 +1405,4 @@ public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAcc isRegularTransaction); } -} \ No newline at end of file +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 8606522e5aa..59ca9dbf23f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -44,17 +44,12 @@ import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksReadService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.portfolio.account.data.AccountTransferData; -import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.client.data.ClientData; -import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.group.data.GroupGeneralData; -import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.savings.DepositAccountType; @@ -116,8 +111,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, - final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, + final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.context = context; this.jdbcTemplate = jdbcTemplate; this.sqlGenerator = sqlGenerator; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java index 65b6a1e0a16..8a51d0172b4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformService.java @@ -19,22 +19,13 @@ package org.apache.fineract.portfolio.savings.service; import java.time.LocalDate; -import java.util.Collection; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.MultiException; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; public interface SavingsAccrualWritePlatformService { void addAccrualEntries(LocalDate tillDate) throws MultiException; - boolean isChargeToBeRecognizedAsAccrual(Collection chargeIds, SavingsAccountCharge savingsAccountCharge); - - SavingsAccountTransaction addSavingsChargeAccrualTransaction(SavingsAccount savingsAccount, SavingsAccountCharge savingsAccountCharge, - LocalDate transactionDate); - CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws MultiException; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java index e495bb29e65..01190357592 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccrualWritePlatformServiceImpl.java @@ -44,8 +44,6 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargePaidBy; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsHelper; @@ -132,30 +130,6 @@ public CommandProcessingResult addAccrualEntries(Long savingsAccountId) throws J return CommandProcessingResult.empty(); } - @Override - public boolean isChargeToBeRecognizedAsAccrual(final Collection chargeIds, final SavingsAccountCharge savingsAccountCharge) { - if (chargeIds.isEmpty()) { - return false; - } - return chargeIds.contains(savingsAccountCharge.getCharge().getId()); - } - - @Transactional - @Override - public SavingsAccountTransaction addSavingsChargeAccrualTransaction(SavingsAccount savingsAccount, - SavingsAccountCharge savingsAccountCharge, LocalDate transactionDate) { - final MonetaryCurrency currency = savingsAccount.getCurrency(); - final Money chargeAmount = savingsAccountCharge.getAmount(currency); - SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, savingsAccount.office(), - transactionDate, chargeAmount, false, false); - final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(savingsAccountTransaction, savingsAccountCharge, - savingsAccountTransaction.getAmount(currency).getAmount()); - savingsAccountTransaction.getSavingsAccountChargesPaid().add(chargePaidBy); - - savingsAccount.addTransaction(savingsAccountTransaction); - return savingsAccountTransaction; - } - private void addAccrualTransactions(SavingsAccount savingsAccount, final LocalDate fromDate, final LocalDate tillDate, final Integer financialYearBeginningMonth, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final MathContext mc) { final Set existingTransactionIds = new HashSet<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java index d43c5264237..ac6492562cf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -42,6 +43,7 @@ import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType; import org.apache.fineract.infrastructure.entityaccess.service.FineractEntityAccessUtil; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsProductDataValidator; @@ -140,10 +142,12 @@ public CommandProcessingResult update(final Long productId, final JsonCommand co final Map changes = product.update(command); if (changes.containsKey(chargesParamName) || changes.containsKey(accrualChargesParamName)) { - product.setCharges(savingsProductAssembler.assembleListOfSavingsProductCharges(command, product.currency().getCode(), - chargesParamName)); - product.setAccrualCharges(savingsProductAssembler.assembleListOfSavingsProductCharges(command, product.currency().getCode(), - accrualChargesParamName)); + final Set savingsProductCharges = this.savingsProductAssembler.assembleListOfSavingsProductCharges(command, + product.currency().getCode()); + final boolean updated = product.update(savingsProductCharges); + if (!updated) { + changes.remove(chargesParamName); + } } if (changes.containsKey(taxGroupIdParamName)) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 3d553a364ae..0c6b89cc8ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -155,8 +155,8 @@ public class SavingsConfiguration { @Bean @ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class) public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context, - GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, DatatableReadService datatableService, - DataTableValidator dataTableValidator, JdbcTemplate jdbcTemplate, SearchUtil searchUtil) { + GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, DatatableReadService datatableService, + DataTableValidator dataTableValidator, JdbcTemplate jdbcTemplate, SearchUtil searchUtil) { return new SavingsAccountTransactionsSearchServiceImpl(context, genericDataService, sqlGenerator, datatableService, dataTableValidator, jdbcTemplate, searchUtil); } @@ -182,7 +182,7 @@ public DepositAccountInterestRateChartReadPlatformService depositAccountInterest @Bean @ConditionalOnMissingBean(DepositAccountOnHoldTransactionReadPlatformService.class) public DepositAccountOnHoldTransactionReadPlatformService depositAccountOnHoldTransactionReadPlatformService(JdbcTemplate jdbcTemplate, - ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { + ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { return new DepositAccountOnHoldTransactionReadPlatformServiceImpl(jdbcTemplate, sqlGenerator, columnValidator, paginationHelper); } @@ -200,15 +200,15 @@ public DepositAccountPreMatureCalculationPlatformService depositAccountPreMature @Bean @ConditionalOnMissingBean(DepositAccountReadPlatformService.class) public DepositAccountReadPlatformService depositAccountReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - DepositAccountInterestRateChartReadPlatformService chartReadPlatformService, - InterestRateChartReadPlatformService productChartReadPlatformService, - PaginationParametersDataValidator paginationParametersDataValidator, DatabaseSpecificSQLGenerator sqlGenerator, - PaginationHelper paginationHelper, ClientReadPlatformService clientReadPlatformService, - GroupReadPlatformService groupReadPlatformService, DepositProductReadPlatformService depositProductReadPlatformService, - SavingsDropdownReadPlatformService savingsDropdownReadPlatformService, ChargeReadPlatformService chargeReadPlatformService, - StaffReadPlatformService staffReadPlatformService, DepositsDropdownReadPlatformService depositsDropdownReadPlatformService, - SavingsAccountReadPlatformService savingsAccountReadPlatformService, DropdownReadPlatformService dropdownReadPlatformService, - CalendarReadPlatformService calendarReadPlatformService, PaymentTypeReadPlatformService paymentTypeReadPlatformService) { + DepositAccountInterestRateChartReadPlatformService chartReadPlatformService, + InterestRateChartReadPlatformService productChartReadPlatformService, + PaginationParametersDataValidator paginationParametersDataValidator, DatabaseSpecificSQLGenerator sqlGenerator, + PaginationHelper paginationHelper, ClientReadPlatformService clientReadPlatformService, + GroupReadPlatformService groupReadPlatformService, DepositProductReadPlatformService depositProductReadPlatformService, + SavingsDropdownReadPlatformService savingsDropdownReadPlatformService, ChargeReadPlatformService chargeReadPlatformService, + StaffReadPlatformService staffReadPlatformService, DepositsDropdownReadPlatformService depositsDropdownReadPlatformService, + SavingsAccountReadPlatformService savingsAccountReadPlatformService, DropdownReadPlatformService dropdownReadPlatformService, + CalendarReadPlatformService calendarReadPlatformService, PaymentTypeReadPlatformService paymentTypeReadPlatformService) { return new DepositAccountReadPlatformServiceImpl(context, jdbcTemplate, chartReadPlatformService, productChartReadPlatformService, paginationParametersDataValidator, sqlGenerator, paginationHelper, clientReadPlatformService, groupReadPlatformService, depositProductReadPlatformService, savingsDropdownReadPlatformService, chargeReadPlatformService, staffReadPlatformService, @@ -219,21 +219,21 @@ public DepositAccountReadPlatformService depositAccountReadPlatformService(Platf @Bean @ConditionalOnMissingBean(DepositAccountWritePlatformService.class) public DepositAccountWritePlatformService depositAccountWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper, - SavingsAccountTransactionRepository savingsAccountTransactionRepository, DepositAccountAssembler depositAccountAssembler, - DepositAccountTransactionDataValidator depositAccountTransactionDataValidator, - SavingsAccountChargeDataValidator savingsAccountChargeDataValidator, - PaymentDetailWritePlatformService paymentDetailWritePlatformService, - ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, - JournalEntryWritePlatformService journalEntryWritePlatformService, DepositAccountDomainService depositAccountDomainService, - NoteRepository noteRepository, AccountTransfersReadPlatformService accountTransfersReadPlatformService, - ChargeRepositoryWrapper chargeRepository, SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository, - AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, - AccountTransfersWritePlatformService accountTransfersWritePlatformService, - DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, - ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, - WorkingDaysRepositoryWrapper workingDaysRepository, - DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository + SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper, + SavingsAccountTransactionRepository savingsAccountTransactionRepository, DepositAccountAssembler depositAccountAssembler, + DepositAccountTransactionDataValidator depositAccountTransactionDataValidator, + SavingsAccountChargeDataValidator savingsAccountChargeDataValidator, + PaymentDetailWritePlatformService paymentDetailWritePlatformService, + ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, + JournalEntryWritePlatformService journalEntryWritePlatformService, DepositAccountDomainService depositAccountDomainService, + NoteRepository noteRepository, AccountTransfersReadPlatformService accountTransfersReadPlatformService, + ChargeRepositoryWrapper chargeRepository, SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository, + AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, + AccountTransfersWritePlatformService accountTransfersWritePlatformService, + DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, + ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, + WorkingDaysRepositoryWrapper workingDaysRepository, + DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository ) { return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper, @@ -248,16 +248,16 @@ public DepositAccountWritePlatformService depositAccountWritePlatformService(Pla @Bean @ConditionalOnMissingBean(DepositApplicationProcessWritePlatformService.class) public DepositApplicationProcessWritePlatformService depositApplicationProcessWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepository, FixedDepositAccountRepository fixedDepositAccountRepository, - RecurringDepositAccountRepository recurringDepositAccountRepository, DepositAccountAssembler depositAccountAssembler, - DepositAccountDataValidator depositAccountDataValidator, AccountNumberGenerator accountNumberGenerator, - ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, - NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, - SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, - SavingsAccountChargeAssembler savingsAccountChargeAssembler, AccountAssociationsRepository accountAssociationsRepository, - FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository, - ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - BusinessEventNotifierService businessEventNotifierService) { + SavingsAccountRepositoryWrapper savingAccountRepository, FixedDepositAccountRepository fixedDepositAccountRepository, + RecurringDepositAccountRepository recurringDepositAccountRepository, DepositAccountAssembler depositAccountAssembler, + DepositAccountDataValidator depositAccountDataValidator, AccountNumberGenerator accountNumberGenerator, + ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, + NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, + SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, + SavingsAccountChargeAssembler savingsAccountChargeAssembler, AccountAssociationsRepository accountAssociationsRepository, + FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository, + ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, + BusinessEventNotifierService businessEventNotifierService) { return new DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, fixedDepositAccountRepository, recurringDepositAccountRepository, depositAccountAssembler, depositAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository, @@ -269,7 +269,7 @@ public DepositApplicationProcessWritePlatformService depositApplicationProcessWr @Bean @ConditionalOnMissingBean(DepositProductReadPlatformService.class) public DepositProductReadPlatformService depositProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - InterestRateChartReadPlatformService interestRateChartReadPlatformService) { + InterestRateChartReadPlatformService interestRateChartReadPlatformService) { return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService); } @@ -282,9 +282,9 @@ public DepositsDropdownReadPlatformService depositsDropdownReadPlatformService() @Bean @ConditionalOnMissingBean(FixedDepositProductWritePlatformService.class) public FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService(PlatformSecurityContext context, - FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, - DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, + DepositProductAssembler depositProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository, fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); } @@ -299,16 +299,16 @@ public GroupSavingsIndividualMonitoringWritePlatformService groupSavingsIndividu @Bean @ConditionalOnMissingBean(GSIMReadPlatformService.class) public GSIMReadPlatformService gsimReadPlatformService(JdbcTemplate jdbcTemplate, PlatformSecurityContext context, - ColumnValidator columnValidator) { + ColumnValidator columnValidator) { return new GSIMReadPlatformServiceImpl(jdbcTemplate, context, columnValidator); } @Bean @ConditionalOnMissingBean(RecurringDepositProductWritePlatformService.class) public RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService(PlatformSecurityContext context, - RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, - DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, + DepositProductAssembler depositProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository, fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); } @@ -323,8 +323,8 @@ public SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplica @Bean @ConditionalOnMissingBean(SavingsAccountChargeReadPlatformService.class) public SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService(PlatformSecurityContext context, - ChargeDropdownReadPlatformService chargeDropdownReadPlatformService, JdbcTemplate jdbcTemplate, - DropdownReadPlatformService dropdownReadPlatformService, DatabaseSpecificSQLGenerator sqlGenerator) { + ChargeDropdownReadPlatformService chargeDropdownReadPlatformService, JdbcTemplate jdbcTemplate, + DropdownReadPlatformService dropdownReadPlatformService, DatabaseSpecificSQLGenerator sqlGenerator) { return new SavingsAccountChargeReadPlatformServiceImpl(context, chargeDropdownReadPlatformService, jdbcTemplate, dropdownReadPlatformService, sqlGenerator); } @@ -338,8 +338,9 @@ public SavingsAccountInterestPostingService savingsAccountInterestPostingService @Bean @ConditionalOnMissingBean(SavingsAccountReadPlatformService.class) public SavingsAccountReadPlatformService savingsAccountReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, - SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, + SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, ColumnValidator columnValidator, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { return new SavingsAccountReadPlatformServiceImpl(context, jdbcTemplate, savingAccountAssembler, paginationHelper, columnValidator, sqlGenerator, savingsAccountRepositoryWrapper, namedParameterJdbcTemplate); } @@ -347,11 +348,11 @@ public SavingsAccountReadPlatformService savingsAccountReadPlatformService(Platf @Bean @ConditionalOnMissingBean(SavingsAccountTemplateReadPlatformService.class) public SavingsAccountTemplateReadPlatformService savingsAccountTemplateReadPlatformService(PlatformSecurityContext context, - JdbcTemplate jdbcTemplate, ClientReadPlatformService clientReadPlatformService, - GroupReadPlatformService groupReadPlatformService, SavingsProductReadPlatformService savingProductReadPlatformService, - StaffReadPlatformService staffReadPlatformService, SavingsDropdownReadPlatformService dropdownReadPlatformService, - ChargeReadPlatformService chargeReadPlatformService, EntityDatatableChecksReadService entityDatatableChecksReadService, - ColumnValidator columnValidator) { + JdbcTemplate jdbcTemplate, ClientReadPlatformService clientReadPlatformService, + GroupReadPlatformService groupReadPlatformService, SavingsProductReadPlatformService savingProductReadPlatformService, + StaffReadPlatformService staffReadPlatformService, SavingsDropdownReadPlatformService dropdownReadPlatformService, + ChargeReadPlatformService chargeReadPlatformService, EntityDatatableChecksReadService entityDatatableChecksReadService, + ColumnValidator columnValidator) { return new SavingsAccountTemplateReadPlatformServiceImpl(context, jdbcTemplate, clientReadPlatformService, groupReadPlatformService, savingProductReadPlatformService, staffReadPlatformService, dropdownReadPlatformService, chargeReadPlatformService, entityDatatableChecksReadService, columnValidator); @@ -388,16 +389,16 @@ public SavingsAccountWritePlatformService savingsAccountWritePlatformService(Pla @Bean @ConditionalOnMissingBean(SavingsApplicationProcessWritePlatformService.class) public SavingsApplicationProcessWritePlatformService savingsApplicationProcessWritePlatformService(PlatformSecurityContext context, - SavingsAccountRepositoryWrapper savingAccountRepository, SavingsAccountAssembler savingAccountAssembler, - SavingsAccountDataValidator savingsAccountDataValidator, AccountNumberGenerator accountNumberGenerator, - ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, - NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, - SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, - SavingsAccountChargeAssembler savingsAccountChargeAssembler, CommandProcessingService commandProcessingService, - SavingsAccountDomainService savingsAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, - AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService, - EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository, - GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService) { + SavingsAccountRepositoryWrapper savingAccountRepository, SavingsAccountAssembler savingAccountAssembler, + SavingsAccountDataValidator savingsAccountDataValidator, AccountNumberGenerator accountNumberGenerator, + ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, SavingsProductRepository savingsProductRepository, + NoteRepository noteRepository, StaffRepositoryWrapper staffRepository, + SavingsAccountApplicationTransitionApiJsonValidator savingsAccountApplicationTransitionApiJsonValidator, + SavingsAccountChargeAssembler savingsAccountChargeAssembler, CommandProcessingService commandProcessingService, + SavingsAccountDomainService savingsAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, + AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService, + EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository, + GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService) { return new SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, savingAccountAssembler, savingsAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository, savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler, @@ -415,17 +416,17 @@ public SavingsDropdownReadPlatformService savingsDropdownReadPlatformService() { @Bean @ConditionalOnMissingBean(SavingsProductReadPlatformService.class) public SavingsProductReadPlatformService savingsProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - FineractEntityAccessUtil fineractEntityAccessUtil) { + FineractEntityAccessUtil fineractEntityAccessUtil) { return new SavingsProductReadPlatformServiceImpl(context, jdbcTemplate, fineractEntityAccessUtil); } @Bean @ConditionalOnMissingBean(SavingsProductWritePlatformService.class) public SavingsProductWritePlatformService savingsProductWritePlatformService(PlatformSecurityContext context, - SavingsProductRepository savingProductRepository, SavingsProductDataValidator fromApiJsonDataValidator, - SavingsProductAssembler savingsProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, - FineractEntityAccessUtil fineractEntityAccessUtil) { + SavingsProductRepository savingProductRepository, SavingsProductDataValidator fromApiJsonDataValidator, + SavingsProductAssembler savingsProductAssembler, + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, + FineractEntityAccessUtil fineractEntityAccessUtil) { return new SavingsProductWritePlatformServiceJpaRepositoryImpl(context, savingProductRepository, fromApiJsonDataValidator, savingsProductAssembler, accountMappingWritePlatformService, fineractEntityAccessUtil); } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 99cadca29fc..33642742e4d 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -468,4 +468,4 @@ resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POS fineract.command.enabled=true fineract.command.executor=sync fineract.command.ring-buffer-size=1024 -fineract.command.producer-type=single \ No newline at end of file +fineract.command.producer-type=single diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java index 442540e72e3..a8b7cdf1bd2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java @@ -33,6 +33,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestReceivableAccount; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; @@ -89,7 +90,7 @@ public class SavingsProductDataValidator { private final SavingsProductAccountingDataValidator savingsProductAccountingDataValidator; private static final Set SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( SavingsApiConstants.localeParamName, SavingsApiConstants.monthDayFormatParamName, nameParamName, shortNameParamName, - "interestReceivableAccountId", descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, + interestReceivableAccount, descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, SavingsApiConstants.withdrawalFeeAmountParamName, diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java index aa96dd51814..aaece884cdb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java @@ -35,10 +35,6 @@ public interface SavingsAccountRepository extends JpaRepository findSavingAccountByClientId(@Param("clientId") Long clientId); - @Query("select s_acc from SavingsAccount s_acc where s_acc.client.id = :clientId and s_acc.depositType = :depositAccountTypeId") - SavingsAccount findSavingAccountByClientIdAndDepositAccountType(@Param("clientId") Long clientId, - @Param("depositAccountTypeId") Integer depositAccountTypeId); - @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select sa from SavingsAccount sa where sa.id = :savingsId") SavingsAccount findOneLocked(@Param("savingsId") Long id); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index 461beb135e2..3a406335698 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -90,17 +90,6 @@ public SavingsAccount findOneWithNotFoundDetection(final Long savingsId, final D return account; } - @Transactional(readOnly = true) - public SavingsAccount findSavingId(final Long clientId, final DepositAccountType depositAccountType) { - final SavingsAccount account = this.repository.findSavingAccountByClientIdAndDepositAccountType(clientId, - depositAccountType.getValue()); - if (account == null) { - throw new SavingsAccountNotFoundException(clientId); - } - account.loadLazyCollections(); - return account; - } - @Transactional(readOnly = true) public List findSavingAccountByClientId(@Param("clientId") Long clientId) { List accounts = this.repository.findSavingAccountByClientId(clientId); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java index b8660e4cfb5..55db0d5d731 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java @@ -18,45 +18,20 @@ */ package org.apache.fineract.portfolio.savings.domain; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accrualChargesParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToDormancyParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToEscheatParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.daysToInactiveParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.digitsAfterDecimalParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.inMultiplesOfParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationDaysInYearTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.maxAllowedLienLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minBalanceForInterestCalculationParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minOverdraftForInterestCalculationParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequiredOpeningBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import java.math.BigDecimal; +import java.util.HashSet; import java.util.Set; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; @@ -68,11 +43,15 @@ import org.springframework.stereotype.Component; @Component -public class SavingsProductAssembler extends SavingsProductBaseAssembler { +public class SavingsProductAssembler { + + private final ChargeRepositoryWrapper chargeRepository; + private final TaxGroupRepositoryWrapper taxGroupRepository; @Autowired - public SavingsProductAssembler(ChargeRepositoryWrapper chargeRepository, TaxGroupRepositoryWrapper taxGroupRepository) { - super(chargeRepository, taxGroupRepository); + public SavingsProductAssembler(final ChargeRepositoryWrapper chargeRepository, final TaxGroupRepositoryWrapper taxGroupRepository) { + this.chargeRepository = chargeRepository; + this.taxGroupRepository = taxGroupRepository; } public SavingsProduct assemble(final JsonCommand command) { @@ -131,10 +110,8 @@ public SavingsProduct assemble(final JsonCommand command) { final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule")); // Savings product charges - final Set charges = assembleListOfSavingsProductCharges(command, currencyCode, chargesParamName); - - // Savings product charges to be accrued - final Set accrualCharges = assembleListOfSavingsProductCharges(command, currencyCode, accrualChargesParamName); + final Set charges = assembleListOfSavingsProductCharges(command, currencyCode); + final Set accrualCharges = assembleListOfSavingsProductCharges(command, currencyCode); boolean allowOverdraft = false; if (command.parameterExists(allowOverdraftParamName)) { @@ -194,4 +171,46 @@ public SavingsProduct assemble(final JsonCommand command) { taxGroup, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, accrualCharges); } + public Set assembleListOfSavingsProductCharges(final JsonCommand command, final String savingsProductCurrencyCode) { + + final Set charges = new HashSet<>(); + + if (command.parameterExists(chargesParamName)) { + final JsonArray chargesArray = command.arrayOfParameterNamed(chargesParamName); + if (chargesArray != null) { + for (int i = 0; i < chargesArray.size(); i++) { + + final JsonObject jsonObject = chargesArray.get(i).getAsJsonObject(); + if (jsonObject.has(idParamName)) { + final Long id = jsonObject.get(idParamName).getAsLong(); + + final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(id); + + if (!charge.isSavingsCharge()) { + final String errorMessage = "Charge with identifier " + charge.getId() + + " cannot be applied to Savings product."; + throw new ChargeCannotBeAppliedToException("savings.product", errorMessage, charge.getId()); + } + + if (!savingsProductCurrencyCode.equals(charge.getCurrencyCode())) { + final String errorMessage = "Charge and Savings Product must have the same currency."; + throw new InvalidCurrencyException("charge", "attach.to.savings.product", errorMessage); + } + charges.add(charge); + } + } + } + } + + return charges; + } + + public TaxGroup assembleTaxGroup(final JsonCommand command) { + final Long taxGroupId = command.longValueOfParameterNamed(taxGroupIdParamName); + TaxGroup taxGroup = null; + if (taxGroupId != null) { + taxGroup = this.taxGroupRepository.findOneWithNotFoundDetection(taxGroupId); + } + return taxGroup; + } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java deleted file mode 100644 index 69e83df2cfe..00000000000 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductBaseAssembler.java +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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.savings.domain; - -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.idParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import java.util.HashSet; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; -import org.apache.fineract.portfolio.tax.domain.TaxGroup; -import org.apache.fineract.portfolio.tax.domain.TaxGroupRepositoryWrapper; - -@RequiredArgsConstructor -public class SavingsProductBaseAssembler { - - protected final ChargeRepositoryWrapper chargeRepository; - protected final TaxGroupRepositoryWrapper taxGroupRepository; - - public Set assembleListOfSavingsProductCharges(final JsonCommand command, final String savingsProductCurrencyCode, - final String chargesParameterName) { - - final Set charges = new HashSet<>(); - - if (command.parameterExists(chargesParameterName)) { - final JsonArray chargesArray = command.arrayOfParameterNamed(chargesParameterName); - if (chargesArray != null) { - for (int i = 0; i < chargesArray.size(); i++) { - - final JsonObject jsonObject = chargesArray.get(i).getAsJsonObject(); - if (jsonObject.has(idParamName)) { - final Long id = jsonObject.get(idParamName).getAsLong(); - - final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(id); - - if (!charge.isSavingsCharge()) { - final String errorMessage = "Charge with identifier " + charge.getId() - + " cannot be applied to Savings product."; - throw new ChargeCannotBeAppliedToException("savings.product", errorMessage, charge.getId()); - } - - if (!savingsProductCurrencyCode.equals(charge.getCurrencyCode())) { - final String errorMessage = "Charge and Savings Product must have the same currency."; - throw new InvalidCurrencyException("charge", "attach.to.savings.product", errorMessage); - } - charges.add(charge); - } - } - } - } - - return charges; - } - - public TaxGroup assembleTaxGroup(final JsonCommand command) { - final Long taxGroupId = command.longValueOfParameterNamed(taxGroupIdParamName); - TaxGroup taxGroup = null; - if (taxGroupId != null) { - taxGroup = this.taxGroupRepository.findOneWithNotFoundDetection(taxGroupId); - } - return taxGroup; - } -} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index a2f4239b4f1..fe5a35417f2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -26,15 +26,11 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; +import java.util.*; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; -import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; @@ -196,9 +192,9 @@ private void batchUpdate(final List savingsAccountDataList) List savingsAccountTransactionDataList = savingsAccountData.getSavingsAccountTransactionData(); for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { if (savingsAccountTransactionData.getId() == null) { - final ExternalId externalId = ExternalId.generate(); - savingsAccountTransactionData.setRefNo(externalId.toString()); - transRefNo.add(externalId.toString()); + UUID uuid = UUID.randomUUID(); + savingsAccountTransactionData.setRefNo(uuid.toString()); + transRefNo.add(uuid.toString()); paramsForTransactionInsertion.add(new Object[] { savingsAccountData.getId(), savingsAccountData.getOfficeId(), savingsAccountTransactionData.isReversed(), savingsAccountTransactionData.getTransactionType().getId(), savingsAccountTransactionData.getTransactionDate(), savingsAccountTransactionData.getAmount(),