From 0dc86ec824019e481ef3eb175020729922b95130 Mon Sep 17 00:00:00 2001 From: Marta Jankovics Date: Mon, 11 Nov 2024 13:42:58 +0100 Subject: [PATCH] FINERACT-2060: Accrual reverse replay logic and Handling --- .../service/ChargeReadPlatformService.java | 3 +- .../core/service/DateUtils.java | 53 +- .../infrastructure/core/service/MathUtil.java | 19 + .../test/helper/WorkFlowJobHelper.java | 45 +- .../BaseFineractInitializerConfiguration.java | 6 +- .../initializer/base/FineractInitializer.java | 7 - .../CobBusinessStepInitializerStep.java | 30 +- .../stepdef/common/BusinessStepStepDef.java | 178 --- .../fineract/test/support/TestContextKey.java | 1 - .../src/test/resources/features/0_COB.feature | 2 - .../features/AssetExternalization.feature | 96 -- .../resources/features/EMICalculation.feature | 380 +++-- .../src/test/resources/features/Loan.feature | 6 - .../resources/features/LoanRepayment.feature | 2 - ...ualAdjustmentTransactionBusinessEvent.java | 35 + .../loanaccount/data/AccrualChargeData.java | 38 + .../loanaccount/data/AccrualPeriodData.java | 86 + .../loanaccount/data/AccrualPeriodsData.java | 64 + .../loanaccount/data/LoanChargeData.java | 18 +- .../data/LoanTransactionEnumData.java | 2 + .../portfolio/loanaccount/domain/Loan.java | 224 +-- .../loanaccount/domain/LoanCharge.java | 70 +- .../LoanInterestRecalculationDetails.java | 2 +- .../LoanRepaymentScheduleInstallment.java | 34 +- ...oanRepaymentScheduleProcessingWrapper.java | 9 + .../loanaccount/domain/LoanRepository.java | 20 + .../domain/LoanRepositoryWrapper.java | 8 + .../loanaccount/domain/LoanTransaction.java | 160 +- ...TransactionToRepaymentScheduleMapping.java | 28 +- .../domain/LoanTransactionType.java | 5 + ...rgeRepaymentScheduleProcessingWrapper.java | 25 +- ...RepaymentScheduleTransactionProcessor.java | 4 +- ...stractCumulativeLoanScheduleGenerator.java | 36 + .../domain/LoanScheduleGenerator.java | 5 + .../LoanAccrualsProcessingService.java | 23 +- .../loanaccount/service/LoanAssembler.java | 50 + .../LoanChargeReadPlatformService.java | 5 +- .../loanproduct/service/LoanEnumerations.java | 2 + ...edPaymentScheduleTransactionProcessor.java | 385 +++-- .../loanschedule/data/InterestPeriod.java | 18 +- .../ProgressiveLoanInterestScheduleModel.java | 22 +- .../loanschedule/data/RepaymentPeriod.java | 58 +- .../ProgressiveLoanScheduleGenerator.java | 102 +- .../loanproduct/calc/EMICalculator.java | 20 +- .../calc/ProgressiveEMICalculator.java | 124 +- ...ymentScheduleTransactionProcessorTest.java | 8 +- .../calc/ProgressiveEMICalculatorTest.java | 109 +- ...ccrualBasedAccountingProcessorForLoan.java | 56 +- .../ChargeReadPlatformServiceImpl.java | 2 +- .../api/LoanChargesApiResource.java | 5 +- .../domain/LoanAccountDomainServiceJpa.java | 10 +- .../AddAccrualEntriesTasklet.java | 40 +- ...heduleRequestWritePlatformServiceImpl.java | 2 +- .../LoanAccrualsProcessingServiceImpl.java | 1424 ++++++----------- ...nAssembler.java => LoanAssemblerImpl.java} | 12 +- .../LoanChargeReadPlatformServiceImpl.java | 242 +-- .../LoanChargeWritePlatformServiceImpl.java | 4 +- .../service/LoanReadPlatformService.java | 12 - .../service/LoanReadPlatformServiceImpl.java | 354 ---- .../LoanStatusChangePlatformServiceImpl.java | 1 + ...WritePlatformServiceJpaRepositoryImpl.java | 30 +- ...gressiveLoanInterestRefundServiceImpl.java | 5 +- .../reaging/LoanReAgingServiceImpl.java | 13 +- .../LoanReAmortizationServiceImpl.java | 12 +- .../starter/LoanAccountAutoStarter.java | 5 +- .../starter/LoanAccountConfiguration.java | 3 +- ...ansaction_external_event_configuration.xml | 6 + ...entConfigurationValidationServiceTest.java | 4 +- .../loanaccount/domain/LoanTest.java | 13 +- .../ClientLoanIntegrationTest.java | 3 +- .../LoanInterestRefundTest.java | 30 +- .../ExternalEventConfigurationHelper.java | 6 +- 72 files changed, 2007 insertions(+), 2914 deletions(-) delete mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepStepDef.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodsData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/{LoanAssembler.java => LoanAssemblerImpl.java} (99%) 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 fbec2a84fc3..5030f2912cc 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 @@ -19,6 +19,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; @@ -53,7 +54,7 @@ public interface ChargeReadPlatformService { * Excludes Given List of Charge Types from the response * @return */ - Collection retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes); + List retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes); /** * Returns all charges applicable for a given loan product (filter based on Currency of Selected Loan Product) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index c7cc68239f0..b40fd872320 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -193,11 +193,23 @@ public static int compare(OffsetDateTime first, OffsetDateTime second) { } public static int compare(OffsetDateTime first, OffsetDateTime second, ChronoUnit truncate) { + return compare(first, second, truncate, true); + } + + public static int compareWithNullsLast(OffsetDateTime first, OffsetDateTime second) { + return compare(first, second, null, false); + } + + public static int compareWithNullsLast(@NotNull Optional first, @NotNull Optional second) { + return compareWithNullsLast(first.orElse(null), second.orElse(null)); + } + + public static int compare(OffsetDateTime first, OffsetDateTime second, ChronoUnit truncate, boolean nullFirst) { if (first == null) { - return second == null ? 0 : -1; + return second == null ? 0 : (nullFirst ? -1 : 1); } if (second == null) { - return 1; + return nullFirst ? 1 : -1; } first = first.withOffsetSameInstant(ZoneOffset.UTC); second = second.withOffsetSameInstant(ZoneOffset.UTC); @@ -291,7 +303,23 @@ public static boolean isDateInTheFuture(final LocalDate localDate) { } public static int compare(LocalDate first, LocalDate second) { - return first == null ? (second == null ? 0 : -1) : (second == null ? 1 : first.compareTo(second)); + return compare(first, second, true); + } + + /** + * Comparing dates. Null will be considered as last elements + * + * @param first + * @param second + * @return + */ + public static int compareWithNullsLast(LocalDate first, LocalDate second) { + return compare(first, second, false); + } + + public static int compare(LocalDate first, LocalDate second, boolean nullFirst) { + return first == null ? (second == null ? 0 : (nullFirst ? -1 : 1)) + : (second == null ? (nullFirst ? 1 : -1) : first.compareTo(second)); } public static boolean isEqual(LocalDate first, LocalDate second) { @@ -426,23 +454,4 @@ private static DateTimeFormatter getDateTimeFormatter(String format, Locale loca } return formatter; } - - /** - * Comparing dates. Null will be considered as last elements - * - * @param first - * @param second - * @return - */ - public static int compareWithNullsLast(LocalDate first, LocalDate second) { - return first == null ? (second == null ? 0 : 1) : (second == null ? -1 : first.compareTo(second)); - } - - public static int compareWithNullsLast(@NotNull Optional first, @NotNull Optional second) { - return DateUtils.compareWithNullsLast(first.orElse(null), second.orElse(null)); - } - - public static int compareWithNullsLast(OffsetDateTime first, OffsetDateTime second) { - return first == null ? (second == null ? 0 : 1) : (second == null ? -1 : first.compareTo(second)); - } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index 4e2178d9a99..69fde3c303f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -291,6 +291,11 @@ public static BigDecimal subtract(BigDecimal first, BigDecimal second) { return subtract(first, second, MoneyHelper.getMathContext()); } + /** @return first minus the others considering null values, maybe negative */ + public static BigDecimal subtract(BigDecimal first, BigDecimal second, BigDecimal third) { + return subtract(subtract(first, second), third); + } + /** @return first minus second considering null values, maybe negative */ public static BigDecimal subtract(BigDecimal first, BigDecimal second, MathContext mc) { return first == null ? null : second == null ? first : first.subtract(second, mc); @@ -336,6 +341,10 @@ public static String formatToSql(BigDecimal amount) { return amount == null ? null : amount.toPlainString(); } + public static Money toMoney(BigDecimal amount, @NotNull MonetaryCurrency currency) { + return amount == null ? null : Money.of(currency, amount); + } + // ----------------- Money ----------------- public static BigDecimal toBigDecimal(Money value) { @@ -454,6 +463,16 @@ public static Money min(Money first, Money second, Money third, boolean notNull) return min(min(first, second, notNull), third, notNull); } + /** @return Money null safe negate */ + public static Money negate(Money amount) { + return negate(amount, MoneyHelper.getMathContext()); + } + + /** @return Money null safe negate */ + public static Money negate(Money amount, MathContext mc) { + return isEmpty(amount) ? amount : amount.negated(mc); + } + /** * Calculate percentage of a value * diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java index c77b2e92177..701e574e38e 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java @@ -21,29 +21,58 @@ import java.io.IOException; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.BusinessStep; import org.apache.fineract.client.models.GetBusinessStepConfigResponse; +import org.apache.fineract.client.models.UpdateBusinessStepConfigRequest; import org.apache.fineract.client.services.BusinessStepConfigurationApi; -import org.apache.fineract.test.support.TestContext; -import org.apache.fineract.test.support.TestContextKey; import org.springframework.stereotype.Component; import retrofit2.Response; @RequiredArgsConstructor @Component +@Slf4j public class WorkFlowJobHelper { private static final String WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS = "LOAN_CLOSE_OF_BUSINESS"; private final BusinessStepConfigurationApi businessStepConfigurationApi; - public void saveOriginalCOBWorkflowJobBusinessStepList() throws IOException { - Response businessStepConfigResponse = businessStepConfigurationApi + public void setWorkflowJobs() throws IOException { + List businessSteps = List.of(// + new BusinessStep().stepName("APPLY_CHARGE_TO_OVERDUE_LOANS").order(1L), // + new BusinessStep().stepName("LOAN_DELINQUENCY_CLASSIFICATION").order(2L), // + new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(3L), // + new BusinessStep().stepName("ADD_PERIODIC_ACCRUAL_ENTRIES").order(4L), // + new BusinessStep().stepName("ACCRUAL_ACTIVITY_POSTING").order(5L), // + new BusinessStep().stepName("CHECK_LOAN_REPAYMENT_DUE").order(6L), // + new BusinessStep().stepName("CHECK_LOAN_REPAYMENT_OVERDUE").order(7L), // + new BusinessStep().stepName("CHECK_DUE_INSTALLMENTS").order(8L), // + new BusinessStep().stepName("UPDATE_LOAN_ARREARS_AGING").order(9L), // + new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(10L)// + ); + UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); + Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) + .execute(); + ErrorHelper.checkSuccessfulApiCall(response); + // --- log changes --- + logChanges(); + } + + private void logChanges() throws IOException { + // --- log changes --- + Response changesResponse = businessStepConfigurationApi .retrieveAllConfiguredBusinessStep(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS).execute(); - ErrorHelper.checkSuccessfulApiCall(businessStepConfigResponse); - List businessSteps = businessStepConfigResponse.body().getBusinessSteps(); - businessSteps.sort(Comparator.comparingLong(BusinessStep::getOrder)); - TestContext.GLOBAL.set(TestContextKey.ORIGINAL_COB_WORKFLOW_JOB_BUSINESS_STEP_LIST, businessSteps); + List businessStepsChanged = changesResponse.body().getBusinessSteps(); + List changes = businessStepsChanged// + .stream()// + .sorted(Comparator.comparingLong(BusinessStep::getOrder))// + .map(BusinessStep::getStepName)// + .collect(Collectors.toList());// + + log.debug("Business steps has been CHANGED to the following:"); + changes.forEach(e -> log.debug(e)); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java index afb74e3ecb9..f1a624e9705 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/BaseFineractInitializerConfiguration.java @@ -21,7 +21,6 @@ import java.util.List; import org.apache.fineract.test.config.CacheConfiguration; import org.apache.fineract.test.helper.BusinessDateHelper; -import org.apache.fineract.test.helper.WorkFlowJobHelper; import org.apache.fineract.test.initializer.global.FineractGlobalInitializerStep; import org.apache.fineract.test.initializer.scenario.FineractScenarioInitializerStep; import org.apache.fineract.test.initializer.suite.FineractSuiteInitializerStep; @@ -40,8 +39,7 @@ public class BaseFineractInitializerConfiguration { @Bean public FineractInitializer fineractInitializer(List globalInitializerSteps, List suiteInitializerSteps, List scenarioInitializerSteps, - BusinessDateHelper businessDateHelper, WorkFlowJobHelper workFlowJobHelper) { - return new FineractInitializer(globalInitializerSteps, suiteInitializerSteps, scenarioInitializerSteps, businessDateHelper, - workFlowJobHelper); + BusinessDateHelper businessDateHelper) { + return new FineractInitializer(globalInitializerSteps, suiteInitializerSteps, scenarioInitializerSteps, businessDateHelper); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/FineractInitializer.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/FineractInitializer.java index a67ce2125bf..d08129406f3 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/FineractInitializer.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/base/FineractInitializer.java @@ -23,7 +23,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.test.helper.BusinessDateHelper; -import org.apache.fineract.test.helper.WorkFlowJobHelper; import org.apache.fineract.test.initializer.global.FineractGlobalInitializerStep; import org.apache.fineract.test.initializer.scenario.FineractScenarioInitializerStep; import org.apache.fineract.test.initializer.suite.FineractSuiteInitializerStep; @@ -39,7 +38,6 @@ public class FineractInitializer implements InitializingBean { private final List suiteInitializerSteps; private final List scenarioInitializerSteps; private final BusinessDateHelper businessDateHelper; - private final WorkFlowJobHelper workFlowJobHelper; @Override public void afterPropertiesSet() throws Exception { @@ -63,7 +61,6 @@ public void setupGlobalDefaults() throws Exception { for (FineractGlobalInitializerStep initializerStep : globalInitializerSteps) { initializerStep.initialize(); } - businessDateHelper.setBusinessDateToday(); } @@ -71,16 +68,13 @@ public void setupDefaultsForSuite() throws Exception { for (FineractSuiteInitializerStep initializerStep : suiteInitializerSteps) { initializerStep.initializeForSuite(); } - businessDateHelper.setBusinessDateToday(); - workFlowJobHelper.saveOriginalCOBWorkflowJobBusinessStepList(); } public void setupDefaultsForScenario() throws Exception { for (FineractScenarioInitializerStep scenarioInitializerStep : scenarioInitializerSteps) { scenarioInitializerStep.initializeForScenario(); } - businessDateHelper.setBusinessDateToday(); } @@ -88,7 +82,6 @@ public void resetDefaultsAfterSuite() throws Exception { for (FineractSuiteInitializerStep initializerStep : suiteInitializerSteps) { initializerStep.resetAfterSuite(); } - businessDateHelper.setBusinessDateToday(); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CobBusinessStepInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CobBusinessStepInitializerStep.java index 4167d36cc2c..b5f08f40ebd 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CobBusinessStepInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CobBusinessStepInitializerStep.java @@ -18,42 +18,18 @@ */ package org.apache.fineract.test.initializer.global; -import java.util.Comparator; -import java.util.List; import lombok.RequiredArgsConstructor; -import org.apache.fineract.client.models.BusinessStep; -import org.apache.fineract.client.models.GetBusinessStepConfigResponse; -import org.apache.fineract.client.models.UpdateBusinessStepConfigRequest; -import org.apache.fineract.client.services.BusinessStepConfigurationApi; -import org.apache.fineract.test.helper.ErrorHelper; +import org.apache.fineract.test.helper.WorkFlowJobHelper; import org.springframework.stereotype.Component; -import retrofit2.Response; @RequiredArgsConstructor @Component public class CobBusinessStepInitializerStep implements FineractGlobalInitializerStep { - private static final String WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS = "LOAN_CLOSE_OF_BUSINESS"; - private static final String BUSINESS_STEP_NAME_ACCRUAL_ACTIVITY_POSTING = "ACCRUAL_ACTIVITY_POSTING"; - private final BusinessStepConfigurationApi businessStepConfigurationApi; + private final WorkFlowJobHelper workFlowJobHelper; @Override public void initialize() throws Exception { - // --- Adding ACCRUAL_ACTIVITY_POSTING to default COB steps --- - Response businessStepConfigResponse = businessStepConfigurationApi - .retrieveAllConfiguredBusinessStep(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS).execute(); - ErrorHelper.checkSuccessfulApiCall(businessStepConfigResponse); - List businessSteps = businessStepConfigResponse.body().getBusinessSteps(); - businessSteps.sort(Comparator.comparingLong(BusinessStep::getOrder)); - Long lastOrder = businessSteps.get(businessSteps.size() - 1).getOrder(); - - BusinessStep accrualActivityPosting = new BusinessStep().stepName(BUSINESS_STEP_NAME_ACCRUAL_ACTIVITY_POSTING).order(lastOrder + 1); - businessSteps.add(accrualActivityPosting); - - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); + workFlowJobHelper.setWorkflowJobs(); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepStepDef.java deleted file mode 100644 index 554999713c1..00000000000 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/BusinessStepStepDef.java +++ /dev/null @@ -1,178 +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.test.stepdef.common; - -import io.cucumber.java.en.Given; -import io.cucumber.java.en.Then; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.client.models.BusinessStep; -import org.apache.fineract.client.models.GetBusinessStepConfigResponse; -import org.apache.fineract.client.models.UpdateBusinessStepConfigRequest; -import org.apache.fineract.client.services.BusinessStepConfigurationApi; -import org.apache.fineract.test.data.CobBusinessStep; -import org.apache.fineract.test.helper.ErrorHelper; -import org.apache.fineract.test.stepdef.AbstractStepDef; -import org.apache.fineract.test.support.TestContext; -import org.apache.fineract.test.support.TestContextKey; -import org.springframework.beans.factory.annotation.Autowired; -import retrofit2.Response; - -@Slf4j -public class BusinessStepStepDef extends AbstractStepDef { - - private static final String WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS = "LOAN_CLOSE_OF_BUSINESS"; - private static final String BUSINESS_STEP_NAME_APPLY_CHARGE_TO_OVERDUE_LOANS = "APPLY_CHARGE_TO_OVERDUE_LOANS"; - private static final String BUSINESS_STEP_NAME_LOAN_DELINQUENCY_CLASSIFICATION = "LOAN_DELINQUENCY_CLASSIFICATION"; - private static final String BUSINESS_STEP_NAME_CHECK_LOAN_REPAYMENT_DUE = "CHECK_LOAN_REPAYMENT_DUE"; - private static final String BUSINESS_STEP_NAME_CHECK_LOAN_REPAYMENT_OVERDUE = "CHECK_LOAN_REPAYMENT_OVERDUE"; - private static final String BUSINESS_STEP_NAME_UPDATE_LOAN_ARREARS_AGING = "UPDATE_LOAN_ARREARS_AGING"; - private static final String BUSINESS_STEP_NAME_ADD_PERIODIC_ACCRUAL_ENTRIES = "ADD_PERIODIC_ACCRUAL_ENTRIES"; - private static final String BUSINESS_STEP_NAME_EXTERNAL_ASSET_OWNER_TRANSFER = "EXTERNAL_ASSET_OWNER_TRANSFER"; - private static final String BUSINESS_STEP_NAME_CHECK_DUE_INSTALLMENTS = "CHECK_DUE_INSTALLMENTS"; - private static final String BUSINESS_STEP_NAME_ACCRUAL_ACTIVITY_POSTING = "ACCRUAL_ACTIVITY_POSTING"; - private static final List ORIGINAL_COB_BUSINESS_STEPS = TestContext.GLOBAL - .get(TestContextKey.ORIGINAL_COB_WORKFLOW_JOB_BUSINESS_STEP_LIST); - - @Autowired - private BusinessStepConfigurationApi businessStepConfigurationApi; - - @Given("Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow") - public void putExternalAssetOwnerTransferJobInCOB() throws IOException { - List businessSteps = new ArrayList<>(ORIGINAL_COB_BUSINESS_STEPS); - Long lastOrder = businessSteps.get(businessSteps.size() - 1).getOrder(); - - BusinessStep externalAssetOwnerTransfer = new BusinessStep().stepName(BUSINESS_STEP_NAME_EXTERNAL_ASSET_OWNER_TRANSFER) - .order(lastOrder + 1); - businessSteps.add(externalAssetOwnerTransfer); - - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); - - // --- log changes --- - logChanges(); - } - - @Then("Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow") - public void removeExternalAssetOwnerTransferJobInCOB() throws IOException { - setBackBusinessStepsToOriginal(); - } - - @Given("Admin puts CHECK_DUE_INSTALLMENTS job into LOAN_CLOSE_OF_BUSINESS workflow") - public void putCheckDueInstallmentsJobInCOB() throws IOException { - List businessSteps = new ArrayList<>(ORIGINAL_COB_BUSINESS_STEPS); - Long lastOrder = businessSteps.get(businessSteps.size() - 1).getOrder(); - - BusinessStep checkDueInstallments = new BusinessStep().stepName(BUSINESS_STEP_NAME_CHECK_DUE_INSTALLMENTS).order(lastOrder + 1); - businessSteps.add(checkDueInstallments); - - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); - - // --- log changes --- - logChanges(); - } - - @Then("Admin removes CHECK_DUE_INSTALLMENTS job from LOAN_CLOSE_OF_BUSINESS workflow") - public void removeCheckDueInstallmentsJobInCOB() throws IOException { - setBackBusinessStepsToOriginal(); - } - - @Given("Admin puts {string} business step into LOAN_CLOSE_OF_BUSINESS workflow") - public void putGivenJobInCOB(String businessStepName) throws IOException { - List businessSteps = new ArrayList<>(ORIGINAL_COB_BUSINESS_STEPS); - Long lastOrder = businessSteps.get(businessSteps.size() - 1).getOrder(); - - CobBusinessStep cobBusinessStep = CobBusinessStep.valueOf(businessStepName); - String stepName = cobBusinessStep.getValue(); - - BusinessStep businessStepToAdd = new BusinessStep().stepName(stepName).order(lastOrder + 1); - businessSteps.add(businessStepToAdd); - - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); - - // --- log changes --- - logChanges(); - } - - @Then("Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state") - public void setBackCOBToInitialState() throws IOException { - setBackBusinessStepsToOriginal(); - } - - @Then("Admin removes {string} business step into LOAN_CLOSE_OF_BUSINESS workflow") - public void removeGivenJobInCOB(String businessStepName) throws IOException { - List businessSteps = new ArrayList<>(ORIGINAL_COB_BUSINESS_STEPS); - - CobBusinessStep cobBusinessStep = CobBusinessStep.valueOf(businessStepName); - String stepName = cobBusinessStep.getValue(); - - businessSteps.removeIf(businessStep -> businessStep.getStepName().equals(stepName)); - - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(businessSteps); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); - - // --- log changes --- - logChanges(); - } - - private void setBackBusinessStepsToOriginal() throws IOException { - log.debug("Setting back Business steps to original..."); - UpdateBusinessStepConfigRequest request = new UpdateBusinessStepConfigRequest().businessSteps(ORIGINAL_COB_BUSINESS_STEPS); - - Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) - .execute(); - ErrorHelper.checkSuccessfulApiCall(response); - - // --- log changes --- - logChanges(); - } - - private void logChanges() throws IOException { - // --- log changes --- - Response changesResponse = businessStepConfigurationApi - .retrieveAllConfiguredBusinessStep(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS).execute(); - List businessStepsChanged = changesResponse.body().getBusinessSteps(); - List changes = businessStepsChanged// - .stream()// - .sorted(Comparator.comparingLong(BusinessStep::getOrder))// - .map(BusinessStep::getStepName)// - .collect(Collectors.toList());// - - log.debug("Business steps has been CHANGED to the following:"); - changes.forEach(e -> log.debug(e)); - } -} 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 3397a2e97fd..7d7ad8e3adb 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 @@ -24,7 +24,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public abstract class TestContextKey { - public static final String ORIGINAL_COB_WORKFLOW_JOB_BUSINESS_STEP_LIST = "originalCOBWorkflowJobBusinessStepList"; public static final String CLIENT_CREATE_RESPONSE = "clientCreateResponse"; public static final String CLIENT_CREATE_SECOND_CLIENT_RESPONSE = "clientCreateSecondClientResponse"; public static final String LOAN_CREATE_RESPONSE = "loanCreateResponse"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/0_COB.feature b/fineract-e2e-tests-runner/src/test/resources/features/0_COB.feature index b1683c0d386..afefc07b64f 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/0_COB.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/0_COB.feature @@ -215,7 +215,6 @@ Feature: COBFeature @TestRailId:C3044 @AdvancedPaymentAllocation Scenario: Verify that LoanAccountCustomSnapshotBusinessEvent is created with proper business date when installment is due date and COB runs - Given Admin puts CHECK_DUE_INSTALLMENTS job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -227,6 +226,5 @@ Feature: COBFeature When Admin sets the business date to "17 January 2024" When Admin runs inline COB job for Loan Then LoanAccountCustomSnapshotBusinessEvent is created with business date "17 January 2024" - Then Admin removes CHECK_DUE_INSTALLMENTS job from LOAN_CLOSE_OF_BUSINESS workflow diff --git a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature index 05f120a1294..eb0f9453e42 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature @@ -3,7 +3,6 @@ Feature: Asset Externalization @TestRailId:C2722 Scenario: Verify that all fields and values are correct in case of a SALES request by loan id and user-generated transferExternalId - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -24,11 +23,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 9999-12-31 | SALE | Then Asset externalization details has the generated transferExternalId - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2723 Scenario: Verify that all fields and values are correct in case of a SALES request by loan id system-generated transferExternalId - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -49,11 +46,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 9999-12-31 | SALE | Then Asset externalization details has the generated transferExternalId - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2724 Scenario: Verify that all fields and values are correct in case of a SALES request by loan external id user-generated transferExternalId - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -74,11 +69,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 9999-12-31 | SALE | Then Asset externalization details has the generated transferExternalId - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2725 Scenario: Verify that all fields and values are correct in case of a SALES request by loan external id system-generated transferExternalId - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -99,11 +92,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 9999-12-31 | SALE | Then Asset externalization details has the generated transferExternalId - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2727 Scenario: Verify that Asset externalization details are correct after CoB in case of a SALES request by loan id - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -123,11 +114,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2729 Scenario: Verify that Asset externalization details has the correct data in case of a BUYBACK request placed before the settlementDate with a same settlementDate as the sales one - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -159,11 +148,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | BUYBACK | 2023-05-10 | 2023-05-21 | BUYBACK | | 2023-05-21 | 1 | CANCELLED | 2023-05-21 | 2023-05-21 | SALE | | 2023-05-21 | 1 | CANCELLED | 2023-05-21 | 2023-05-21 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2730 Scenario: Verify that Asset externalization details has the correct data in case of a BUYBACK request placed on a business date before the settlementDate of sales request and with a settlementDate for buyback after the sales got active - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -200,11 +187,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-10 | 2023-05-30 | BUYBACK | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-30 | SALE | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2731 Scenario: Verify that Asset externalization details has the correct data in case of a BUYBACK request placed after the settlementDate - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -245,11 +230,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-30 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 2023-05-30 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2732 Scenario: Verify that BUYBACK request on a loan with PENDING ownership where BUYBACK settlement date is earlier than SALE settlement date results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -268,11 +251,9 @@ Feature: Asset Externalization Then BUYBACK transaction results a 403 error and proper error message when its settlementDate is earlier than the original settlementDate | Transaction type | settlementDate | | buyback | 2023-05-15 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2733 Scenario: Verify that SALES request on a fully paid loan results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -284,11 +265,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-21 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2734 Scenario: Verify that SALES request on an overpaid loan results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -300,11 +279,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-21 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2735 Scenario: Verify that SALES request on a loan with ACTIVE ownership results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -330,11 +307,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "ASSET_OWNED_CANNOT_BE_SOLD" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-30 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2736 Scenario: Verify that BUYBACK request on a fully paid loan can be done successfully - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -367,11 +342,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 9999-12-31 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2737 Scenario: Verify that BUYBACK request on an overpaid loan can be done successfully - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -404,11 +377,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 9999-12-31 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2738 Scenario: Verify that BUYBACK request on a loan with INACTIVE ownership results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -418,11 +389,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "ASSET_NOT_OWNED_CANNOT_BE_BOUGHT" error message | Transaction type | settlementDate | purchasePriceRatio | | buyback | 2023-05-21 | | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2739 Scenario: Verify that SALES request can NOT be placed on a loan which is not APPROVED yet - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -430,11 +399,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-30 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2740 Scenario: Verify that SALES request can NOT be placed on a loan which is not DISBURSED yet - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -443,11 +410,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-30 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2741 Scenario: Verify that SALES request on a loan with PENDING ownership results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -466,11 +431,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "ALREADY_PENDING" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-30 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2742 Scenario: Verify that SALES with settlement date earlier than actual business date results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -480,11 +443,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "SETTLEMENT_DATE_IN_THE_PAST" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-04-21 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2743 Scenario: Verify that SALES with null owner external id results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -494,11 +455,9 @@ Feature: Asset Externalization Then Asset externalization SALES transaction with ownerExternalId = null and the following data results a 400 error and "INVALID_REQUEST" error message | settlementDate | purchasePriceRatio | | 2023-05-21 | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2744 Scenario: Verify that SALES with null purchase price ratio results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -508,11 +467,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 400 error and "INVALID_REQUEST" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | 2023-05-21 | | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2745 Scenario: Verify that SALES with null settlement date results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -522,11 +479,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 400 error and "INVALID_REQUEST" error message | Transaction type | settlementDate | purchasePriceRatio | | sale | | 1 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2746 Scenario: Verify that BUYBACK request on a loan with PENDING BUYBACK ownership result an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -563,11 +518,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "BUYBACK_ALREADY_IN_PROGRESS_CANNOT_BE_BOUGHT" error message | Transaction type | settlementDate | purchasePriceRatio | | buyback | 2023-05-30 | | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2747 Scenario: Verify that BUYBACK with settlement date earlier than actual business date results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -594,11 +547,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 403 error and "SETTLEMENT_DATE_IN_THE_PAST" error message | Transaction type | settlementDate | purchasePriceRatio | | buyback | 2023-05-21 | | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2748 Scenario: Verify that BUYBACK with ownerExternalId=NULL can be placed, and results a 200OK response - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -628,11 +579,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 9999-12-31 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2749 Scenario: Verify that BUYBACK with purchasePriceRatio=NULL can be placed, and results a 200OK response - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -664,11 +613,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 9999-12-31 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2750 Scenario: Verify that SALES with null settlement date results an error - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -695,11 +642,9 @@ Feature: Asset Externalization Then Asset externalization transaction with the following data results a 400 error and "INVALID_REQUEST" error message | Transaction type | settlementDate | purchasePriceRatio | | buyback | | | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2751 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: no other transactions - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -757,11 +702,9 @@ Feature: Asset Externalization | glAccountType | glAccountCode | glAccountName | entryType | amount | | ASSET | 112601 | Loans Receivable | DEBIT | 1000.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 1000.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2752 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: fee applied before sale, and penalty applied before buyback - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -834,11 +777,9 @@ Feature: Asset Externalization | INCOME | 404007 | Fee Income | CREDIT | 20.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 1000.00 | | ASSET | 112603 | Interest/Fee Receivable | CREDIT | 30.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2753 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: Repyment while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -899,11 +840,9 @@ Feature: Asset Externalization | ASSET | 112601 | Loans Receivable | CREDIT | 200.00 | | LIABILITY | 145023 | Suspense/Clearing account | DEBIT | 200.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 800.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2754 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: GOODWILL_CREDIT transaction while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -964,11 +903,9 @@ Feature: Asset Externalization | ASSET | 112601 | Loans Receivable | CREDIT | 200.00 | | EXPENSE | 744003 | Goodwill Expense Account | DEBIT | 200.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 800.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2755 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: MERCHANT_ISSUED_REFUND transaction while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1029,11 +966,9 @@ Feature: Asset Externalization | ASSET | 112601 | Loans Receivable | CREDIT | 200.00 | | LIABILITY | 145023 | Suspense/Clearing account | DEBIT | 200.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 800.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2756 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: PAYOUT_REFUND transaction while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1094,11 +1029,9 @@ Feature: Asset Externalization | ASSET | 112601 | Loans Receivable | CREDIT | 200.00 | | LIABILITY | 145023 | Suspense/Clearing account | DEBIT | 200.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 800.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2757 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: REPAYMENT_ADJUSTMENT_REFUND chargeback transaction while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1164,11 +1097,9 @@ Feature: Asset Externalization | ASSET | 112601 | Loans Receivable | DEBIT | 800.00 | | LIABILITY | 145023 | Suspense/Clearing account | CREDIT | 800.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 800.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2758 @AssetExternalizationJournalEntry Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: CHARGE ADJUSTMENT transaction while status is ACTIVE - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1237,11 +1168,9 @@ Feature: Asset Externalization | INCOME | 404007 | Fee Income | DEBIT | 100.00 | | ASSET | 112601 | Loans Receivable | CREDIT | 1000.00 | | ASSET | 112603 | Interest/Fee Receivable | CREDIT | 200.00 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2759 Scenario: Verify that LoanOwnershipTransferBusinessEvent and LoanAccountSnapshotBusinessEvent is created with correct data - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1277,11 +1206,9 @@ Feature: Asset Externalization | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 2023-05-30 | BUYBACK | Then LoanOwnershipTransferBusinessEvent is created Then LoanAccountSnapshotBusinessEvent is created - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2760 Scenario: Verify that LoanOwnershipTransferBusinessEvent and LoanAccountSnapshotBusinessEvent is created with correct data for partial repayment, fee, penalty - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1320,11 +1247,9 @@ Feature: Asset Externalization | 2023-05-30 | 1 | BUYBACK | 2023-05-25 | 2023-05-30 | BUYBACK | Then LoanOwnershipTransferBusinessEvent is created Then LoanAccountSnapshotBusinessEvent is created - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2771 Scenario: Verify that SALE and BUYBACK can be cancelled in right order - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1361,11 +1286,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | BUYBACK | 2023-05-10 | 2023-05-10 | | 2023-05-21 | 1 | CANCELLED | 2023-05-10 | 2023-05-10 | | 2023-05-21 | 1 | CANCELLED | 2023-05-01 | 2023-05-10 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2772 Scenario: Verify that SALE can be cancelled - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1385,11 +1308,9 @@ Feature: Asset Externalization | settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-10 | | 2023-05-21 | 1 | CANCELLED | 2023-05-01 | 2023-05-10 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2773 Scenario: Verify that active SALE can not be cancelled - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1410,11 +1331,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | When Admin send "cancel" command on "SALE" transaction it will throw an error - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2774 Scenario: Verify that Asset cannot be cancelled after SALE and BUYBACK is completed - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1452,11 +1371,9 @@ Feature: Asset Externalization | 2023-05-30 | 1 | BUYBACK | 2023-05-10 | 2023-05-30 | BUYBACK | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-30 | SALE | When Admin send "cancel" command on "SALE" transaction it will throw an error - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2775 Scenario: Verify that SALE and BUYBACK can be cancelled in right order with double cancel test - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1513,11 +1430,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | BUYBACK | 2023-05-10 | 2023-05-10 | | 2023-05-21 | 1 | CANCELLED | 2023-05-10 | 2023-05-10 | | 2023-05-21 | 1 | CANCELLED | 2023-05-01 | 2023-05-10 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2785 Scenario: Verify that when a loan with PENDING SALES is fully paid asset transfer status will be DECLINED - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1539,11 +1454,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-15 | SALE | | 2023-05-21 | 1 | DECLINED | 2023-05-15 | 2023-05-15 | SALE | Then LoanOwnershipTransferBusinessEvent with transfer status: "DECLINED" and transfer status reason "BALANCE_ZERO" is created - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2786 Scenario: Verify that when a loan with PENDING SALES is overpaid asset transfer status will be DECLINED - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1565,11 +1478,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-15 | SALE | | 2023-05-21 | 1 | DECLINED | 2023-05-15 | 2023-05-15 | SALE | Then LoanOwnershipTransferBusinessEvent with transfer status: "DECLINED" and transfer status reason "BALANCE_NEGATIVE" is created - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2787 Scenario: Verify that when a loan with PENDING BUYBACK is fully paid BUYBACK transaction can be done successfully - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1607,11 +1518,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-25 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-22 | 2023-05-25 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2788 Scenario: Verify that when a loan with PENDING BUYBACK is overpaid BUYBACK transaction can be done successfully - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1649,11 +1558,9 @@ Feature: Asset Externalization | 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-25 | SALE | | 2023-05-30 | 1 | BUYBACK | 2023-05-22 | 2023-05-25 | BUYBACK | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C2811 Scenario: Verify that transaction and transaction adjustment events has the proper external owner - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a new default Loan with date: "1 May 2023" @@ -1676,12 +1583,10 @@ Feature: Asset Externalization | 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | And Customer makes "AUTOPAY" repayment on "22 May 2023" with 10 EUR transaction amount and check external owner When Customer adjust "1"th repayment on "22 May 2023" with amount "9" and check external owner - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow @TestRailId:C3193 Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries: no other transactions - interest bearing loan - Given Admin puts EXTERNAL_ASSET_OWNER_TRANSFER job into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "1 May 2023" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -1774,4 +1679,3 @@ Feature: Asset Externalization | INCOME | 404000 | Interest Income | CREDIT | 0.33 | | ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 | | INCOME | 404000 | Interest Income | CREDIT | 0.33 | - Then Admin removes EXTERNAL_ASSET_OWNER_TRANSFER job from LOAN_CLOSE_OF_BUSINESS workflow diff --git a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature index d3b7387559e..652c5cce246 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature @@ -1300,6 +1300,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | | 15 February 2024 | Repayment | 83.81 | 83.57 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 0.82 | 0.0 | 0.82 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met @TestRailId:C3212 @@ -1365,6 +1366,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | | 15 February 2024 | Repayment | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met @TestRailId:C3213 @@ -1411,6 +1413,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 15 February 2024 | Repayment | 100.86 | 100.0 | 0.86 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 0.86 | 0.0 | 0.86 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met @TestRailId:C3214 @@ -1457,6 +1460,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 15 February 2024 | Repayment | 101.11 | 100.0 | 1.11 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 1.11 | 0.0 | 1.11 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met # TODO unskip and check when PS-2037 is done @@ -1577,11 +1581,11 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | - | 2 | 29 | 01 March 2024 | | 67.04 | 16.53 | 0.37 | 0.0 | 0.0 | 16.90 | 0.0 | 0.0 | 0.0 | 16.90 | - | 3 | 31 | 01 April 2024 | | 50.36 | 16.68 | 0.22 | 0.0 | 0.0 | 16.90 | 0.0 | 0.0 | 0.0 | 16.90 | - | 4 | 30 | 01 May 2024 | | 33.63 | 16.73 | 0.17 | 0.0 | 0.0 | 16.90 | 0.0 | 0.0 | 0.0 | 16.90 | - | 5 | 31 | 01 June 2024 | | 16.84 | 16.79 | 0.11 | 0.0 | 0.0 | 16.90 | 0.0 | 0.0 | 0.0 | 16.90 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.84 | 0.06 | 0.0 | 0.0 | 16.90 | 0.0 | 0.0 | 0.0 | 16.90 | + | 2 | 29 | 01 March 2024 | | 67.04 | 16.53 | 0.37 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | + | 3 | 31 | 01 April 2024 | | 50.36 | 16.68 | 0.22 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | + | 4 | 30 | 01 May 2024 | | 33.63 | 16.73 | 0.17 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | + | 5 | 31 | 01 June 2024 | | 16.84 | 16.79 | 0.11 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.84 | 0.06 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 100.0 | 1.51 | 0.0 | 0.0 | 101.51 | 17.01 | 0.0 | 0.0 | 84.5 | @@ -1873,7 +1877,6 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan @TestRailId:C3226 Scenario: Verify interest recalculation in case of overdue installments: UC1 - 1st installment overdue, interest recalculation: daily, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -1914,13 +1917,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3227 Scenario: Verify interest recalculation in case of overdue installments: UC2 - 1st installment overdue, interest recalculation: daily, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -1961,13 +1961,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3228 Scenario: Verify interest recalculation in case of overdue installments: UC3 - 1st installment overdue, interest recalculation: same as repayment period, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2008,13 +2005,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.26 | 0.0 | 0.26 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3229 Scenario: Verify interest recalculation in case of overdue installments: UC4 - 1st installment overdue, interest recalculation: same as repayment period, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2055,13 +2049,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.26 | 0.0 | 0.26 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3230 Scenario: Verify interest recalculation in case of overdue installments: UC5 - 1st and 2nd installment overdue, interest recalculation: daily, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2102,15 +2093,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.31 | 0.0 | 1.31 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3231 Scenario: Verify interest recalculation in case of overdue installments: UC6 - 1st and 2nd installment overdue, interest recalculation: daily, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2151,15 +2137,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.31 | 0.0 | 1.31 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3232 Scenario: Verify interest recalculation in case of overdue installments: UC7 - 1st and 2nd installment overdue, interest recalculation: same as repayment period, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2200,15 +2181,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.31 | 0.0 | 1.31 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3233 Scenario: Verify interest recalculation in case of overdue installments: UC8 - 1st and 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2249,15 +2225,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.31 | 0.0 | 1.31 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3234 Scenario: Verify interest recalculation in case of overdue installments: UC9 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: daily, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2318,14 +2289,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3235 Scenario: Verify interest recalculation in case of overdue installments: UC10 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: daily, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2386,14 +2353,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3236 Scenario: Verify interest recalculation in case of overdue installments: UC11 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2454,14 +2417,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3237 Scenario: Verify interest recalculation in case of overdue installments: UC12 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2522,14 +2481,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3238 Scenario: Verify interest recalculation in case of overdue installments: UC13 - 1st installment paid on due date, 2nd installment overdue with partial late repayment, interest recalculation: daily, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2610,15 +2565,11 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.18 | 0.0 | 1.18 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3239 Scenario: Verify interest recalculation in case of overdue installments: UC14 - 1st installment paid on due date, 2nd installment overdue with partial late repayment, interest recalculation: daily, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2699,15 +2650,11 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.18 | 0.0 | 1.18 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3240 Scenario: Verify interest recalculation in case of overdue installments: UC15 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till preclose - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2788,15 +2735,11 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3241 Scenario: Verify interest recalculation in case of overdue installments: UC16 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -2877,11 +2820,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3244 Scenario: Verify support of interest rate calculation with frequency Whole term configured for progressive loan @@ -3026,6 +2966,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 15 February 2024 | Repayment | 100.86 | 100.0 | 0.86 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 0.86 | 0.0 | 0.86 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met @TestRailId:C3246 @@ -3071,6 +3012,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 15 February 2024 | Repayment | 101.11 | 100.0 | 1.11 | 0.0 | 0.0 | 0.0 | false | false | + | 15 February 2024 | Accrual | 1.11 | 0.0 | 1.11 | 0.0 | 0.0 | 0.0 | false | false | Then Loan's all installments have obligations met @TestRailId:C3248 @@ -3116,8 +3058,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | | 15 February 2024 | Repayment | 15.0 | 15.0 | 0.0 | 0.0 | 0.0 | 85.0 | false | false | @TestRailId:C3249 @@ -3163,12 +3104,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | | 15 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - # TODO unskip and check when PS-1958 and PS-2076 is done - @Skip @TestRailId:C3250 + @TestRailId:C3250 Scenario: Verify interest recalculation - late repayment, adjust NEXT installment - UC2: 360/30, daily, excess then EMI amount is paid When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3199,10 +3138,10 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Repayment schedule has 6 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 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | 15 February 2024 | 66.56 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 17.01 | 0.0 | - | 2 | 29 | 01 March 2024 | 15 February 2024 | 66.84 | 16.78 | 0.28 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 1 | 31 | 01 February 2024 | 15 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 17.01 | 0.0 | + | 2 | 29 | 01 March 2024 | 15 February 2024 | 66.84 | 16.73 | 0.28 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | | 3 | 31 | 01 April 2024 | | 50.42 | 16.42 | 0.59 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.70 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.7 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 5 | 31 | 01 June 2024 | | 16.89 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 6 | 30 | 01 July 2024 | | 0.0 | 16.89 | 0.1 | 0.0 | 0.0 | 16.99 | 0.0 | 0.0 | 0.0 | 16.99 | Then Loan Repayment schedule has the following data in Total row: @@ -3211,13 +3150,11 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | | 15 February 2024 | Repayment | 34.02 | 33.16 | 0.86 | 0.0 | 0.0 | 66.84 | false | false | + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | - # TODO unskip and check when PS-1958 and PS-2076 is done - @Skip @TestRailId:C3267 - Scenario: Verify interest recalculation - late repayment, adjust LAST installment - UC2: 360/30, daily, lesser then EMI amount is paid + @TestRailId:C3267 + Scenario: Verify interest recalculation - late repayment, adjust LAST installment - UC2: 360/30, daily, excess EMI amount is paid When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule @@ -3248,27 +3185,24 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Repayment schedule has 6 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 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | 15 February 2024 | 66.56 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 16.43 | 0.0 | 16.43 | 0.0 | - | 2 | 29 | 01 March 2024 | | 50.03 | 16.53 | 0.48 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 33.31 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 16.49 | 16.82 | 0.19 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 0.01 | 16.48 | 0.1 | 0.0 | 0.0 | 16.48 | 0.0 | 0.0 | 0.0 | 16.58 | - | 6 | 30 | 01 July 2024 | 15 February 2024 | -17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 1 | 31 | 01 February 2024 | 15 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 17.01 | 0.0 | + | 2 | 29 | 01 March 2024 | | 67.04 | 16.53 | 0.48 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.32 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.5 | 16.82 | 0.19 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 17.01 | 16.49 | 0.1 | 0.0 | 0.0 | 16.58 | 0.0 | 0.0 | 0.0 | 16.59 | + | 6 | 30 | 01 July 2024 | 15 February 2024 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 100.57 | 1.64 | 0.0 | 0.0 | 101.64 | 34.02 | 34.02 | 0.0 | 67.61 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.22 | 0.0 | 0.22 | 0.0 | 0.0 | 0.0 | false | false | | 15 February 2024 | Repayment | 34.02 | 33.44 | 0.58 | 0.0 | 0.0 | 66.56 | false | false | + | 14 February 2024 | Accrual | 0.84 | 0.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | false | When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3251 + @TestRailId:C3251 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC1: 360/30, early repayment with amount less then due interest - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3326,12 +3260,9 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 0.2 | 0.0 | 0.2 | 0.0 | 0.0 | 100.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3252 + @@TestRailId:C3252 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC2: 360/30, early repayment with amount 1 cent more then due interest - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3389,12 +3320,9 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 0.27 | 0.01 | 0.26 | 0.0 | 0.0 | 99.99 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3253 + @TestRailId:C3253 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC3: 360/30, early repayment with less than EMI amount - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3439,25 +3367,22 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 05 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 07 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 10 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 13 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 14 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 15.0 | 14.74 | 0.26 | 0.0 | 0.0 | 85.26 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3254 + @TestRailId:C3254 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC4: 360/30, early repayment with only 1 cent less than EMI amount - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3501,24 +3426,23 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 02 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 04 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 06 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 07 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 08 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 09 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 10 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 12 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 14 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.0 | 16.74 | 0.26 | 0.0 | 0.0 | 83.26 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3255 + @TestRailId:C3255 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC5: 360/30, multiple early repayments for the same installment - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3567,25 +3491,28 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 05 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 07 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 10 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - | 13 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 15.0 | 14.74 | 0.26 | 0.0 | 0.0 | 85.26 | false | false | + | 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | | 20 January 2024 | Repayment | 2.01 | 1.93 | 0.08 | 0.0 | 0.0 | 83.33 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3256 + @TestRailId:C3256 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC6: 360/30, early repayment with exact EMI amount - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3629,24 +3556,23 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 02 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 04 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 06 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 07 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 08 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 09 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 10 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 12 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | - | 14 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3257 + @TestRailId:C3257 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC7: 360/30, early repayment with twice than EMI amount - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3690,13 +3616,23 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 34.02 | 33.76 | 0.26 | 0.0 | 0.0 | 66.24 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state -# TODO unskip and check when PS-2076 and PS-2106 is done - @Skip @TestRailId:C3258 + @TestRailId:C3258 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC8: 360/30, preclose after early repayment - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -3732,8 +3668,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Repayment schedule has 6 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 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2024 | 15 January 2024 | 83.33 | 16.67 | 0.34 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | - | 2 | 29 | 01 March 2024 | 20 January 2024 | 66.32 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 1 | 31 | 01 February 2024 | 15 January 2024 | 83.25 | 16.75 | 0.26 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 20 January 2024 | 66.32 | 16.93 | 0.08 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | | 3 | 31 | 01 April 2024 | 20 January 2024 | 49.31 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | | 4 | 30 | 01 May 2024 | 20 January 2024 | 32.3 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | | 5 | 31 | 01 June 2024 | 20 January 2024 | 15.29 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | @@ -3744,20 +3680,30 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | | 20 January 2024 | Repayment | 83.33 | 83.25 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 20 January 2024 | Accrual | 0.1 | 0.0 | 0.1 | 0.0 | 0.0 | 0.0 | false | false | -# TODO unskip and check when PS-2076 and PS-2106 is done -# TODO is it till preclose or tillrestperiod? - @Skip @TestRailId:C3259 + @TestRailId:C3259 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC9: 360/30, interest modification after early repayment - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule 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 | + | 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_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1 | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount @@ -3785,24 +3731,71 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan When Admin sets the business date to "20 January 2024" When Admin creates and approves Loan reschedule with the following data: | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | - | 01 March 2024 | 20 January 2024 | | | | | 4 | + | 21 January 2024 | 20 January 2024 | | | | | 4 | + When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 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 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 01 February 2024 | 15 January 2024 | 83.25 | 16.75 | 0.26 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | - | 2 | 29 | 01 March 2024 | | 66.88 | 16.37 | 0.47 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 3 | 31 | 01 April 2024 | | 50.26 | 16.62 | 0.22 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 4 | 30 | 01 May 2024 | | 33.59 | 16.67 | 0.17 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 5 | 31 | 01 June 2024 | | 16.86 | 16.73 | 0.11 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.86 | 0.06 | 0.0 | 0.0 | 16.92 | 0.0 | 0.0 | 0.0 | 16.92 | + | 2 | 29 | 01 March 2024 | | 66.86 | 16.39 | 0.47 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 3 | 31 | 01 April 2024 | | 50.22 | 16.64 | 0.22 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 4 | 30 | 01 May 2024 | | 33.53 | 16.69 | 0.17 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 5 | 31 | 01 June 2024 | | 16.78 | 16.75 | 0.11 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.78 | 0.06 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 100.0 | 1.29 | 0.0 | 0.0 | 101.29 | 17.01 | 17.01 | 0.0 | 84.28 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state + | 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "25 January 2024" + When Admin runs inline COB job for Loan + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | + | 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 22 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 23 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 24 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3269 Scenario: UC1 - Single disbursement, full refund within first installment period @@ -3876,6 +3869,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 February 2024 | Repayment | 255.14 | 246.75 | 8.39 | 0.0 | 0.0 | 753.25 | false | false | | 09 February 2024 | Payout Refund | 1000.0 | 753.25 | 1.63 | 0.0 | 0.0 | 0.0 | false | false | | 09 February 2024 | Interest Refund | 10.02 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 09 February 2024 | Accrual | 10.02 | 0.0 | 10.02 | 0.0 | 0.0 | 0.0 | false | false | When Admin sets the business date to "10 February 2024" When Admin makes Credit Balance Refund transaction on "10 February 2024" with 255.14 EUR transaction amount Then Loan Repayment schedule has 4 periods, with the following data for periods: @@ -3894,8 +3888,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 February 2024 | Repayment | 255.14 | 246.75 | 8.39 | 0.0 | 0.0 | 753.25 | false | false | | 09 February 2024 | Payout Refund | 1000.0 | 753.25 | 1.63 | 0.0 | 0.0 | 0.0 | false | false | | 09 February 2024 | Interest Refund | 10.02 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 09 February 2024 | Accrual | 10.02 | 0.0 | 10.02 | 0.0 | 0.0 | 0.0 | false | false | | 10 February 2024 | Credit Balance Refund | 255.14 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | - | 10 February 2024 | Accrual | 10.02 | 0.0 | 10.02 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3271 @@ -4012,6 +4006,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 February 2024 | Repayment | 255.14 | 247.57 | 7.57 | 0.0 | 0.0 | 752.43 | false | false | | 09 February 2024 | Merchant Issued Refund | 1000.0 | 752.43 | 1.63 | 0.0 | 0.0 | 0.0 | false | false | | 09 February 2024 | Interest Refund | 9.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 09 February 2024 | Accrual | 9.2 | 0.0 | 9.2 | 0.0 | 0.0 | 0.0 | false | false | When Admin sets the business date to "10 February 2024" When Admin makes Credit Balance Refund transaction on "10 February 2024" with 255.14 EUR transaction amount Then Loan Repayment schedule has 4 periods, with the following data for periods: @@ -4032,8 +4027,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 February 2024 | Repayment | 255.14 | 247.57 | 7.57 | 0.0 | 0.0 | 752.43 | false | false | | 09 February 2024 | Merchant Issued Refund | 1000.0 | 752.43 | 1.63 | 0.0 | 0.0 | 0.0 | false | false | | 09 February 2024 | Interest Refund | 9.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 09 February 2024 | Accrual | 9.2 | 0.0 | 9.2 | 0.0 | 0.0 | 0.0 | false | false | | 10 February 2024 | Credit Balance Refund | 255.14 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | - | 10 February 2024 | Accrual | 9.2 | 0.0 | 9.2 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3277 @@ -4099,6 +4094,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 March 2024 | Repayment | 4.33 | 4.33 | 0.0 | 0.0 | 0.0 | 498.11 | false | false | | 01 April 2024 | Repayment | 255.14 | 245.28 | 9.86 | 0.0 | 0.0 | 252.83 | false | false | | 01 May 2024 | Repayment | 254.88 | 252.83 | 2.05 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 20.3 | 0.0 | 20.3 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @@ -4167,6 +4163,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 09 February 2024 | Interest Refund | 5.29 | 5.29 | 0.0 | 0.0 | 0.0 | 249.59 | false | false | | 01 April 2024 | Repayment | 4.99 | 1.48 | 3.51 | 0.0 | 0.0 | 248.11 | false | false | | 01 May 2024 | Repayment | 250.12 | 248.11 | 2.01 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 15.54 | 0.0 | 15.54 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3279 @@ -4239,6 +4236,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 March 2024 | Repayment | 7.44 | 2.14 | 5.3 | 0.0 | 0.0 | 500.7 | false | false | | 01 April 2024 | Repayment | 255.14 | 250.94 | 4.2 | 0.0 | 0.0 | 249.76 | false | false | | 01 May 2024 | Repayment | 251.79 | 249.76 | 2.03 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 17.21 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3280 @@ -4297,6 +4295,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 March 2024 | Repayment | 7.22 | 1.92 | 5.3 | 0.0 | 0.0 | 500.11 | false | false | | 01 April 2024 | Repayment | 254.83 | 250.64 | 4.19 | 0.0 | 0.0 | 249.47 | false | false | | 01 May 2024 | Repayment | 251.49 | 249.47 | 2.02 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 15.98 | 0.0 | 15.98 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3281 @@ -4354,6 +4353,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 March 2024 | Repayment | 254.83 | 248.93 | 5.9 | 0.0 | 0.0 | 503.41 | false | false | | 01 April 2024 | Repayment | 254.83 | 250.61 | 4.22 | 0.0 | 0.0 | 252.8 | false | false | | 01 May 2024 | Repayment | 254.85 | 252.8 | 2.05 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 19.34 | 0.0 | 19.34 | 0.0 | 0.0 | 0.0 | false | false | | 10 May 2024 | Payout Refund | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | | 10 May 2024 | Interest Refund | 14.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | When Admin sets the business date to "11 May 2024" @@ -4377,15 +4377,14 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 01 March 2024 | Repayment | 254.83 | 248.93 | 5.9 | 0.0 | 0.0 | 503.41 | false | false | | 01 April 2024 | Repayment | 254.83 | 250.61 | 4.22 | 0.0 | 0.0 | 252.8 | false | false | | 01 May 2024 | Repayment | 254.85 | 252.8 | 2.05 | 0.0 | 0.0 | 0.0 | false | false | + | 01 May 2024 | Accrual | 19.34 | 0.0 | 19.34 | 0.0 | 0.0 | 0.0 | false | false | | 10 May 2024 | Payout Refund | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | | 10 May 2024 | Interest Refund | 14.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | | 11 May 2024 | Credit Balance Refund | 514.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | - | 11 May 2024 | Accrual | 19.34 | 0.0 | 19.34 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" @TestRailId:C3288 Scenario: Verify the recalculated EMI after interest rate change on the repayment schedule - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 January 2024" When Admin creates a client with random data When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -4406,8 +4405,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan When Admin runs inline COB job for Loan When Admin sets the business date to "02 February 2024" When Admin runs inline COB job for Loan - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state - Then Loan Repayment schedule has 6 periods, with the following data for periods: + Then Loan Repayment schedule has 6 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 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | @@ -4418,7 +4416,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 6 | 30 | 01 July 2024 | | 0.0 | 16.79 | 0.06 | 0.0 | 0.0 | 16.85 | 0.0 | 0.0 | 0.0 | 16.85 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 1.36 | 0.0 | 0.0 | 101.36 | 33.91 | 16.90 | 0.0 | 67.45 | + | 100.0 | 1.36 | 0.0 | 0.0 | 101.36 | 33.91 | 16.9 | 0.0 | 67.45 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index f3453e6d10b..a2164667e92 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -6060,7 +6060,6 @@ Feature: Loan And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount When Admin sets the business date to "15 July 2024" - When Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 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 | @@ -6071,7 +6070,6 @@ Feature: Loan | 4 | 30 | 01 May 2024 | | 34.28 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 5 | 31 | 01 June 2024 | | 17.85 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 6 | 30 | 01 July 2024 | | 0.0 | 17.85 | 0.58 | 0.0 | 0.0 | 18.43 | 0.0 | 0.0 | 0.0 | 18.43 | - When Admin removes "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow Scenario: Interest recalculation - S2 2 overdue Given Global configuration "enable-business-date" is enabled @@ -6083,7 +6081,6 @@ Feature: Loan And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount When Admin sets the business date to "10 March 2024" - When Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 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 | @@ -6094,7 +6091,6 @@ Feature: Loan | 4 | 30 | 01 May 2024 | | 33.87 | 16.71 | 0.3 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 5 | 31 | 01 June 2024 | | 17.06 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 6 | 30 | 01 July 2024 | | 0.0 | 17.06 | 0.1 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | - When Admin removes "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow Scenario: Interest recalculation - S3 1 paid, 1 overdue Given Global configuration "enable-business-date" is enabled @@ -6108,7 +6104,6 @@ Feature: Loan When Admin sets the business date to "1 February 2024" And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount When Admin sets the business date to "10 March 2024" - When Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 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 | @@ -6119,7 +6114,6 @@ Feature: Loan | 4 | 30 | 01 May 2024 | | 33.74 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 5 | 31 | 01 June 2024 | | 16.93 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | | 6 | 30 | 01 July 2024 | | 0.0 | 16.93 | 0.1 | 0.0 | 0.0 | 17.03 | 0.0 | 0.0 | 0.0 | 17.03 | - When Admin removes "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow Scenario: Loan Details Emi Amount Variations - AssociationsAll Given Global configuration "is-interest-to-be-recovered-first-when-greater-than-emi" is enabled diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index 5e19a7c79d5..f6f10ca7fbe 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3501,7 +3501,6 @@ Feature: LoanRepayment @TestRailId:C3224 Scenario: Verify that interest recalculation works properly when triggered by COB - Given Admin puts "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow When Admin sets the business date to "01 April 2024" When Admin creates a client with random data When Admin creates a fully customized loan with the following data: @@ -3549,7 +3548,6 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 05 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 500.0 | | 20 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 | - Then Admin sets back LOAN_CLOSE_OF_BUSINESS workflow to its initial state @TestRailId:C3225 Scenario: Verify that payment allocation is correct in case of fee charged on an OVERPAID Loan and payment is backdated diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java new file mode 100644 index 00000000000..73cb3619248 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.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.infrastructure.event.business.domain.loan.transaction; + +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanAccrualAdjustmentTransactionBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanAccrualAdjustmentTransactionBusinessEvent"; + + public LoanAccrualAdjustmentTransactionBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java new file mode 100644 index 00000000000..a4798fcda39 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +@Accessors(chain = true) +@RequiredArgsConstructor +public class AccrualChargeData { + + private final Long loanChargeId; + private final Long loanInstallmentChargeId; + private final boolean isPenalty; + private Money chargeAmount; + private Money chargeAccruable; + private Money chargeAccrued; + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.java new file mode 100644 index 00000000000..51b689a8a50 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.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.loanaccount.data; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +@Accessors(chain = true) +@RequiredArgsConstructor +public class AccrualPeriodData { + + private final Integer installmentNumber; + private final boolean isFirstPeriod; + private final LocalDate startDate; + private final LocalDate dueDate; + private Money interestAmount; + private Money interestAccruable; + private Money interestAccrued; + private final List charges = new ArrayList<>(); + + public AccrualPeriodData addCharge(AccrualChargeData charge) { + charges.add(charge); + return this; + } + + public Money getChargeAmount() { + return charges.stream().map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getFeeAmount() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getPenaltyAmount() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getChargeAccrued() { + return charges.stream().map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getFeeAccrued() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getPenaltyAccrued() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getChargeAccruable() { + return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus); + } + + public Money getFeeAccruable() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccruable).reduce(null, + MathUtil::plus); + } + + public Money getPenaltyAccruable() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccruable).reduce(null, + MathUtil::plus); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodsData.java new file mode 100644 index 00000000000..b4bdf67e2ca --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodsData.java @@ -0,0 +1,64 @@ +/** + * 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.data; + +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; + +@Data +@Accessors(chain = true) +@RequiredArgsConstructor +public class AccrualPeriodsData { + + private final MonetaryCurrency currency; + private final List periods = new ArrayList<>(); + + public AccrualPeriodsData addPeriod(AccrualPeriodData period) { + periods.add(period); + return this; + } + + public static AccrualPeriodsData create(@NotNull List installments, Integer firstInstallmentNumber, + MonetaryCurrency currency) { + AccrualPeriodsData accrualPeriods = new AccrualPeriodsData(currency); + for (LoanRepaymentScheduleInstallment installment : installments) { + Integer installmentNumber = installment.getInstallmentNumber(); + boolean isFirst = installmentNumber.equals(firstInstallmentNumber); + accrualPeriods + .addPeriod(new AccrualPeriodData(installmentNumber, isFirst, installment.getFromDate(), installment.getDueDate())); + } + return accrualPeriods; + } + + public AccrualPeriodData getPeriodByInstallmentNumber(Integer installmentNumber) { + return installmentNumber == null ? null + : periods.stream().filter(p -> installmentNumber.equals(p.getInstallmentNumber())).findFirst().orElse(null); + } + + public Integer getFirstInstallmentNumber() { + return periods.stream().filter(AccrualPeriodData::isFirstPeriod).map(AccrualPeriodData::getInstallmentNumber).findFirst() + .orElse(null); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java index 5ed0085b5c9..4c9a4c5bdb7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java @@ -20,7 +20,7 @@ import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Collection; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -66,7 +66,7 @@ public class LoanChargeData { private final BigDecimal amountOrPercentage; - private final Collection chargeOptions; + private final List chargeOptions; private final boolean penalty; @@ -84,7 +84,7 @@ public class LoanChargeData { private final BigDecimal maxCap; - private final Collection installmentChargeData; + private final List installmentChargeData; private BigDecimal amountAccrued; @@ -94,7 +94,7 @@ public class LoanChargeData { private final ExternalId externalLoanId; - public static LoanChargeData template(final Collection chargeOptions) { + public static LoanChargeData template(final List chargeOptions) { return new LoanChargeData(null, null, null, null, null, null, null, null, chargeOptions, false, null, false, false, null, ExternalId.empty(), null, null, null, null, ExternalId.empty()); } @@ -116,7 +116,7 @@ public LoanChargeData(final Long id, final Long chargeId, final String name, fin final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap, - final BigDecimal amountOrPercentage, Collection installmentChargeData, final ExternalId externalId) { + final BigDecimal amountOrPercentage, List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -160,9 +160,9 @@ public LoanChargeData(final Long id, final Long chargeId, final String name, fin private LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount, final BigDecimal percentage, final EnumOptionData chargeTimeType, final EnumOptionData chargeCalculationType, - final Collection chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, + final List chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap, - final BigDecimal amountOrPercentage, Collection installmentChargeData, final ExternalId externalId) { + final BigDecimal amountOrPercentage, List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -207,7 +207,7 @@ private LoanChargeData(final Long id, final Long chargeId, final String name, fi public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final LocalDate submittedOnDate, final BigDecimal amountOutstanding, EnumOptionData chargeTimeType, final Long loanId, final ExternalId externalLoanId, - Collection installmentChargeData, final ExternalId externalId) { + List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = null; this.name = null; @@ -308,7 +308,7 @@ public LoanChargeData(final BigDecimal amountUnrecognized, final LoanChargeData this.externalId = chargeData.externalId; } - public LoanChargeData(LoanChargeData chargeData, Collection installmentChargeData) { + public LoanChargeData(LoanChargeData chargeData, List installmentChargeData) { this.id = chargeData.id; this.chargeId = chargeData.chargeId; this.name = chargeData.name; 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 b3e34c74446..82b95d667a9 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 @@ -61,6 +61,7 @@ public class LoanTransactionEnumData { private final boolean reAmortize; private final boolean accrualActivity; private final boolean interestRefund; + private final boolean accrualAdjustment; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -96,6 +97,7 @@ public LoanTransactionEnumData(final Long id, final String code, final String va this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id); this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id); this.interestRefund = Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id); + this.accrualAdjustment = Long.valueOf(LoanTransactionType.ACCRUAL_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 d607f2764c2..73198f30907 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 @@ -749,11 +749,11 @@ public void addLoanCharge(final LoanCharge loanCharge) { } public ChangedTransactionDetail reprocessTransactions() { - ChangedTransactionDetail changedTransactionDetail; final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), + getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); } @@ -778,10 +778,10 @@ public ChangedTransactionDetail reprocessTransactionsWithPostTransactionChecks(L public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { final Money chargeAmount = loanCharge.getAmount(getCurrency()); Money feeCharges = chargeAmount; - Money penaltyCharges = Money.zero(loanCurrency()); + Money penaltyCharges = Money.zero(getCurrency()); if (loanCharge.isPenaltyCharge()) { penaltyCharges = chargeAmount; - feeCharges = Money.zero(loanCurrency()); + feeCharges = Money.zero(getCurrency()); } LocalDate transactionDate; @@ -898,8 +898,7 @@ public void removeLoanCharge(final LoanCharge loanCharge) { removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) { + if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) { /* * TODO Vishwas Currently we do not allow removing a loan charge after a loan is approved (hence there is no * need to adjust any loan transactions). @@ -907,9 +906,7 @@ public void removeLoanCharge(final LoanCharge loanCharge) { * Consider removing this block of code or logically completing it for the future by getting the list of * affected Transactions */ - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); + reprocessTransactions(); } this.charges.remove(loanCharge); updateLoanSummaryDerivedFields(); @@ -922,7 +919,7 @@ private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursemen for (final LoanTransaction transaction : transactions) { if (transaction.isRepaymentAtDisbursement() && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) { - final MonetaryCurrency currency = loanCurrency(); + final MonetaryCurrency currency = getCurrency(); final Money chargeAmount = Money.of(currency, loanCharge.amount()); if (transaction.isGreaterThan(chargeAmount)) { final Money principalPortion = Money.zero(currency); @@ -969,9 +966,7 @@ public Map updateLoanCharge(final LoanCharge loanCharge, final J * Consider removing this block of code or logically completing it for the future by getting the list of * affected Transactions */ - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); + reprocessTransactions(); } else { // reprocess loan schedule based on charge been waived. final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); @@ -1067,7 +1062,7 @@ public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLi final ExternalId externalId) { validateLoanIsNotClosed(loanCharge); - final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber); + final Money amountWaived = loanCharge.waive(getCurrency(), loanInstallmentNumber); changes.put("amount", amountWaived.getAmount()); Money unrecognizedIncome = amountWaived.zero(); @@ -1089,10 +1084,10 @@ public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLi } } Money feeChargesWaived = chargeComponent; - Money penaltyChargesWaived = Money.zero(loanCurrency()); + Money penaltyChargesWaived = Money.zero(getCurrency()); if (loanCharge.isPenaltyCharge()) { penaltyChargesWaived = chargeComponent; - feeChargesWaived = Money.zero(loanCurrency()); + feeChargesWaived = Money.zero(getCurrency()); } LocalDate transactionDate = getDisbursementDate(); @@ -1132,7 +1127,7 @@ public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLi // Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule // to be reprocessed. final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) { + if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) { /* * TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan * charges only waives the remaining amount. @@ -1140,9 +1135,7 @@ public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLi * Consider removing this block of code or logically completing it for the future by getting the list of * affected Transactions */ - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); + reprocessTransactions(); } else { // reprocess loan schedule based on charge been waived. final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); @@ -1347,8 +1340,7 @@ public void updateLoanSummaryDerivedFields() { this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero(); final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); - this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, - this.charges); + this.summary.updateSummary(getCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, this.charges); updateLoanOutstandingBalances(); } } @@ -1923,7 +1915,7 @@ private boolean atLeastOnceDisbursed() { public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) { - repaymentPeriod.updateObligationsMet(loanCurrency(), actualDisbursementDate); + repaymentPeriod.updateObligationsMet(getCurrency(), actualDisbursementDate); } } @@ -2002,7 +1994,7 @@ public void handleDisbursementTransaction(final LocalDate disbursedOn, final Pay * TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too? */ - final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(loanCurrency()); + final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(getCurrency()); /* * all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other * fees ( which are created during disbursal but not repaid) @@ -2221,7 +2213,7 @@ private void updateLoanToPreDisbursalState() { if (charge.isOverdueInstallmentCharge()) { charge.setActive(false); } else { - charge.resetToOriginal(loanCurrency()); + charge.resetToOriginal(getCurrency()); } } List installments = getRepaymentScheduleInstallments(); @@ -2354,7 +2346,7 @@ public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycl loanTransaction.updateLoan(this); - if (loanTransaction.isNotZero(loanCurrency())) { + if (loanTransaction.isNotZero()) { addLoanTransaction(loanTransaction); } updateLoanSummaryDerivedFields(); @@ -2371,7 +2363,7 @@ public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fin } if (loanTransaction.isRecoveryRepayment() - && loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) { + && loanTransaction.getAmount(getCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) { final String errorMessage = "The transaction amount cannot greater than the remaining written off amount."; throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage); } @@ -2380,7 +2372,7 @@ public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fin final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction); - if (loanTransaction.isNotZero(loanCurrency())) { + if (loanTransaction.isNotZero()) { addLoanTransaction(loanTransaction); } @@ -2400,12 +2392,12 @@ public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fin if (loanTransaction.isInterestWaiver()) { Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan(); if (adjustedTransaction != null) { - totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loanCurrency())); + totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(getCurrency())); } - if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) { + if (loanTransaction.getAmount(getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) { final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan."; throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage, - loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount()); + loanTransaction.getAmount(getCurrency()), totalInterestOutstandingOnLoan.getAmount()); } } @@ -2450,17 +2442,7 @@ public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fin && !getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); - } - /* - * Commented since throwing exception if external id present for one of the transactions. for this need to - * save the reversed transactions first and then new transactions. - */ - this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values()); + reprocessTransactions(); } updateLoanSummaryDerivedFields(); @@ -2531,7 +2513,7 @@ public boolean doPostLoanTransactionChecks(final LocalDate transactionDate, fina // FIXME - kw - update account balance to negative amount. handleLoanOverpayment(transactionDate, loanLifecycleStateMachine); statusChanged = true; - } else if (this.summary.isRepaidInFull(loanCurrency())) { + } else if (this.summary.isRepaidInFull(getCurrency())) { handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine); statusChanged = true; } else { @@ -2645,7 +2627,7 @@ private LocalDate getEarliestUnpaidInstallmentDate() { LocalDate lastTransactionDate = null; for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepaymentLikeType() && transaction.isNonZero()) { + if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) { lastTransactionDate = transaction.getTransactionDate(); } } @@ -2669,7 +2651,7 @@ public LoanTransaction deriveDefaultInterestWaiverTransaction() { List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency()); + final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency()); if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate()) && scheduledRepayment.isNotFullyPaidOff() && outstandingForPeriod.isGreaterThanZero()) { transactionDate = scheduledRepayment.getDueDate(); @@ -2697,11 +2679,7 @@ public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLif if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), - getActiveCharges()); - updateLoanSummaryDerivedFields(); - return changedTransactionDetail; + return reprocessTransactions(); } public LoanTransaction findWriteOffTransaction() { @@ -2718,7 +2696,7 @@ public boolean isOverPaid() { private Money calculateTotalOverpayment() { Money totalPaidInRepayments = getTotalPaidInRepayments(); - final MonetaryCurrency currency = loanCurrency(); + final MonetaryCurrency currency = getCurrency(); Money cumulativeTotalPaidOnInstallments = Money.zero(currency); Money cumulativeTotalWaivedOnInstallments = Money.zero(currency); List installments = getRepaymentScheduleInstallments(); @@ -2759,10 +2737,6 @@ public Money calculateTotalRecoveredPayments() { return getTotalRecoveredPayments(); } - public MonetaryCurrency loanCurrency() { - return this.loanRepaymentScheduleDetail.getCurrency(); - } - public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, final AppUser currentUser, final ScheduleGeneratorDTO scheduleGeneratorDTO) { @@ -2822,7 +2796,7 @@ public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, fin } addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(), + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); updateLoanSummaryDerivedFields(); @@ -2844,14 +2818,7 @@ private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO s if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); - addLoanTransaction(mapEntry.getValue()); - } - updateLoanSummaryDerivedFields(); + reprocessTransactions(); LocalDate lastLoanTransactionDate = getLatestTransactionDate(); doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); } @@ -2904,7 +2871,7 @@ public ChangedTransactionDetail close(final JsonCommand command, final LoanLifec LoanTransaction loanTransaction = null; if (isOpen()) { - final Money totalOutstanding = this.summary.getTotalOutstanding(loanCurrency()); + final Money totalOutstanding = this.summary.getTotalOutstanding(getCurrency()); if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) { this.closedOnDate = closureDate; @@ -2923,7 +2890,7 @@ public ChangedTransactionDetail close(final JsonCommand command, final LoanLifec } addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(), + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); updateLoanSummaryDerivedFields(); @@ -3123,11 +3090,11 @@ public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate } private Money getTotalPaidInRepayments() { - Money cumulativePaid = Money.zero(loanCurrency()); + Money cumulativePaid = Money.zero(getCurrency()); for (final LoanTransaction repayment : this.loanTransactions) { if (repayment.isRepaymentLikeType() && !repayment.isReversed()) { - cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency())); + cumulativePaid = cumulativePaid.plus(repayment.getAmount(getCurrency())); } } @@ -3135,11 +3102,11 @@ private Money getTotalPaidInRepayments() { } public Money getTotalRecoveredPayments() { - Money cumulativePaid = Money.zero(loanCurrency()); + Money cumulativePaid = Money.zero(getCurrency()); for (final LoanTransaction recoveredPayment : this.loanTransactions) { if (recoveredPayment.isRecoveryRepayment()) { - cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(loanCurrency())); + cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(getCurrency())); } } return cumulativePaid; @@ -3148,16 +3115,16 @@ public Money getTotalRecoveredPayments() { public Money getTotalPrincipalOutstandingUntil(LocalDate date) { return getRepaymentScheduleInstallments().stream() .filter(installment -> installment.getDueDate().isBefore(date) || installment.getDueDate().isEqual(date)) - .map(installment -> installment.getPrincipalOutstanding(loanCurrency())).reduce(Money.zero(loanCurrency()), Money::add); + .map(installment -> installment.getPrincipalOutstanding(getCurrency())).reduce(Money.zero(getCurrency()), Money::add); } private Money getTotalInterestOutstandingOnLoan() { - Money cumulativeInterest = Money.zero(loanCurrency()); + Money cumulativeInterest = Money.zero(getCurrency()); List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency())); + cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency())); } return cumulativeInterest; @@ -3169,7 +3136,7 @@ private Money getTotalInterestOverdueOnLoan() { List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency()); + final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency()); if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate())) { cumulativeInterestOverdue = cumulativeInterestOverdue.plus(interestOutstandingForPeriod); } @@ -3515,7 +3482,7 @@ public Money getReceivableInterest(final LocalDate tillDate) { && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { if (transaction.isAccrual()) { receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency())); - } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) { + } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency())); } } @@ -3841,8 +3808,7 @@ public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTra public LocalDate getLastUserTransactionDate() { LocalDate currentTransactionDate = getDisbursementDate(); for (final LoanTransaction previousTransaction : this.loanTransactions) { - if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting() - || previousTransaction.isAccrualActivity()) + if (!(previousTransaction.isReversed() || previousTransaction.isAccrualRelated() || previousTransaction.isIncomePosting()) && DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) { currentTransactionDate = previousTransaction.getTransactionDate(); } @@ -4131,17 +4097,7 @@ public ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final regenerateRepaymentSchedule(scheduleGeneratorDTO); } - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), - getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); - addLoanTransaction(mapEntry.getValue()); - } - - return changedTransactionDetail; + return reprocessTransactions(); } public BigDecimal getPrincipalAmountForRepaymentSchedule() { @@ -4219,7 +4175,7 @@ public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Sch } else { regenerateRepaymentSchedule(generatorDTO); } - return processTransactions(); + return reprocessTransactions(); } @@ -4229,33 +4185,13 @@ public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Sch } else { regenerateRepaymentSchedule(generatorDTO); } - return processTransactions(); + return reprocessTransactions(); } public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) { regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); - return processTransactions(); - - } - - public ChangedTransactionDetail processTransactions() { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), - getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); - } - /* - * Commented since throwing exception if external id present for one of the transactions. for this need to save - * the reversed transactions first and then new transactions. - */ - this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values()); - updateLoanSummaryDerivedFields(); - - return changedTransactionDetail; + return reprocessTransactions(); } public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) { @@ -4417,17 +4353,17 @@ public BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, B } private OutstandingAmountsDTO getTotalOutstandingOnLoan() { - Money totalPrincipal = Money.zero(loanCurrency()); - Money totalInterest = Money.zero(loanCurrency()); - Money feeCharges = Money.zero(loanCurrency()); - Money penaltyCharges = Money.zero(loanCurrency()); + Money totalPrincipal = Money.zero(getCurrency()); + Money totalInterest = Money.zero(getCurrency()); + Money feeCharges = Money.zero(getCurrency()); + Money penaltyCharges = Money.zero(getCurrency()); final Set compoundingDetails = null; List repaymentSchedule = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) { - totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loanCurrency())); - totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency())); - feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency())); - penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency())); + totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(getCurrency())); + totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency())); + feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(getCurrency())); + penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(getCurrency())); } return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest) .feeCharges(feeCharges).penaltyCharges(penaltyCharges); @@ -4533,6 +4469,16 @@ public LoanRepaymentScheduleInstallment getRepaymentScheduleInstallment( return getRepaymentScheduleInstallments().stream().filter(predicate).findFirst().orElse(null); } + /** + * @param predicate + * filter of the installments + * @return the installments matching the filter + **/ + public List getRepaymentScheduleInstallments( + @NotNull Predicate predicate) { + return getRepaymentScheduleInstallments().stream().filter(predicate).toList(); + } + /** * @return loan disbursement data **/ @@ -4691,17 +4637,18 @@ public Boolean shouldCreateStandingInstructionAtDisbursement() { return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement; } - public Collection getLoanCharges(LocalDate dueDate) { - Collection loanCharges = new ArrayList<>(); - - for (LoanCharge loanCharge : charges) { - - if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) { - loanCharges.add(loanCharge); - } - } + public List getLoanChargesByDueDate(LocalDate dueDate) { + return getLoanCharges( + loanCharge -> loanCharge.getDueLocalDate() != null && DateUtils.isEqual(loanCharge.getDueLocalDate(), dueDate)); + } - return loanCharges; + /** + * @param predicate + * filter of the charges + * @return the loan charges matching the filter + **/ + public List getLoanCharges(@NotNull Predicate predicate) { + return getLoanCharges().stream().filter(predicate).toList(); } public void setGuaranteeAmount(BigDecimal guaranteeAmountDerived) { @@ -4796,7 +4743,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage); } - if (loanTransaction.isNotZero(loanCurrency())) { + if (loanTransaction.isNotZero()) { addLoanTransaction(loanTransaction); } if (loanTransaction.isNotRefundForActiveLoan()) { @@ -4828,13 +4775,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); } else { - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), - allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(this); - } - + reprocessTransactions(); } updateLoanSummaryDerivedFields(); @@ -4869,7 +4810,7 @@ public LocalDate possibleNextRefundDate() { LocalDate lastTransactionDate = null; for (final LoanTransaction transaction : this.loanTransactions) { if ((transaction.isRepaymentLikeType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund()) - && transaction.isNonZero() && transaction.isNotReversed()) { + && transaction.isGreaterThanZero() && transaction.isNotReversed()) { lastTransactionDate = transaction.getTransactionDate(); } } @@ -4979,7 +4920,7 @@ private void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursement charge.setActive(false); } else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate() .equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) { - charge.resetToOriginal(loanCurrency()); + charge.resetToOriginal(getCurrency()); } } this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(disbursementDetail.principal())); @@ -5461,8 +5402,7 @@ public List getSupportedInterestRefundTransactionTypes() { public LoanTransaction getLastUserTransaction() { return getLoanTransactions().stream() // - .filter(LoanTransaction::isNotReversed) // - .filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) // + .filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) // .reduce((first, second) -> second) // .orElse(null); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 9e3dcfdb1b7..9058b7f778c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -28,6 +28,7 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -40,7 +41,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; +import java.util.function.Predicate; +import lombok.Getter; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; @@ -58,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; +@Getter @Entity @Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") }) public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { @@ -305,10 +308,6 @@ public Money waive(final MonetaryCurrency currency, final Integer loanInstallmen } - public BigDecimal getAmountPercentageAppliedTo() { - return this.amountPercentageAppliedTo; - } - private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) { return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount(); } @@ -509,15 +508,7 @@ private static boolean isGreaterThanZero(final BigDecimal value) { } public LocalDate getDueLocalDate() { - return this.dueDate; - } - - public LocalDate getDueDate() { - return this.dueDate; - } - - public LocalDate getSubmittedOnDate() { - return submittedOnDate; + return this.dueDate; // TODO delete duplicated method } private boolean determineIfFullyPaid() { @@ -597,11 +588,11 @@ private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { } public BigDecimal amount() { - return this.amount; + return this.amount; // TODO delete duplicated method } public BigDecimal amountOutstanding() { - return this.amountOutstanding; + return this.amountOutstanding; // TODO delete duplicated method } public Money getAmountOutstanding(final MonetaryCurrency currency) { @@ -644,14 +635,6 @@ public boolean isWaived() { return this.waived; } - public BigDecimal getMinCap() { - return this.minCap; - } - - public BigDecimal getMaxCap() { - return this.maxCap; - } - public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) { final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency)); @@ -729,10 +712,6 @@ public String currencyCode() { return this.charge.getCurrencyCode(); } - public Charge getCharge() { - return this.charge; - } - /* * @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return * true; } if (obj.getClass() != getClass()) { return false; } final LoanCharge rhs = (LoanCharge) obj; return new @@ -756,10 +735,6 @@ public ChargeCalculationType getChargeCalculation() { return ChargeCalculationType.fromInt(this.chargeCalculation); } - public BigDecimal getPercentage() { - return this.percentage; - } - public void updateAmount(final BigDecimal amount) { this.amount = amount; calculateOutstanding(); @@ -851,7 +826,7 @@ public void setActive(boolean active) { } public BigDecimal amountOrPercentage() { - return this.amountOrPercentage; + return this.amountOrPercentage; // TODO delete duplicated method } public BigDecimal chargeAmount() { @@ -906,12 +881,8 @@ public void updateWaivedAmount(MonetaryCurrency currency) { } - public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() { - return this.overdueInstallmentCharge; - } - public LoanTrancheDisbursementCharge getTrancheDisbursementCharge() { - return this.loanTrancheDisbursementCharge; + return this.loanTrancheDisbursementCharge; // TODO delete duplicated method } public Money undoPaidOrPartiallyAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) { @@ -960,14 +931,6 @@ public LoanInstallmentCharge getLastPaidOrPartiallyPaidInstallmentLoanCharge(Mon return paidChargePerInstallment; } - public Set getLoanChargePaidBySet() { - return this.loanChargePaidBySet; - } - - public Loan getLoan() { - return this.loan; - } - public boolean isDisbursementCharge() { return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT); } @@ -988,10 +951,6 @@ public void undoWaived() { this.waived = false; } - public ExternalId getExternalId() { - return externalId; - } - public ChargeTimeType getChargeTimeType() { return ChargeTimeType.fromInt(this.chargeTime); } @@ -1013,6 +972,11 @@ public LocalDate getEffectiveDueDate() { return dueDate; } + @NotNull + public List getLoanChargePaidBy(@NotNull Predicate filter) { + return getLoanChargePaidBySet().stream().filter(filter).toList(); + } + public LoanChargeData toData() { EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(), String.valueOf(getChargeTimeType().getValue())); @@ -1020,8 +984,8 @@ public LoanChargeData toData() { getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue())); EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(), String.valueOf(getChargePaymentMode().getValue())); - Set loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData) - .collect(Collectors.toSet()); + List loanInstallmentChargeDataList = installmentCharges().stream().map(LoanInstallmentCharge::toData) + .toList(); return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName()) .currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived) @@ -1029,6 +993,6 @@ public LoanChargeData toData() { .submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage) .amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge) .chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap) - .installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build(); + .installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build(); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java index 5da30a483cc..57783668d37 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java @@ -183,7 +183,7 @@ public Integer getCompoundingFrequencyOnDay() { } public boolean isCompoundingToBePostedAsTransaction() { - return null == this.isCompoundingToBePostedAsTransaction ? false : this.isCompoundingToBePostedAsTransaction; + return this.isCompoundingToBePostedAsTransaction != null && this.isCompoundingToBePostedAsTransaction; } public boolean allowCompoundingOnEod() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 51a3dacbd52..00e2687e77f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -34,6 +34,7 @@ import lombok.Getter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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.loanproduct.domain.AllocationType; @@ -692,29 +693,32 @@ public boolean isOverdueOn(final LocalDate date) { public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff, final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) { - this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.getAmount()); - this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.getAmount()); - this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.getAmount()); - this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.getAmount()); - this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount()); - this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount()); + this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesDue)); + this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWaived)); + this.feeChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWrittenOff)); + this.penaltyCharges = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesDue)); + this.penaltyChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWaived)); + this.penaltyChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWrittenOff)); } public void addToChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff, final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) { - this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount()); - this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount()); - this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount()); - this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount()); - this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount()); - this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount()); + this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesDue), this.feeChargesCharged)); + this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWaived), this.feeChargesWaived)); + this.feeChargesWrittenOff = MathUtil + .zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWrittenOff), this.feeChargesWrittenOff)); + this.penaltyCharges = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesDue), this.penaltyCharges)); + this.penaltyChargesWaived = MathUtil + .zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWaived), this.penaltyChargesWaived)); + this.penaltyChargesWrittenOff = MathUtil + .zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWrittenOff), this.penaltyChargesWrittenOff)); checkIfRepaymentPeriodObligationsAreMet(getObligationsMetOnDate(), feeChargesDue.getCurrency()); } public void updateAccrualPortion(final Money interest, final Money feeCharges, final Money penalityCharges) { - this.interestAccrued = defaultToNullIfZero(interest.getAmount()); - this.feeAccrued = defaultToNullIfZero(feeCharges.getAmount()); - this.penaltyAccrued = defaultToNullIfZero(penalityCharges.getAmount()); + this.interestAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(interest)); + this.feeAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeCharges)); + this.penaltyAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(penalityCharges)); } public void updateObligationsMet(final MonetaryCurrency currency, final LocalDate transactionDate) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 643c114f1b4..c14074199ff 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -253,6 +253,15 @@ public static boolean isInPeriod(LocalDate targetDate, LocalDate fromDate, Local : DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate); } + public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) { + LocalDate fromDate = installment.getFromDate(); + return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate); + } + + public static boolean isAfterPeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment) { + return DateUtils.isAfter(targetDate, installment.getDueDate()); + } + public static Optional findInPeriod(LocalDate targetDate, List installments) { int firstNumber = fetchFirstNormalInstallmentNumber(installments); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 07263eeb7ae..0ddebb1a68c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -101,6 +101,18 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ALL_LOAN_IDS_BY_STATUS_ID = "SELECT loan.id FROM Loan loan WHERE loan.loanStatus = :statusId"; + String LOANS_FOR_ACCRUAL = "select l from Loan l left join l.loanInterestRecalculationDetails recalcDetails " + + "where l.loanStatus = 300 and l.isNpa = false and l.chargedOff = false " + + "and l.loanProduct.accountingRule = :accountingType " + + "and (recalcDetails.isCompoundingToBePostedAsTransaction is null or recalcDetails.isCompoundingToBePostedAsTransaction = false) " + + "and (exists (select ls.id from LoanRepaymentScheduleInstallment ls where ls.loan.id = l.id and ls.isDownPayment = false " + + "and ((coalesce(ls.interestCharged, 0) - coalesce(ls.interestWaived, 0)) <> coalesce(ls.interestAccrued, 0) " + + "or (coalesce(ls.feeChargesCharged, 0) - coalesce(ls.feeChargesWaived, 0)) <> coalesce(ls.feeAccrued, 0) " + + "or (coalesce(ls.penaltyCharges, 0) - coalesce(ls.penaltyChargesWaived, 0)) <> coalesce(ls.penaltyAccrued, 0)) "; + String FIND_LOANS_FOR_PERIODIC_ACCRUAL = LOANS_FOR_ACCRUAL + + "and (:futureCharges = true or ls.fromDate < :tillDate or (ls.installmentNumber = (select min(lsi.installmentNumber) from LoanRepaymentScheduleInstallment lsi where lsi.loan.id = l.id and lsi.isDownPayment = false) and ls.fromDate = :tillDate))))"; + String FIND_LOANS_FOR_ADD_ACCRUAL = LOANS_FOR_ACCRUAL + "and (:futureCharges = true or ls.dueDate <= :tillDate)))"; + @Query(FIND_GROUP_LOANS_DISBURSED_AFTER) List getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -227,4 +239,12 @@ List findAllNonClosedLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanI @Query(FIND_ALL_LOAN_IDS_BY_STATUS_ID) List findLoanIdByStatusId(@Param("statusId") Integer statusId); + + @Query(FIND_LOANS_FOR_PERIODIC_ACCRUAL) + List findLoansForPeriodicAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate, + @Param("futureCharges") boolean futureCharges); + + @Query(FIND_LOANS_FOR_ADD_ACCRUAL) + List findLoansForAddAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate, + @Param("futureCharges") boolean futureCharges); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 959d71eda26..3c0f7526e01 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -263,4 +263,12 @@ public List findLoanIdsByStatusId(Integer statusId) { return repository.findLoanIdByStatusId(statusId); } + public List findLoansForPeriodicAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) { + return repository.findLoansForPeriodicAccrual(accountingType, tillDate, futureCharges); + } + + public List findLoansForAddAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) { + return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges); + } + } 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 98b5166c817..09f5d3b53b8 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 @@ -252,26 +252,29 @@ public static LoanTransaction waiver(final Office office, final Loan loan, final public static LoanTransaction accrueInterest(final Office office, final Loan loan, final Money amount, final LocalDate interestAppliedDate, final ExternalId externalId) { - BigDecimal principalPortion = null; - BigDecimal feesPortion = null; - BigDecimal penaltiesPortion = null; BigDecimal interestPortion = amount.getAmount(); - BigDecimal overPaymentPortion = null; - boolean reversed = false; - PaymentDetail paymentDetail = null; - return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), interestAppliedDate, interestPortion, - principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId); + return accrueTransaction(loan, office, interestAppliedDate, interestPortion, interestPortion, null, null, externalId); + } + + public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, + final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) { + final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate, + externalId); + applyCharge.updateChargesComponents(feeCharges, penaltyCharges); + return applyCharge; } public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final ExternalId externalId) { - BigDecimal principalPortion = null; - BigDecimal overPaymentPortion = null; - boolean reversed = false; - PaymentDetail paymentDetail = null; - return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, principalPortion, interestPortion, - feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId); + return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, null, interestPortion, + feeChargesPortion, penaltyChargesPortion, null, false, null, externalId); + } + + public static LoanTransaction accrualAdjustment(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, + final BigDecimal interestPortion, final BigDecimal feePortion, final BigDecimal penaltyPortion, final ExternalId externalId) { + return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue(), dateOf, amount, null, interestPortion, + feePortion, penaltyPortion, null, false, null, externalId); } public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate, @@ -325,14 +328,6 @@ public LoanTransaction copyTransactionPropertiesAndMappings() { return newTransaction; } - public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, - final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) { - final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate, - externalId); - applyCharge.updateChargesComponents(feeCharges, penaltyCharges); - return applyCharge; - } - public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate, final ExternalId externalId, PaymentDetail paymentDetail) { return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(), @@ -492,47 +487,41 @@ public void updateLoan(final Loan loan) { * penaltyCharges */ public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) { - final MonetaryCurrency currency = principal.getCurrency(); - this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal).getAmount()); - this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount()); + this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), MathUtil.toBigDecimal(principal))); + this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest))); updateChargesComponents(feeCharges, penaltyCharges); } public void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) { - final MonetaryCurrency currency = feeCharges.getCurrency(); - this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount()); - this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount()); + this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges))); + this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges))); } private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges, final Money unrecognizedCharges) { - final MonetaryCurrency currency = feeCharges.getCurrency(); - this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount()); - this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount()); - this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedCharges).getAmount()); + this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges))); + this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges))); + this.unrecognizedIncomePortion = MathUtil + .zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedCharges))); } private void updateInterestComponent(final Money interest, final Money unrecognizedInterest) { - final MonetaryCurrency currency = interest.getCurrency(); - this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount()); - this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedInterest).getAmount()); + this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest))); + this.unrecognizedIncomePortion = MathUtil + .zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedInterest))); } - public void adjustInterestComponent(final MonetaryCurrency currency) { - this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).minus(getUnrecognizedIncomePortion(currency)).getAmount()); + public void adjustInterestComponent() { + this.interestPortion = MathUtil.zeroToNull(MathUtil.subtract(getInterestPortion(), getUnrecognizedIncomePortion())); } public void updateComponentsAndTotal(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) { updateComponents(principal, interest, feeCharges, penaltyCharges); - - final MonetaryCurrency currency = principal.getCurrency(); - this.amount = getPrincipalPortion(currency).plus(getInterestPortion(currency)).plus(getFeeChargesPortion(currency)) - .plus(getPenaltyChargesPortion(currency)).getAmount(); + this.amount = MathUtil + .nullToZero(MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion())); } public void setOverPayments(final Money overPayment) { - if (overPayment != null) { - this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount()); - } + this.overPaymentPortion = MathUtil.zeroToNull(MathUtil.toBigDecimal(overPayment)); } public Money getPrincipalPortion(final MonetaryCurrency currency) { @@ -720,48 +709,20 @@ public boolean isReAmortize() { return getTypeOf().isReAmortize() && isNotReversed(); } - public boolean isAccrualActivity() { - return getTypeOf().isAccrualActivity(); - } - - public boolean isIdentifiedBy(final Long identifier) { - return getId().equals(identifier); - } - - public boolean isBelongingToLoanOf(final Loan check) { - return this.loan.getId().equals(check.getId()); - } - - public boolean isNotBelongingToLoanOf(final Loan check) { - return !isBelongingToLoanOf(check); - } - - public boolean isNonZero() { - return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0; - } - public boolean isGreaterThan(final Money monetaryAmount) { - return getAmount(monetaryAmount.getCurrency()).isGreaterThan(monetaryAmount); + return MathUtil.isGreaterThan(amount, MathUtil.toBigDecimal(monetaryAmount)); } - public boolean isGreaterThanZero(final MonetaryCurrency currency) { - return getAmount(currency).isGreaterThanZero(); + public boolean isGreaterThanZero() { + return MathUtil.isGreaterThanZero(amount); } public boolean isGreaterThanZeroAndLessThanOrEqualTo(BigDecimal totalOverpaid) { - return isNonZero() && this.amount.compareTo(totalOverpaid) <= 0; - } - - public boolean isNotZero(final MonetaryCurrency currency) { - return !getAmount(currency).isZero(); + return isGreaterThanZero() && !MathUtil.isGreaterThan(amount, totalOverpaid); } - private BigDecimal defaultToNullIfZero(final BigDecimal value) { - BigDecimal result = value; - if (BigDecimal.ZERO.compareTo(value) == 0) { - result = null; - } - return result; + public boolean isNotZero() { + return !MathUtil.isEmpty(amount); } public LoanTransactionData toData(final CurrencyData currencyData, final AccountTransferData transfer) { @@ -785,7 +746,7 @@ public Map toMapData(final String currencyCode) { thisTransactionData.put("id", getId()); thisTransactionData.put("officeId", this.office.getId()); thisTransactionData.put("type", transactionType); - thisTransactionData.put("reversed", Boolean.valueOf(isReversed())); + thisTransactionData.put("reversed", isReversed()); thisTransactionData.put("date", getTransactionDate()); thisTransactionData.put("currencyCode", currencyCode); thisTransactionData.put("amount", this.amount); @@ -871,16 +832,37 @@ public void updateExternalId(final ExternalId externalId) { } public boolean isAccrual() { - return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed(); + return getTypeOf().isAccrual(); + } + + public boolean isAccrualAdjustment() { + return getTypeOf().isAccrualAdjustment(); + } + + public boolean isAccrualActivity() { + return getTypeOf().isAccrualActivity(); + } + + public boolean isAccrualRelated() { + return isAccrual() || isAccrualAdjustment() || isAccrualActivity(); + } + + public boolean isWaiveCharge() { + return getTypeOf().isWaiveCharges(); + } + + public boolean isWaiveInterest() { + return getTypeOf().isWaiveInterest(); } public boolean isNonMonetaryTransaction() { - return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf()) - || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.ACCRUAL.equals(getTypeOf()) - || LoanTransactionType.ACCRUAL_ACTIVITY.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) - || LoanTransactionType.REAMORTIZE.equals(getTypeOf()) || LoanTransactionType.REAGE.equals(getTypeOf())); + LoanTransactionType type = getTypeOf(); + return isNotReversed() && (type == LoanTransactionType.CONTRA || type == LoanTransactionType.MARKED_FOR_RESCHEDULING + || type == LoanTransactionType.ACCRUAL || type == LoanTransactionType.ACCRUAL_ADJUSTMENT + || type == LoanTransactionType.ACCRUAL_ACTIVITY || type == LoanTransactionType.APPROVE_TRANSFER + || type == LoanTransactionType.INITIATE_TRANSFER || type == LoanTransactionType.REJECT_TRANSFER + || type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF + || type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE); } public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) { @@ -985,11 +967,7 @@ public Set getLoanTransactionToRepaym } public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() { - return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment() || isAccrualActivity(); - } - - public boolean isAccrualTransaction() { - return isAccrual(); + return isNotReversed() && (isDisbursement() || isAccrualRelated() || isRepaymentAtDisbursement() || isRepayment()); } public Money getOutstandingLoanBalanceMoney(final MonetaryCurrency currency) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java index c879ec62f83..ec699382a13 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java @@ -24,12 +24,12 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; 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.loanproduct.domain.AllocationType; @Entity @Table(name = "m_loan_transaction_repayment_schedule_mapping") @@ -86,21 +86,13 @@ private static BigDecimal defaultToNullIfZero(final Money value) { return (value == null || value.isZero()) ? null : value.getAmount(); } - private BigDecimal defaultToZeroIfNull(final BigDecimal value) { - BigDecimal result = value; - if (value == null) { - result = BigDecimal.ZERO; - } - return result; - } - public LoanRepaymentScheduleInstallment getLoanRepaymentScheduleInstallment() { return this.installment; } - public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges, - @NotNull Money penaltyCharges) { - updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount()); + public void updateComponents(Money principal, Money interest, Money feeCharges, Money penaltyCharges) { + updateComponents(MathUtil.toBigDecimal(principal), MathUtil.toBigDecimal(interest), MathUtil.toBigDecimal(feeCharges), + MathUtil.toBigDecimal(penaltyCharges)); } void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, @@ -112,8 +104,7 @@ void updateComponents(final BigDecimal principal, final BigDecimal interest, fin } private void updateAmount() { - this.amount = defaultToZeroIfNull(getPrincipalPortion()).add(defaultToZeroIfNull(getInterestPortion())) - .add(defaultToZeroIfNull(getFeeChargesPortion())).add(defaultToZeroIfNull(getPenaltyChargesPortion())); + this.amount = MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion()); } public void setComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, @@ -146,6 +137,15 @@ public Money getPenaltyChargesPortion(final MonetaryCurrency currency) { return Money.of(currency, this.penaltyChargesPortion); } + public BigDecimal getPortion(AllocationType allocationType) { + return switch (allocationType) { + case PRINCIPAL -> getPrincipalPortion(); + case INTEREST -> getInterestPortion(); + case FEE -> getFeeChargesPortion(); + case PENALTY -> getPenaltyChargesPortion(); + }; + } + public LoanTransaction getLoanTransaction() { return loanTransaction; } 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 2af4b1d9076..967f24a2f75 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 @@ -66,6 +66,7 @@ public enum LoanTransactionType { INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), // ACCRUAL_ACTIVITY(32, "loanTransactionType.accrualActivity"), // INTEREST_REFUND(33, "loanTransactionType.interestRefund"), // + ACCRUAL_ADJUSTMENT(34, "loanTransactionType.accrualAdjustment"), // ; private final Integer value; @@ -115,6 +116,7 @@ public static LoanTransactionType fromInt(final Integer transactionType) { case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER; case 32 -> LoanTransactionType.ACCRUAL_ACTIVITY; case 33 -> LoanTransactionType.INTEREST_REFUND; + case 34 -> LoanTransactionType.ACCRUAL_ADJUSTMENT; default -> LoanTransactionType.INVALID; }; } @@ -228,4 +230,7 @@ public boolean isInterestRefund() { return this.equals(LoanTransactionType.INTEREST_REFUND); } + public boolean isAccrualAdjustment() { + return this == LoanTransactionType.ACCRUAL_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 e31f6c11358..d12449b894b 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 @@ -45,14 +45,14 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem totalInterest = totalInterest.plus(installment.getInterestCharged(currency)); totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } - LoanChargePaidBy accrualBy = null; + List accruals = null; if (loanCharge.isSpecifiedDueDate()) { LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, installments); if (addedPeriod != null) { addedPeriod.updateObligationsMet(currency, disbursementDate); } - accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst() - .orElse(null); + accruals = loanCharge.getLoanChargePaidBySet().stream().filter(e -> !e.getLoanTransaction().isReversed() + && (e.getLoanTransaction().isAccrual() || e.getLoanTransaction().isAccrualAdjustment())).toList(); } LocalDate startDate = disbursementDate; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); @@ -61,7 +61,8 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem continue; } boolean installmentChargeApplicable = !installment.isRecalculatedInterestComponent(); - boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); + Integer installmentNumber = installment.getInstallmentNumber(); + boolean isFirstPeriod = installmentNumber.equals(firstNormalInstallmentNumber); Predicate feePredicate = e -> e.isFeeCharge() && !e.isDueAtDisbursement(); LocalDate dueDate = installment.getDueDate(); final Money feeChargesDue = calcChargeDue(startDate, dueDate, loanCharge, currency, installment, totalPrincipal, totalInterest, @@ -82,13 +83,19 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem installment.addToChargePortion(feeChargesDue, feeChargesWaived, feeChargesWrittenOff, penaltyChargesDue, penaltyChargesWaived, penaltyChargesWrittenOff); - if (accrualBy != null && installment.isAdditional() && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) { - Money amount = Money.of(currency, accrualBy.getAmount()); + if (accruals != null && !accruals.isEmpty() && installment.isAdditional() + && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) { + BigDecimal amount = null; + for (LoanChargePaidBy accrual : accruals) { + accrual.setInstallmentNumber(installmentNumber); + amount = accrual.getLoanTransaction().isAccrual() ? MathUtil.add(amount, accrual.getAmount()) + : MathUtil.subtract(amount, accrual.getAmount()); + } + Money accruedAmount = Money.of(currency, MathUtil.negativeToZero(amount)); boolean isFee = loanCharge.isFeeCharge(); installment.updateAccrualPortion(installment.getInterestAccrued(currency), - MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? amount : null)), - MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : amount))); - accrualBy.setInstallmentNumber(installment.getInstallmentNumber()); + MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? accruedAmount : null)), + MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : accruedAmount))); } startDate = dueDate; } 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 3b40c7cb3a9..059f93e5815 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 @@ -166,7 +166,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur // pass through for new transactions if (loanTransaction.getId() == null) { processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null)); - loanTransaction.adjustInterestComponent(currency); + loanTransaction.adjustInterestComponent(); } else { /** * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) @@ -178,7 +178,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur // re-process transaction processLatestTransaction(newLoanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null)); - newLoanTransaction.adjustInterestComponent(currency); + newLoanTransaction.adjustInterestComponent(); /** * Check if the transaction amounts have changed. If so, reverse the original transaction and update * changedTransactionDetail accordingly 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 916ebfef6c1..15a2abf52bb 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 @@ -18,6 +18,10 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -2805,4 +2809,36 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency cu .feeCharges(feeCharges) // .penaltyCharges(penaltyCharges); } + + @Override + public Money getPeriodInterestTillDate(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, + @NotNull LocalDate targetDate) { + if (isBeforePeriod(targetDate, installment, false)) { + return null; + } + MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); + BigDecimal interest = installment.getInterestCharged(); + Money zero = Money.zero(currency); + if (MathUtil.isEmpty(interest)) { + return zero; + } + if (isAfterPeriod(targetDate, installment) || DateUtils.isEqual(targetDate, installment.getDueDate())) { + return installment.getInterestCharged(currency); + } + + BigDecimal interestPortion; + LocalDate startDate = installment.getFromDate(); + LocalDate dueDate = installment.getDueDate(); + if (DateUtils.isBefore(startDate, loan.getInterestChargedFromDate())) { + startDate = loan.getInterestChargedFromDate(); + } + if (!DateUtils.isBefore(startDate, dueDate) || !DateUtils.isBefore(startDate, targetDate)) { + return zero; + } + int totalNumberOfDays = DateUtils.getExactDifferenceInDays(startDate, dueDate); + int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, targetDate); + double interestPerDay = interest.doubleValue() / totalNumberOfDays; + interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + return Money.of(currency, interestPortion); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 9cb841f5ffc..295a55e4c0f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -18,14 +18,17 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import jakarta.validation.constraints.NotNull; import java.math.MathContext; import java.time.LocalDate; import java.util.Set; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; @@ -42,4 +45,6 @@ OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, Local MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + Money getPeriodInterestTillDate(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, + @NotNull LocalDate targetDate); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java index cf32cbf7635..807203beadb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java @@ -18,30 +18,31 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -import java.util.Collection; +import java.util.List; import org.apache.fineract.infrastructure.core.exception.MultiException; -import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; public interface LoanAccrualsProcessingService { - void addPeriodicAccruals(LocalDate tilldate) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tilldate) throws MultiException; - void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tilldate, @NotNull Loan loan) throws MultiException; - void addAccrualAccounting(Long loanId, Collection loanScheduleAccrualDatas) throws Exception; + void addAccruals(@NotNull LocalDate tilldate) throws MultiException; - void addIncomeAndAccrualTransactions(Long loanId) throws Exception; + void reprocessExistingAccruals(@NotNull Loan loan); - void reprocessExistingAccruals(Loan loan); + void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled); - void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled); + void processIncomePostingAndAccruals(@NotNull Loan loan); - void processIncomePostingAndAccruals(Loan loan); + void addIncomeAndAccrualTransactions(Long loanId) throws Exception; - void processAccrualsForLoanClosure(Loan loan); + void processAccrualsForLoanClosure(@NotNull Loan loan); - void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection newAccrualTransactions); + void processAccrualsForLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate, + @NotNull List newAccrualTransactions); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java new file mode 100644 index 00000000000..d8c12da8312 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.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.loanaccount.service; + +import java.util.Map; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.fund.domain.Fund; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface LoanAssembler { + + Loan assembleFrom(Long accountId); + + Loan assembleFrom(JsonCommand command); + + void setHelpers(Loan loanAccount); + + void accountNumberGeneration(JsonCommand command, Loan loan); + + CodeValue findCodeValueByIdIfProvided(Long codeValueId); + + Fund findFundByIdIfProvided(Long fundId); + + Staff findLoanOfficerByIdIfProvided(Long loanOfficerId); + + Map updateFrom(JsonCommand command, Loan loan); + + Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser); + + Map updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java index fb2588fb441..30e62a15d6d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.util.Collection; +import java.util.List; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.Charge; @@ -38,12 +39,10 @@ public interface LoanChargeReadPlatformService { Collection retrieveLoanChargesForFeePayment(Integer paymentMode, Integer loanStatus); - Collection retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges); + List retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges); Collection retrieveOverdueInstallmentChargeFrequencyNumber(Loan loan, Charge charge, Integer periodNumber); - Collection retrieveLoanChargesForAccrual(Long loanId); - Collection retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType, Integer installmentNumber); 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 4db9c5cdcc2..acf6abb04c1 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 @@ -323,6 +323,8 @@ public static LoanTransactionEnumData transactionType(final LoanTransactionType LoanTransactionType.ACCRUAL_ACTIVITY.getCode(), "Accrual Activity"); case INTEREST_REFUND -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_REFUND.getValue().longValue(), LoanTransactionType.INTEREST_REFUND.getCode(), "Interest Refund"); + case ACCRUAL_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue().longValue(), + LoanTransactionType.ACCRUAL_ADJUSTMENT.getCode(), "Accrual 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 dc35c20c327..552e608107a 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 @@ -73,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; @@ -95,6 +96,8 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Slf4j @RequiredArgsConstructor @@ -104,6 +107,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy"; public final EMICalculator emiCalculator; + public final LoanRepositoryWrapper loanRepositoryWrapper; public final InterestRefundService interestRefundService; @Override @@ -152,7 +156,7 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu // only for progressive loans public Pair reprocessProgressiveLoanTransactions( - LocalDate disbursementDate, LocalDate currentDate, List loanTransactions, MonetaryCurrency currency, + LocalDate disbursementDate, LocalDate targetDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { @@ -179,8 +183,8 @@ public Pair repr final Loan loan = loanTransactions.get(0).getLoan(); final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); - ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail, - installmentAmountInMultiplesOf, installments, overpaymentHolder.getMoneyObject().getMc()); + ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments, + loanProductRelatedDetail, installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc()); ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); @@ -217,7 +221,7 @@ public Pair repr LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); createNewTransaction(oldTransaction, newTransaction, ctx); } - recalculateInterestForDate(currentDate, ctx); + recalculateInterestForDate(targetDate, ctx); List txs = changeOperations.stream() // .filter(ChangeOperation::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); @@ -225,6 +229,32 @@ public Pair repr return Pair.of(changedTransactionDetail, scheduleModel); } + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) + .getLeft(); + } + + @NotNull + @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + List transactions = loan.retrieveListOfTransactionsForReprocessing(); + MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency(); + List installments = loan.getRepaymentScheduleInstallments(); + Set charges = loan.getActiveCharges(); + return reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), targetDate, transactions, currency, installments, charges) + .getRight(); + } + + @NotNull + private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { + LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); + return newTransaction == null ? transaction : newTransaction; + } + private void processInterestRateChange(final List installments, final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) { final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); @@ -249,29 +279,14 @@ private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInter }); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { - LocalDate currentDate = DateUtils.getBusinessLocalDate(); - return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) - .getLeft(); - } - - @NotNull - private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { - LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); - return newTransaction == null ? transaction : newTransaction; - } - @Override public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { switch (loanTransaction.getTypeOf()) { case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx); - case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); - case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges()); + case WRITEOFF -> handleWriteOff(loanTransaction, ctx); + case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx); case CHARGEBACK -> handleChargeback(loanTransaction, ctx); - case CREDIT_BALANCE_REFUND -> - handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx); case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER -> handleRepayment(loanTransaction, ctx); @@ -281,7 +296,7 @@ public void processLatestTransaction(LoanTransaction loanTransaction, Transactio case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); case REAMORTIZE -> handleReAmortization(loanTransaction, ctx); case REAGE -> handleReAge(loanTransaction, ctx); - case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); + case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx); // TODO: Cover rest of the transaction types default -> { log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); @@ -357,9 +372,14 @@ protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx processCreditTransaction(loanTransaction, ctx); } + protected void handleCreditBalanceRefund(LoanTransaction transaction, TransactionCtx ctx) { + super.handleCreditBalanceRefund(transaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + } + private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) { - return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules() - .stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf()))); + List creditAllocationRules = loanTransaction.getLoan().getCreditAllocationRules(); + return (creditAllocationRules == null || creditAllocationRules.stream() + .noneMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf()))); } protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) { @@ -396,77 +416,76 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); } else { loanTransaction.resetDerivedComponents(); + MonetaryCurrency currency = ctx.getCurrency(); final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); ctx.getInstallments().sort(byDate); - final Money zeroMoney = Money.zero(ctx.getCurrency()); - Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency()); + final Money zeroMoney = Money.zero(currency); + Money transactionAmount = loanTransaction.getAmount(currency); Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject(); - Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid)); + Money amountToDistribute = MathUtil.negativeToZero(transactionAmount).minus(totalOverpaid); Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); loanTransaction.setOverPayments(overpaymentAmount); + if (!transactionAmount.isGreaterThanZero()) { + return; + } + if (!loanTransaction.isChargeback()) { + throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); + } - if (transactionAmount.isGreaterThanZero()) { - if (loanTransaction.isChargeback()) { - LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); - // get the original allocation from the opriginal transaction - Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, - ctx.getCurrency()); - LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); - - // if there were earlier chargebacks then let's calculate the remaining amounts for each portion - Map originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, - originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); - - // calculate the current chargeback allocation - Map chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, - transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); - - loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), - chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); - - final LocalDate transactionDate = loanTransaction.getTransactionDate(); - boolean loanTransactionMapped = false; - LocalDate pastDueDate = null; - for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { - pastDueDate = currentInstallment.getDueDate(); - if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - loanTransactionMapped = true; - break; - - // If already exists an additional installment just update the due date and - // principal from the Loan chargeback / CBR transaction - } else if (currentInstallment.isAdditional()) { - if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { - currentInstallment.updateDueDate(transactionDate); - } - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - loanTransactionMapped = true; - break; - } - } + LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); + // get the original allocation from the opriginal transaction + Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency); + LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); + + // if there were earlier chargebacks then let's calculate the remaining amounts for each portion + Map originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, + originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); + + // calculate the current chargeback allocation + Map chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, + transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency); + + loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), + chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + boolean loanTransactionMapped = false; + LocalDate pastDueDate = null; + for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { + pastDueDate = currentInstallment.getDueDate(); + if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); + loanTransactionMapped = true; + break; - // New installment will be added (N+1 scenario) - if (!loanTransactionMapped) { - if (loanTransaction.getTransactionDate().equals(pastDueDate)) { - LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments() - .get(ctx.getInstallments().size() - 1); - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - } else { - Loan loan = loanTransaction.getLoan(); - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, - (ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), - zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation); - installment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(installment); - } + // If already exists an additional installment just update the due date and + // principal from the Loan chargeback / CBR transaction + } else if (currentInstallment.isAdditional()) { + if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { + currentInstallment.updateDueDate(transactionDate); } - allocateOverpayment(loanTransaction, ctx); + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); + loanTransactionMapped = true; + break; + } + } + + // New installment will be added (N+1 scenario) + if (!loanTransactionMapped) { + if (loanTransaction.getTransactionDate().equals(pastDueDate)) { + LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1); + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); } else { - throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); + Loan loan = loanTransaction.getLoan(); + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, + (ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(), + zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); + recognizeAmountsAfterChargeback(ctx, transactionDate, installment, chargebackAllocation); + installment.markAsAdditional(); + loan.addLoanRepaymentScheduleInstallment(installment); } } + allocateOverpayment(loanTransaction, ctx); } } @@ -511,14 +530,15 @@ private Map adjustOriginalAllocationWithFormerChargebacks return allocation; } - private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate, + private void recognizeAmountsAfterChargeback(TransactionCtx ctx, LocalDate transactionDate, LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { Money principal = chargebackAllocation.get(PRINCIPAL); if (principal.isGreaterThanZero()) { installment.addToCreditedPrincipal(principal.getAmount()); - installment.addToPrincipal(localDate, principal); + installment.addToPrincipal(transactionDate, principal); } + MonetaryCurrency currency = ctx.getCurrency(); Money fee = chargebackAllocation.get(FEE); if (fee.isGreaterThanZero()) { installment.addToCreditedFee(fee.getAmount()); @@ -577,9 +597,8 @@ private Predicate hasMatchingToLoanTransaction(Long id, return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id); } - @Override - protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges) { + protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); Money zero = Money.zero(currency); List transactionMappings = new ArrayList<>(); Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); @@ -604,18 +623,16 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { - transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments, - transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, - transactionMappings, charges, balances); + transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, ctx, transactionAmountUnprocessed, + paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, transactionMappings, balances); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } } } else if (scheduleProcessingType.isVertical()) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { - transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero, - transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances, - paymentAllocationType); + transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, ctx, transactionMappings, + transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } @@ -639,7 +656,7 @@ private void processSingleTransaction(LoanTransaction loanTransaction, final Pro // Reset derived component of new loan transaction and re-process transaction processLatestTransaction(processTransaction, ctx); if (loanTransaction.isInterestWaiver()) { - processTransaction.adjustInterestComponent(ctx.getCurrency()); + processTransaction.adjustInterestComponent(); } if (isNew) { checkRegisteredNewTransaction(loanTransaction, ctx); @@ -684,7 +701,7 @@ private List processOverpaidTransactions(List processTransaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings); if (processTransaction.isInterestWaiver()) { - processTransaction.adjustInterestComponent(currency); + processTransaction.adjustInterestComponent(); } if (isNew) { processTransaction = checkRegisteredNewTransaction(transaction, ctx); @@ -810,97 +827,99 @@ private List createSortedChangeList(final LoanTermVariationsDat return changeOperations; } + private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { + // TODO: Fix this and enhance EMICalculator to support reamortization and reaging + if (disbursementTransaction.getLoan().isInterestBearing()) { + handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); + } else { + handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); + } + } + private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx; - if (progressiveTransactionCtx.getModel() == null) { + ProgressiveLoanInterestScheduleModel model; + if (!(transactionCtx instanceof ProgressiveTransactionCtx) + || (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) { throw new IllegalStateException("TransactionCtx has no model"); } - disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); - LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); - Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); - Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency()); + Loan loan = disbursementTransaction.getLoan(); + LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); + Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); + List installments = transactionCtx.getInstallments(); + LocalDate transactionDate = disbursementTransaction.getTransactionDate(); + MonetaryCurrency currency = transactionCtx.getCurrency(); + Money downPaymentAmount = Money.zero(currency); if (loanProductRelatedDetail.isEnableDownPayment()) { - LoanRepaymentScheduleInstallment downPaymentInstallment = progressiveTransactionCtx.getInstallments().stream() - .filter(i -> i.isDownPayment() && i.getPrincipal(progressiveTransactionCtx.getCurrency()).isZero()).findFirst() - .orElseThrow(); BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(), loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc); if (installmentAmountInMultiplesOf != null) { downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); } - downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt); - downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); + downPaymentAmount = Money.of(currency, downPaymentAmt); + LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream() + .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow(); + downPaymentInstallment.addToPrincipal(transactionDate, downPaymentAmount); } - Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount); - - emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(), - amortizableAmount); + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); + emiCalculator.addDisbursement(model, transactionDate, amortizableAmount); + disbursementTransaction.resetDerivedComponents(); if (amortizableAmount.isGreaterThanZero()) { - progressiveTransactionCtx.getModel().repaymentPeriods().forEach(rm -> { - LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream() - .filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() - && !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate())) + model.repaymentPeriods().forEach(rm -> { + LoanRepaymentScheduleInstallment installment = installments.stream().filter( + ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate)) .findFirst().orElse(null); if (installment != null) { installment.updatePrincipal(rm.getDuePrincipal().getAmount()); installment.updateInterestCharged(rm.getDueInterest().getAmount()); - installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate()); + installment.updateObligationsMet(currency, transactionDate); } }); } - allocateOverpayment(disbursementTransaction, transactionCtx); } - private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - // TODO: Fix this and enhance EMICalculator to support reamortization and reaging - if (disbursementTransaction.getLoan().isInterestBearing()) { - handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); - } else { - handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); - } - } - private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); - List candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter( + MonetaryCurrency currency = transactionCtx.getCurrency(); + List installments = transactionCtx.getInstallments(); + List candidateRepaymentInstallments = installments.stream().filter( i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) .toList(); int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); - Money downPaymentAmount = Money.zero(transactionCtx.getCurrency()); + Money downPaymentAmount = Money.zero(currency); if (loanProductRelatedDetail.isEnableDownPayment()) { - LoanRepaymentScheduleInstallment downPaymentInstallment = transactionCtx.getInstallments().stream() - .filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.getCurrency()).isZero()).findFirst().orElseThrow(); + LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream() + .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow(); BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(), loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc); if (installmentAmountInMultiplesOf != null) { downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); } - downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt); + downPaymentAmount = Money.of(currency, downPaymentAmt); downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); } - Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount); + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); if (amortizableAmount.isGreaterThanZero()) { Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext()); MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount); candidateRepaymentInstallments.forEach(i -> { - Money previousPrincipal = i.getPrincipal(transactionCtx.getCurrency()); + Money previousPrincipal = i.getPrincipal(currency); Money newPrincipal = previousPrincipal.add(increasePrincipalBy); if (installmentAmountInMultiplesOf != null) { newPrincipal = Money.roundToMultiplesOf(newPrincipal, installmentAmountInMultiplesOf); } i.updatePrincipal(newPrincipal.getAmount()); moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal)); - i.updateObligationsMet(transactionCtx.getCurrency(), disbursementTransaction.getTransactionDate()); + i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate()); }); // Hence the rounding, we might need to amend the last installment amount candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) @@ -930,26 +949,30 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx } } - private List findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate currentDate, + protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) { + super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments()); + } + + private List findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate, ProgressiveTransactionCtx transactionCtx) { return transactionCtx.getInstallments().stream() // .filter(installment -> !installment.isDownPayment() && !installment.isAdditional()) - .filter(installment -> installment.isOverdueOn(currentDate)) + .filter(installment -> installment.isOverdueOn(targetDate)) .sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList(); } - private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx) { + private void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty() && ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct() && !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) { List overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber( - currentDate, ctx); + targetDate, ctx); if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { List normalInstallments = ctx.getInstallments().stream() // .filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList(); Optional currentInstallmentOptional = normalInstallments.stream().filter( - installment -> installment.getFromDate().isBefore(currentDate) && !installment.getDueDate().isBefore(currentDate)) + installment -> installment.getFromDate().isBefore(targetDate) && !installment.getDueDate().isBefore(targetDate)) .findAny(); // get DUE installment or last installment @@ -962,7 +985,7 @@ private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransa for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) { // add and subtract outstanding principal if (!overDuePrincipal.isZero()) { - adjustOverduePrincipalForInstallment(currentDate, processingInstallment, overDuePrincipal, + adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx); } @@ -970,11 +993,9 @@ private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransa aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal); } - boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(currentDate); + boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(targetDate); if (adjustNeeded) { - adjustOverduePrincipalForInstallment(currentDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, - ctx); - + adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx); } } } @@ -1074,15 +1095,15 @@ private Money processPaymentAllocation(PaymentAllocationType paymentAllocationTy LoanTransaction loanTransaction, Money transactionAmountUnprocessed, LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set chargesOfInstallment, Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) { + AllocationType allocationType = paymentAllocationType.getAllocationType(); + MonetaryCurrency currency = loanTransaction.getLoan().getCurrency(); + Money zero = Money.zero(currency); LocalDate transactionDate = loanTransaction.getTransactionDate(); - Money zero = transactionAmountUnprocessed.zero(); - - LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); + LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment.getPaymentFunction(allocationType, action); ChargesPaidByFunction chargesPaidByFunction = getChargesPaymentFunction(action); Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed); - switch (paymentAllocationType.getAllocationType()) { + switch (allocationType) { case PENALTY -> { balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion); @@ -1105,7 +1126,7 @@ private Money processPaymentAllocation(PaymentAllocationType paymentAllocationTy } } - currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, loanTransaction.getLoan().loanCurrency()); + currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return portion; } @@ -1126,13 +1147,14 @@ private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping l loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty); } - private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, MoneyHolder overpaymentHolder) { - if (overpaymentPortion.isGreaterThanZero()) { + private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, TransactionCtx transactionCtx) { + MoneyHolder overpaymentHolder = transactionCtx.getOverpaymentHolder(); + if (MathUtil.isGreaterThanZero(overpaymentPortion)) { onLoanOverpayment(loanTransaction, overpaymentPortion); overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().add(overpaymentPortion)); loanTransaction.setOverPayments(overpaymentPortion); } else { - overpaymentHolder.setMoneyObject(overpaymentPortion.zero()); + overpaymentHolder.setMoneyObject(Money.zero(transactionCtx.getCurrency())); } } @@ -1206,11 +1228,13 @@ private void handleChargePayment(LoanTransaction loanTransaction, TransactionCtx } } - private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, + private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Set charges, Balances balances) { + List transactionMappings, Balances balances) { + MonetaryCurrency currency = ctx.getCurrency(); Money zero = Money.zero(currency); + List installments = ctx.getInstallments(); + Set charges = ctx.getCharges(); Money refundedPortion; outerLoop: do { LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, @@ -1280,13 +1304,16 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon return transactionAmountUnprocessed; } - private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money zero, + private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx, List transactionMappings, Money transactionAmountUnprocessed, - FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set charges, Balances balances, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances, PaymentAllocationType paymentAllocationType) { - LoanRepaymentScheduleInstallment currentInstallment = null; + MonetaryCurrency currency = ctx.getCurrency(); + Money zero = Money.zero(currency); Money refundedPortion = zero; + List installments = ctx.getInstallments(); + Set charges = ctx.getCharges(); + LoanRepaymentScheduleInstallment currentInstallment = null; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); do { switch (paymentAllocationType.getDueType()) { @@ -1397,7 +1424,7 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); - handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder()); + handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx); } private Money processPeriods(LoanTransaction transaction, Money processAmount, Set charges, @@ -1409,7 +1436,6 @@ private Money processPeriods(LoanTransaction transaction, Money processAmount, S private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule, Set charges, List transactionMappings, Balances balances, TransactionCtx transactionCtx) { - MonetaryCurrency currency = transactionCtx.getCurrency(); LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail() .getLoanScheduleProcessingType(); if (scheduleProcessingType.isHorizontal()) { @@ -1417,8 +1443,8 @@ private Money processPeriods(LoanTransaction transaction, Money processAmount, L balances); } if (scheduleProcessingType.isVertical()) { - return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule, - transactionMappings, charges, balances); + return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges, + balances); } return processAmount; } @@ -1442,7 +1468,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, List transactionMappings, Set charges, Balances balances) { - if (transactionAmountUnprocessed.isZero()) { + if (MathUtil.isEmpty(transactionAmountUnprocessed)) { return transactionAmountUnprocessed; } @@ -1585,7 +1611,7 @@ private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTra if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) { payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment); - updateRepaymentPeriodBalances(paymentAllocationType, installment, model, payDate); + updateRepaymentPeriodBalances(paymentAllocationType, installment, ctx, payDate); } paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, transactionAmountUnprocessed, @@ -1593,17 +1619,16 @@ private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTra if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) { emiCalculator.payPrincipal(model, installment.getDueDate(), payDate, paidPortion); - updateRepaymentPeriods(loanTransaction, ctx, model); + updateRepaymentPeriods(loanTransaction, ctx); } else if (INTEREST.equals(paymentAllocationType.getAllocationType())) { emiCalculator.payInterest(model, installment.getDueDate(), payDate, paidPortion); - updateRepaymentPeriods(loanTransaction, ctx, model); + updateRepaymentPeriods(loanTransaction, ctx); } return paidPortion; } - private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx, - ProgressiveLoanInterestScheduleModel model) { - model.repaymentPeriods().forEach(rm -> { + private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) { + ctx.getModel().repaymentPeriods().forEach(rm -> { LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream() .filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()).findFirst().orElse(null); if (installment != null) { @@ -1615,8 +1640,8 @@ private void updateRepaymentPeriods(LoanTransaction loanTransaction, Progressive } private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType, - LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) { - PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate); + LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveTransactionCtx ctx, LocalDate payDate) { + PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(ctx.getModel(), inAdvanceInstallment.getDueDate(), payDate); switch (paymentAllocationType) { case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount()); @@ -1633,6 +1658,7 @@ private LocalDate calculateNewPayDateInCaseOfInAdvancePayment(LoanTransaction lo LocalDate payDate = switch (strategy) { case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate(); + // TODO use isInPeriod case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) // && !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) // ? inAdvanceInstallment.getDueDate() // @@ -1651,10 +1677,11 @@ private Set getLoanChargesOfInstallment(Set charges, Loa .collect(Collectors.toSet()); } - private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, + private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, Set charges, Balances balances) { + MonetaryCurrency currency = ctx.getCurrency(); + List installments = ctx.getInstallments(); int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); @@ -1819,6 +1846,10 @@ private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) { reprocessInstallmentsOrder(installments); } + protected void calculateAccrualActivity(LoanTransaction transaction, TransactionCtx ctx) { + super.calculateAccrualActivity(transaction, ctx.getCurrency(), ctx.getInstallments()); + } + private void reprocessInstallmentsOrder(List installments) { AtomicInteger counter = new AtomicInteger(1); installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java index 1b7c10f3072..c950bc687b2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java @@ -84,10 +84,13 @@ public void addDisbursementAmount(final Money disbursementAmount) { } public Money getCalculatedDueInterest() { - final BigDecimal interestDueTillRepaymentDueDate = getOutstandingLoanBalance()// - .multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() // - .divide(BigDecimal.valueOf(getLengthTillPeriodDueDate()), mc) // - .multiply(BigDecimal.valueOf(getLength()), mc); // + long lengthTillPeriodDueDate = getLengthTillPeriodDueDate(); + final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 // + ? BigDecimal.ZERO // + : getOutstandingLoanBalance() // + .multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() // + .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // + .multiply(BigDecimal.valueOf(getLength()), mc); // return Money.of(outstandingLoanBalance.getCurrency(), interestDueTillRepaymentDueDate, mc); } @@ -103,8 +106,7 @@ public void updateOutstandingLoanBalance() { if (isFirstInterestPeriod()) { Optional previousRepaymentPeriod = getRepaymentPeriod().getPrevious(); if (previousRepaymentPeriod.isPresent()) { - InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods() - .get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1); + InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod(); this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()// .plus(previousInterestPeriod.getDisbursementAmount(), mc)// .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)// @@ -120,7 +122,7 @@ public void updateOutstandingLoanBalance() { } } - private boolean isFirstInterestPeriod() { - return getRepaymentPeriod().getInterestPeriods().get(0).equals(this); + public boolean isFirstInterestPeriod() { + return this.equals(getRepaymentPeriod().getFirstInterestPeriod()); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index ad7acca6fa4..0dec32dcd4b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.data; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -83,7 +86,7 @@ public BigDecimal getInterestRate(final LocalDate effectiveDate) { private BigDecimal findInterestRate(final LocalDate effectiveDate) { return interestRates.stream() // - .filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) // + .filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)) // .map(InterestRate::interestRate) // .findFirst() // .orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); // @@ -93,12 +96,12 @@ public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigD interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate)); } - public Optional findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) { + public Optional findRepaymentPeriodByDueDate(final LocalDate repaymentPeriodDueDate) { if (repaymentPeriodDueDate == null) { return Optional.empty(); } return repaymentPeriods.stream()// - .filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))// + .filter(repaymentPeriodItem -> DateUtils.isEqual(repaymentPeriodItem.getDueDate(), repaymentPeriodDueDate))// .findFirst(); } @@ -107,7 +110,7 @@ public List getRelatedRepaymentPeriods(final LocalDate calculat return repaymentPeriods; } return repaymentPeriods.stream()// - .filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))// + .filter(period -> !DateUtils.isBefore(period.getDueDate(), calculateFromRepaymentPeriodDueDate))// .toList();// } @@ -120,6 +123,10 @@ public int getLoanTermInDays() { return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate()); } + public LocalDate getStartDate() { + return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(0).getFromDate() : null; + } + public LocalDate getMaturityDate() { return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null; } @@ -135,6 +142,7 @@ Optional findRepaymentPeriodForBalanceChange(final LocalDate ba if (balanceChangeDate == null) { return Optional.empty(); } + // TODO use isInPeriod return repaymentPeriods.stream()// .filter(repaymentPeriod -> { final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty(); @@ -214,4 +222,10 @@ public Money getTotalPaidInterest() { public Money getTotalPaidPrincipal() { return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus); } + + public Optional findRepaymentPeriod(@NotNull LocalDate transactionDate) { + return repaymentPeriods.stream() // + .filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))// + .findFirst(); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 6a531a90230..5ecd47ca62a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.data; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -106,6 +109,7 @@ private BigDecimal calculateRateFactorPlus1() { return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add); } + @NotNull public Money getCalculatedDueInterest() { if (calculatedDueInterestCalculation == null) { calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, @@ -123,23 +127,6 @@ private Money calculateCalculatedDueInterest() { return calculatedDueInterest; } - private Money getZero(MathContext mc) { - // EMI is always initiated - return this.emi.zero(mc); - } - - public Money getCalculatedDuePrincipal() { - return getEmi().minus(getCalculatedDueInterest(), mc); - } - - public Money getTotalPaidAmount() { - return getPaidPrincipal().plus(getPaidInterest()); - } - - public boolean isFullyPaid() { - return getEmi().isEqualTo(getTotalPaidAmount()); - } - public Money getDueInterest() { if (dueInterestCalculation == null) { // Due interest might be the maximum paid if there is pay-off or early repayment @@ -150,11 +137,23 @@ public Money getDueInterest() { return dueInterestCalculation.get(); } + public Money getCalculatedDuePrincipal() { + return getEmi().minus(getCalculatedDueInterest(), mc); + } + public Money getDuePrincipal() { // Due principal might be the maximum paid if there is pay-off or early repayment return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false); } + public Money getTotalPaidAmount() { + return getPaidPrincipal().plus(getPaidInterest()); + } + + public boolean isFullyPaid() { + return getEmi().isEqualTo(getTotalPaidAmount()); + } + public Money getUnrecognizedInterest() { return getCalculatedDueInterest().minus(getDueInterest(), mc); } @@ -193,4 +192,29 @@ public Money getInitialBalanceForEmiRecalculation() { (m1, m2) -> m1.plus(m2, mc)); return initialBalance.add(totalDisbursedAmount, mc); } + + private Money getZero(MathContext mc) { + // EMI is always initiated + return this.emi.zero(mc); + } + + public InterestPeriod getFirstInterestPeriod() { + return getInterestPeriods().get(0); + } + + public InterestPeriod getLastInterestPeriod() { + List interestPeriods = getInterestPeriods(); + return interestPeriods.get(interestPeriods.size() - 1); + } + + public Optional findInterestPeriod(@NotNull LocalDate transactionDate) { + return interestPeriods.stream() // + .filter(interestPeriod -> isInPeriod(transactionDate, interestPeriod.getFromDate(), interestPeriod.getDueDate(), + isFirstRepaymentPeriod() && interestPeriod.isFirstInterestPeriod()))// + .reduce((one, two) -> two); + } + + public boolean isFirstRepaymentPeriod() { + return previous == null; + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index c11ee1fa98a..d03f2b2d7c3 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.findInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -39,7 +42,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; 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.loanschedule.data.LoanScheduleDTO; @@ -89,7 +91,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer // generate list of proposed schedule due dates final List expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, periodStartDate, loanApplicationTerms, holidayDetailDTO); - final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel( + final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(), loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); @@ -147,6 +149,58 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } + @Override + public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom) { + LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO); + return LoanScheduleDTO.from(null, model); + } + + @Override + public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, + LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, + LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment actualInstallment = findInPeriod(onDate, installments).orElse(installments.get(0)); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); + }; + + ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), onDate); + OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate); + // TODO: We should add all the past due outstanding amounts as well + OutstandingAmountsDTO result = new OutstandingAmountsDTO(currency) // + .principal(outstandingAmounts.getOutstandingPrincipal()) // + .interest(outstandingAmounts.getOutstandingInterest());// + + installments.forEach(installment -> result // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); + + return result; + } + + @Override + public Money getPeriodInterestTillDate(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, + @NotNull LocalDate targetDate) { + LoanRepaymentScheduleTransactionProcessor transactionProcessor = loan.getTransactionProcessor(); + if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate); + return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate); + } + + // Private, internal methods + private List getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) { final List disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); @@ -238,49 +292,6 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm } } - @Override - public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, - HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, - LocalDate rescheduleFrom) { - LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO); - return LoanScheduleDTO.from(null, model); - } - - @Override - public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { - throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); - } - - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment actualInstallment = LoanRepaymentScheduleProcessingWrapper.findInPeriod(onDate, installments) - .orElse(installments.get(0)); - - LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> onDate; - case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment - case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); - }; - - ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate, - loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); - - OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate); - // TODO: We should add all the past due outstanding amounts as well - OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // - .principal(result.getOutstandingPrincipal()) // - .interest(result.getOutstandingInterest());// - - installments.forEach(installment -> amounts // - .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) - .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); - - return amounts; - } - - // Private, internal methods private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; if (loanCharges != null) { @@ -430,5 +441,4 @@ private Set separateTotalCompoundingPercentageCharges(final Set periods, - LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc); + @NotNull + ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, + @NotNull LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc); - ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail, - Integer installmentAmountInMultiplesOf, List repaymentPeriods, MathContext mc); + @NotNull + ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( + @NotNull List installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + Integer installmentAmountInMultiplesOf, MathContext mc); Optional findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate); @@ -56,7 +60,13 @@ void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate r void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money principalAmount); - PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate); + @NotNull + PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate); + + @NotNull + Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate); Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, LocalDate targetDate); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index 87a6e8c06db..25205779c12 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -18,14 +18,16 @@ */ package org.apache.fineract.portfolio.loanproduct.calc; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.time.Year; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -42,7 +44,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @Component @@ -53,20 +54,36 @@ public final class ProgressiveEMICalculator implements EMICalculator { private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7); @Override - public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List periods, - final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) { - final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency(), mc); - final ArrayList interestRepaymentModelList = new ArrayList<>(periods.size()); - RepaymentPeriod previousPeriod = null; - for (final LoanScheduleModelRepaymentPeriod period : periods) { - RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(), zeroAmount, - mc); - previousPeriod = currentPeriod; - interestRepaymentModelList.add(currentPeriod); + @NotNull + public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, + @NotNull LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, + final MathContext mc) { + return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate, + LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + } - } - return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail, - installmentAmountInMultiplesOf, mc); + @Override + @NotNull + public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( + @NotNull List installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + final Integer installmentAmountInMultiplesOf, final MathContext mc) { + installments = installments.stream().filter(installment -> !installment.isDownPayment() && !installment.isAdditional()).toList(); + return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate, + LoanRepaymentScheduleInstallment::getDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + } + + @NotNull + private ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List periods, Function from, + Function to, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + final Integer installmentAmountInMultiplesOf, final MathContext mc) { + final Money zero = Money.zero(loanProductRelatedDetail.getCurrency(), mc); + final AtomicReference prev = new AtomicReference<>(); + List repaymentPeriods = periods.stream().map(e -> { + RepaymentPeriod rp = new RepaymentPeriod(prev.get(), from.apply(e), to.apply(e), zero, mc); + prev.set(rp); + return rp; + }).toList(); + return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); } @Override @@ -75,7 +92,7 @@ public Optional findRepaymentPeriod(final ProgressiveLoanIntere if (scheduleModel == null) { return Optional.empty(); } - return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate); + return scheduleModel.findRepaymentPeriodByDueDate(repaymentPeriodDueDate); } /** @@ -139,13 +156,14 @@ public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, Loca @Override public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money principalAmount) { + if (MathUtil.isEmpty(principalAmount)) { + return; + } Optional repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate); repaymentPeriod.ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount)); - LocalDate balanceCorrectionDate = transactionDate; - if (repaymentPeriodDueDate.isBefore(transactionDate)) { - // If it is paid late, we need to calculate with the period due date - balanceCorrectionDate = repaymentPeriodDueDate; - } + // If it is paid late, we need to calculate with the period due date + LocalDate balanceCorrectionDate = DateUtils.isBefore(repaymentPeriodDueDate, transactionDate) ? repaymentPeriodDueDate + : transactionDate; addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated()); repaymentPeriod.ifPresent(rp -> { @@ -159,11 +177,12 @@ public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, Loc } @Override - public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate, - final LocalDate targetDate) { + @NotNull + public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate) { ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, - repaymentPeriodDueDate, targetDate); - RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + periodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .count() > 1L; if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) { @@ -176,11 +195,21 @@ public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel } @Override - public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, + @NotNull + public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate) { + ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, + periodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); + return repaymentPeriod.getCalculatedDueInterest(); + } + + @Override + public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate) { ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, - repaymentPeriodDueDate, targetDate); - RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + periodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); return repaymentPeriod.getOutstandingLoanBalance(); } @@ -189,7 +218,7 @@ public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleMo public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - + // TODO use findInterestPeriod scheduleModelCopy.repaymentPeriods().stream()// .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// .flatMap(rp -> rp.getInterestPeriods().stream()// @@ -214,25 +243,26 @@ public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestS } @NotNull - private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, - LocalDate repaymentPeriodDueDate, LocalDate targetDate) { + private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate( + @NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream() - .filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow(); + RepaymentPeriod repaymentPeriod = scheduleModelCopy.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); LocalDate adjustedTargetDate = targetDate; InterestPeriod interestPeriod; if (!targetDate.isAfter(repaymentPeriod.getFromDate())) { - interestPeriod = repaymentPeriod.getInterestPeriods().get(0); + interestPeriod = repaymentPeriod.getFirstInterestPeriod(); adjustedTargetDate = repaymentPeriod.getFromDate(); } else if (targetDate.isAfter(repaymentPeriod.getDueDate())) { - interestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1); + interestPeriod = repaymentPeriod.getLastInterestPeriod(); adjustedTargetDate = repaymentPeriod.getDueDate(); } else { + // TODO use findInterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().stream() .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow(); } + // TODO use findInterestPeriod scheduleModelCopy.repaymentPeriods().stream()// .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// .flatMap(rp -> rp.getInterestPeriods().stream()// @@ -645,6 +675,9 @@ BigDecimal rateFactorByRepaymentEveryMonth(final BigDecimal interestRate, final BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays, final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, final MathContext mc) { + if (MathUtil.isZero(calculatedDaysInPeriod)) { + return BigDecimal.ZERO; + } final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays// .multiply(repaymentEvery, mc)// .divide(daysInYear, mc);// @@ -661,6 +694,9 @@ BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigD BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, final MathContext mc) { + if (MathUtil.isZero(calculatedDaysInPeriod)) { + return BigDecimal.ZERO; + } final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio); return interestRate// .multiply(interestFractionPerPeriod, mc)// @@ -682,24 +718,6 @@ BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRat return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc); } - @Override - public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail, - Integer installmentAmountInMultiplesOf, List repaymentPeriods, MathContext mc) { - List repaymentModelsWithoutDownPayment = repaymentPeriods.stream() - .filter(period -> !period.isDownPayment() && !period.isAdditional()).toList(); - - List repaymentModels = new ArrayList<>(); - RepaymentPeriod previousPeriod = null; - for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) { - RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(), - Money.zero(repaymentModel.getLoan().getCurrency(), mc), mc); - previousPeriod = currentPeriod; - repaymentModels.add(currentPeriod); - } - - return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - } - /** * Calculates the sum of due interests on interest periods. * 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 cb1c285fdd5..2d23c01e0dd 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 @@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; 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.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; @@ -95,6 +96,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class); private AdvancedPaymentScheduleTransactionProcessor underTest; private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class); + private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class); @BeforeAll public static void init() { @@ -109,7 +111,7 @@ public static void destruct() { @BeforeEach public void setUp() { - underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, null); + underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); @@ -235,7 +237,7 @@ public void chargePaymentTransactionTestWithMoreTransactionAmount() { when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); when(loanTransaction.getLoan()).thenReturn(loan); - when(loan.loanCurrency()).thenReturn(currency); + when(loan.getCurrency()).thenReturn(currency); when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); when(loan.getDisbursementDate()).thenReturn(disbursementDate); @@ -451,7 +453,7 @@ public void testProcessLatestTransaction_PassesThroughHandlingPaymentAllocationF when(loanProductRelatedDetail.isInterestRecalculationEnabled()).thenReturn(true); when(loanTransaction.getLoan()).thenReturn(loan); - when(loan.loanCurrency()).thenReturn(currency); + when(loan.getCurrency()).thenReturn(currency); when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule)); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 8c0686acf30..a49824b7b72 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -158,7 +158,6 @@ public void test_fnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual @Test public void test_generateInterestScheduleModel() { - final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -169,8 +168,8 @@ public void test_generateInterestScheduleModel() { Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator - .generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Assertions.assertTrue(interestScheduleModel != null); Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null); @@ -183,7 +182,6 @@ public void test_generateInterestScheduleModel() { @Test @Timeout(1) // seconds public void test_emi_calculator_performance() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -209,8 +207,8 @@ public void test_emi_calculator_performance() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -234,7 +232,6 @@ public void test_emi_calculator_performance() { @Test public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -254,8 +251,8 @@ public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -271,7 +268,6 @@ public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { @Test public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -291,8 +287,8 @@ public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -308,7 +304,6 @@ public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() @Test public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -328,8 +323,8 @@ public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -355,7 +350,6 @@ public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_ @Test public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -375,8 +369,8 @@ public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEve Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -392,7 +386,6 @@ public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEve @Test public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -414,8 +407,8 @@ public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -435,7 +428,6 @@ public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_d @Test public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -457,8 +449,8 @@ public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt1 threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -504,8 +496,8 @@ public void test_reschedule_interest_on0120_adjsLst_dsbAmt100_dayInYears360_days threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -529,7 +521,6 @@ public void test_reschedule_interest_on0120_adjsLst_dsbAmt100_dayInYears360_days @Test public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -551,8 +542,8 @@ public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -576,7 +567,6 @@ public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_d */ @Test public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -598,8 +588,8 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -684,7 +674,6 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM @Test public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -706,8 +695,8 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -723,9 +712,8 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal())); Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest())); - PeriodDueDetails details = null; - // check getdueAmounts forcast - details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + // check getDueAmounts forcast + PeriodDueDetails details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal())); Assertions.assertEquals(0.49, toDouble(details.getDueInterest())); @@ -757,7 +745,6 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay @Test public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -779,8 +766,8 @@ public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -856,8 +843,8 @@ public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -905,8 +892,8 @@ public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -954,8 +941,8 @@ public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); @@ -998,8 +985,8 @@ public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1M Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); @@ -1032,8 +1019,8 @@ public void test_disbursedAmt1000_NoInterest_repayEvery1Month() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1065,8 +1052,8 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1098,8 +1085,8 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1131,8 +1118,8 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1163,8 +1150,8 @@ public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1225,8 +1212,8 @@ public void test_dailyInterest_disbursedAmt2000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount1st = toMoney(1000.0); final Money disbursedAmount2nd = toMoney(1000.0); 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 0d7e4902a20..fb9aa1e766d 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 @@ -35,6 +35,7 @@ import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.springframework.stereotype.Component; @Component @@ -50,69 +51,66 @@ public void createJournalEntriesForLoan(final LoanDTO loanDTO) { for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) { final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); this.helper.checkForBranchClosures(latestGLClosure, transactionDate); + final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType(); - /** Handle Disbursements **/ - if (loanTransactionDTO.getTransactionType().isDisbursement()) { + // Handle Disbursements + if (transactionType.isDisbursement()) { createJournalEntriesForDisbursements(loanDTO, loanTransactionDTO, office); } - /*** Handle Accruals ***/ - if (loanTransactionDTO.getTransactionType().isAccrual()) { + // Handle Accruals + if (transactionType.isAccrual() || transactionType.isAccrualAdjustment()) { createJournalEntriesForAccruals(loanDTO, loanTransactionDTO, office); } - /*** + /* * Handle repayments, loan refunds, repayments at disbursement and reversal of Repayments and Repayments at * disbursement (except charge adjustment) - ***/ - else if ((loanTransactionDTO.getTransactionType().isRepaymentType() - && !loanTransactionDTO.getTransactionType().isChargeAdjustment()) - || loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement() - || loanTransactionDTO.getTransactionType().isChargePayment()) { + */ + else if ((transactionType.isRepaymentType() && !transactionType.isChargeAdjustment()) + || transactionType.isRepaymentAtDisbursement() || transactionType.isChargePayment()) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false, - loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()); + transactionType.isRepaymentAtDisbursement()); } - /** Logic for handling recovery payments **/ - else if (loanTransactionDTO.getTransactionType().isRecoveryRepayment()) { + // Logic for handling recovery payments + else if (transactionType.isRecoveryRepayment()) { createJournalEntriesForRecoveryRepayments(loanDTO, loanTransactionDTO, office); } - /** Logic for Refunds of Overpayments **/ - else if (loanTransactionDTO.getTransactionType().isRefund()) { + // Logic for Refunds of Overpayments + else if (transactionType.isRefund()) { createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office); } - /** Logic for Credit Balance Refunds **/ - else if (loanTransactionDTO.getTransactionType().isCreditBalanceRefund()) { + // Logic for Credit Balance Refunds + else if (transactionType.isCreditBalanceRefund()) { createJournalEntriesForCreditBalanceRefund(loanDTO, loanTransactionDTO, office); } - /** Handle Write Offs, waivers and their reversals **/ - else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest() - || loanTransactionDTO.getTransactionType().isWaiveCharges())) { + // Handle Write Offs, waivers and their reversals + else if ((transactionType.isWriteOff() || transactionType.isWaiveInterest() || transactionType.isWaiveCharges())) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false); } - /** Logic for Refunds of Active Loans **/ - else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) { + // Logic for Refunds of Active Loans + else if (transactionType.isRefundForActiveLoans()) { createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office); } // Logic for Chargebacks - else if (loanTransactionDTO.getTransactionType().isChargeback()) { + else if (transactionType.isChargeback()) { createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office); } // Logic for Charge Adjustment - else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { + else if (transactionType.isChargeAdjustment()) { createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); } // Logic for Charge-Off - else if (loanTransactionDTO.getTransactionType().isChargeoff()) { + else if (transactionType.isChargeoff()) { createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office); } // Logic for Interest Payment Waiver - else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver() - || loanTransactionDTO.getTransactionType().isInterestRefund()) { + else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterestRefund()) { createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO, loanTransactionDTO, office); } } @@ -1170,7 +1168,6 @@ private void createJournalEntriesForRecoveryRepayments(final LoanDTO loanDTO, fi * @param office */ private void createJournalEntriesForAccruals(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) { - // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); @@ -1179,10 +1176,11 @@ private void createJournalEntriesForAccruals(final LoanDTO loanDTO, final LoanTr // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType(); final BigDecimal interestAmount = loanTransactionDTO.getInterest(); final BigDecimal feesAmount = loanTransactionDTO.getFees(); final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); - final boolean isReversed = loanTransactionDTO.isReversed(); + final boolean isReversed = transactionType.isAccrualAdjustment() != loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); // create journal entries for recognizing interest (or reversal) 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 35fb7e5e4f9..9298daed661 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 @@ -190,7 +190,7 @@ public Collection retrieveLoanApplicableFees() { } @Override - public Collection retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) { + public List retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) { final ChargeMapper rm = new ChargeMapper(); StringBuilder excludeClause = new StringBuilder(""); Map paramMap = new HashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java index 3318a4d04ec..379366f928d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java @@ -42,6 +42,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; @@ -442,7 +443,7 @@ private String retrieveLoanCharge(final Long loanId, final String loanExternalId final LoanChargeData loanCharge = this.loanChargeReadPlatformService.retrieveLoanChargeDetails(resolvedLoanChargeId, resolvedLoanId); - final Collection installmentChargeData = this.loanChargeReadPlatformService + final List installmentChargeData = this.loanChargeReadPlatformService .retrieveInstallmentLoanCharges(resolvedLoanChargeId, true); final LoanChargeData loanChargeData = new LoanChargeData(loanCharge, installmentChargeData); @@ -536,7 +537,7 @@ private String retrieveTemplate(final Long loanId, final String loanExternalIdSt ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); - final Collection chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId, + final List chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId, new ChargeTimeType[] { ChargeTimeType.OVERDUE_INSTALLMENT }); final LoanChargeData loanChargeTemplate = LoanChargeData.template(chargeOptions); 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 af9617b25d6..34528bbd3ee 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 @@ -915,15 +915,7 @@ public Pair makeRefund(final Loan loan, final if (interestRefundTransaction != null) { loan.addLoanTransaction(interestRefundTransaction); } - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(loan); - } - - loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values()); + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); // Store and flush newly created transaction to generate PK saveLoanTransactionWithDataIntegrityViolationChecks(refundTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java index 208f33532ad..72dc168a0d4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java @@ -18,15 +18,12 @@ */ package org.apache.fineract.portfolio.loanaccount.jobs.addaccrualentries; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.time.LocalDate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +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.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.StepContribution; @@ -45,30 +42,15 @@ public class AddAccrualEntriesTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - Collection loanScheduleAccrualDataList = loanReadPlatformService.retriveScheduleAccrualData(); - Map> loanDataMap = new HashMap<>(); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { - if (loanDataMap.containsKey(accrualData.getLoanId())) { - loanDataMap.get(accrualData.getLoanId()).add(accrualData); - } else { - Collection accrualDataList = new ArrayList<>(); - accrualDataList.add(accrualData); - loanDataMap.put(accrualData.getLoanId(), accrualDataList); - } - } - - List errors = new ArrayList<>(); - for (Map.Entry> mapEntry : loanDataMap.entrySet()) { - try { - loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue()); - } catch (Exception e) { - log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e); - errors.add(e); - } - } - if (!errors.isEmpty()) { - throw new JobExecutionException(errors); + try { + addAccruals(DateUtils.getBusinessLocalDate()); + } catch (MultiException e) { + throw new JobExecutionException(e); } return RepeatStatus.FINISHED; } + + private void addAccruals(final LocalDate tilldate) throws MultiException { + loanAccrualsProcessingService.addAccruals(tilldate); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 24e0befbf4a..38371fb53f0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -440,7 +440,7 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { } loanAccrualsProcessingService.reprocessExistingAccruals(loan); loan.recalculateAllCharges(); - ChangedTransactionDetail changedTransactionDetail = loan.processTransactions(); + ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList); 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 9a730875101..ba118dfb994 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 @@ -18,68 +18,66 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrualAdjustment; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; 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.organisation.office.domain.Office; -import org.apache.fineract.organisation.office.domain.OfficeRepository; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; -import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; -import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData; +import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData; +import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; -import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; 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.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; -import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.dao.DataAccessException; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -88,32 +86,38 @@ @RequiredArgsConstructor public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessingService { - private static final String ACCRUAL_ON_CHARGE_DUE_DATE = "due-date"; private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date"; - private final LoanChargeReadPlatformService loanChargeReadPlatformService; private final ExternalIdFactory externalIdFactory; private final BusinessEventNotifierService businessEventNotifierService; private final ConfigurationDomainService configurationDomainService; - private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; - private final LoanReadPlatformService loanReadPlatformService; private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final LoanTransactionRepository loanTransactionRepository; - private final PlatformSecurityContext context; - private final LoanRepository loanRepository; - private final OfficeRepository officeRepository; - private final LoanChargeRepository loanChargeRepository; + private final LoanScheduleGeneratorFactory loanScheduleFactory; + private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan */ @Override @Transactional - public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService - .retrievePeriodicAccrualData(tillDate); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); + public void addPeriodicAccruals(@NotNull LocalDate tillDate) throws JobExecutionException { + List loans = loanRepositoryWrapper.findLoansForPeriodicAccrual(AccountingRuleType.ACCRUAL_PERIODIC.getValue(), tillDate, + !isChargeOnDueDate()); + List errors = new ArrayList<>(); + for (Loan loan : loans) { + try { + setSetHelpers(loan); + addPeriodicAccruals(tillDate, loan); + } catch (Exception e) { + log.error("Failed to add accrual for loan {}", loan.getId(), e); + errors.add(e); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } } /** @@ -121,31 +125,24 @@ public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionExc */ @Override @Transactional - public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate, - loan); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); + public void addPeriodicAccruals(@NotNull LocalDate tillDate, @NotNull Loan loan) { + addAccruals(loan, tillDate, true, false, true); } - private void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualDataList) - throws JobExecutionException { - Map> loanDataMap = new HashMap<>(); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { - if (loanDataMap.containsKey(accrualData.getLoanId())) { - loanDataMap.get(accrualData.getLoanId()).add(accrualData); - } else { - Collection accrualDataList = new ArrayList<>(); - accrualDataList.add(accrualData); - loanDataMap.put(accrualData.getLoanId(), accrualDataList); - } - } - + /** + * method adds accrual for batch job "Add Accrual Transactions" + */ + @Transactional + public void addAccruals(@NotNull LocalDate tillDate) throws JobExecutionException { + List loans = loanRepositoryWrapper.findLoansForAddAccrual(AccountingRuleType.ACCRUAL_PERIODIC.getValue(), tillDate, + !isChargeOnDueDate()); List errors = new ArrayList<>(); - for (Map.Entry> mapEntry : loanDataMap.entrySet()) { + for (Loan loan : loans) { try { - addPeriodicAccruals(tillDate, mapEntry.getKey(), mapEntry.getValue()); + setSetHelpers(loan); + addAccruals(loan, tillDate, false, false, true); } catch (Exception e) { - log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e); + log.error("Failed to add accrual for loan {}", loan.getId(), e); errors.add(e); } } @@ -154,87 +151,342 @@ private void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualData) { - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection loanWaiverTransactionData = new ArrayList<>(1); - - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) { - if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { - loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); - loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); - } - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate()); - calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); + private void addAccruals(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean periodic, boolean isFinal, boolean addJournal) { + if ((!isFinal && !loan.isOpen()) || loan.isNpa() || loan.isChargedOff() + || !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + return; } - } - - private void addPeriodicAccruals(final LocalDate tillDate, Long loanId, Collection loanScheduleAccrualData) { - boolean firstTime = true; - LocalDate accruedTill = null; - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection loanWaiverTransactionData = new ArrayList<>(1); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) { - if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { - loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); - loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + boolean progressiveAccrual = isProgressiveAccrual(loan); + MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); + + AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic); + LocalDate transactionDate = isFinal ? getFinalAccrualTransactionDate(loan) : tillDate; + boolean mergeTreansactions = isFinal || progressiveAccrual; + List accrualTransactions = new ArrayList<>(); + LoanTransaction mergeAccrualTransaction = null; + LoanTransaction mergeAdjustTransaction = null; + for (AccrualPeriodData period : accrualPeriods.getPeriods()) { + Money interestAccruable = MathUtil.nullToZero(period.getInterestAccruable(), currency); + Money interestPortion = MathUtil.minus(interestAccruable, period.getInterestAccrued()); + Money feeAccruable = MathUtil.nullToZero(period.getFeeAccruable(), currency); + Money feePortion = MathUtil.minus(feeAccruable, period.getFeeAccrued()); + Money penaltyAccruable = MathUtil.nullToZero(period.getPenaltyAccruable(), currency); + Money penaltyPortion = MathUtil.minus(penaltyAccruable, period.getPenaltyAccrued()); + if (MathUtil.isEmpty(interestPortion) && MathUtil.isEmpty(feePortion) && MathUtil.isEmpty(penaltyPortion)) { + continue; } - - if (DateUtils.isAfter(accrualData.getDueDateAsLocaldate(), tillDate)) { - if (accruedTill == null || firstTime) { - accruedTill = accrualData.getAccruedTill(); - firstTime = false; + if (mergeTreansactions) { + if (progressiveAccrual) { + Money interestAdjustmentPortion = MathUtil.negate(interestPortion); + Money feeAdjustmentPortion = MathUtil.negate(feePortion); + Money penaltyAdjustmentPortion = MathUtil.negate(penaltyPortion); + if (mergeAdjustTransaction == null) { + mergeAdjustTransaction = addAccrualTransaction(loan, transactionDate, period, interestAdjustmentPortion, + feeAdjustmentPortion, penaltyAdjustmentPortion, true); + if (mergeAdjustTransaction != null) { + accrualTransactions.add(mergeAdjustTransaction); + } + } else { + mergeAccrualTransaction(mergeAdjustTransaction, period, interestAdjustmentPortion, feeAdjustmentPortion, + penaltyAdjustmentPortion, true); + } } - if (accruedTill == null || DateUtils.isBefore(accruedTill, tillDate)) { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tillDate); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(tillDate, accrualData); + if (mergeAccrualTransaction == null) { + mergeAccrualTransaction = addAccrualTransaction(loan, transactionDate, period, interestPortion, feePortion, + penaltyPortion, false); + if (mergeAccrualTransaction != null) { + accrualTransactions.add(mergeAccrualTransaction); + } + } else { + mergeAccrualTransaction(mergeAccrualTransaction, period, interestPortion, feePortion, penaltyPortion, false); } } else { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); - accruedTill = accrualData.getDueDateAsLocaldate(); + LocalDate periodTransactionDate = DateUtils.isBefore(period.getDueDate(), transactionDate) ? period.getDueDate() + : transactionDate; + LoanTransaction accrualTransaction = addAccrualTransaction(loan, periodTransactionDate, period, interestPortion, feePortion, + penaltyPortion, false); + if (accrualTransaction != null) { + accrualTransactions.add(accrualTransaction); + } + } + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(period.getInstallmentNumber()); + installment.updateAccrualPortion(interestAccruable, feeAccruable, penaltyAccruable); + } + loan.setAccruedTill(tillDate); + + if (accrualTransactions.isEmpty()) { + return; + } + ArrayList> newTransactionMapping = new ArrayList<>(); + for (LoanTransaction accrualTransaction : accrualTransactions) { + accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction); + LoanTransactionBusinessEvent businessEvent = accrualTransaction.isAccrual() + ? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction) + : new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction); + businessEventNotifierService.notifyPostBusinessEvent(businessEvent); + if (addJournal) { + newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode())); } } + if (addJournal) { + Map accountingBridgeData = deriveAccountingBridgeData(loan, newTransactionMapping); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } } - @Transactional - @Override - public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException { - if (loanId != null) { - Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); - if (loan == null) { - throw new LoanNotFoundException(loanId); + private AccrualPeriodsData calculateAccrualAmounts(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean periodic) { + boolean chargeOnDueDate = isChargeOnDueDate(); + LoanProductRelatedDetail productDetail = loan.getLoanProductRelatedDetail(); + MonetaryCurrency currency = productDetail.getCurrency(); + LoanScheduleGenerator scheduleGenerator = loanScheduleFactory.create(productDetail.getLoanScheduleType(), + productDetail.getInterestMethod()); + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + List installments = getInstallmentsToAccrue(loan, tillDate, periodic); + AccrualPeriodsData accrualPeriods = AccrualPeriodsData.create(installments, firstInstallmentNumber, currency); + for (LoanRepaymentScheduleInstallment installment : installments) { + addInterestAccrual(loan, tillDate, scheduleGenerator, installment, accrualPeriods); + addChargeAccrual(loan, tillDate, chargeOnDueDate, installment, accrualPeriods); + } + return accrualPeriods; + } + + @NotNull + private List getInstallmentsToAccrue(@NotNull Loan loan, @NotNull LocalDate tillDate, + boolean periodic) { + LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + return loan.getRepaymentScheduleInstallments(i -> (!isChargeOnDueDate() + || (periodic ? !isBeforePeriod(tillDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber)) + : isFullPeriod(tillDate, i))) + && !isAfterPeriod(organisationStartDate, i)); + } + + private void addInterestAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, LoanScheduleGenerator scheduleGenerator, + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + AccrualPeriodData period = accrualPeriods.getPeriodByInstallmentNumber(installment.getInstallmentNumber()); + MonetaryCurrency currency = accrualPeriods.getCurrency(); + Money interest = null; + boolean isFullPeriod = isFullPeriod(tillDate, installment); + if (isFullPeriod) { + interest = installment.getInterestCharged(currency); + } else if (isInPeriod(tillDate, installment, false)) { // first period first day is not accrued + interest = scheduleGenerator.getPeriodInterestTillDate(loan, installment, tillDate); + } + period.setInterestAmount(interest); + Money interestAccruable = null; + if (!MathUtil.isEmpty(interest)) { + Money waived = isFullPeriod ? installment.getInterestWaived(currency) + : Money.of(currency, calcInterestWaivedAmount(installment, tillDate)); + interestAccruable = MathUtil.minusToZero(period.getInterestAmount(), waived); + } + period.setInterestAccruable(interestAccruable); + period.setInterestAccrued( + isFullPeriod ? installment.getInterestAccrued(currency) : Money.of(currency, calcInterestAccruedAmount(installment))); + } + + @NotNull + private BigDecimal calcInterestWaivedAmount(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate tillDate) { + Predicate transactionPredicate = t -> !t.isReversed() && t.isInterestWaiver() + && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + BigDecimal waivedAmount = installment.getLoanTransactionToRepaymentScheduleMappings().stream() + .filter(tm -> transactionPredicate.test(tm.getLoanTransaction())) + .map(LoanTransactionToRepaymentScheduleMapping::getInterestPortion).reduce(BigDecimal.ZERO, MathUtil::add); + Loan loan = installment.getLoan(); + // unrecognized amount of the transaction is not mapped to installments + BigDecimal unrecognizedAmount = loan.getLoanTransactions().stream().filter(transactionPredicate) + .map(LoanTransaction::getUnrecognizedIncomePortion).reduce(BigDecimal.ZERO, MathUtil::add); + // total interest from previous periods + BigDecimal interestAmount = loan.getRepaymentScheduleInstallments().stream().filter(i -> isAfterPeriod(tillDate, i)) + .map(LoanRepaymentScheduleInstallment::getInterestCharged).reduce(BigDecimal.ZERO, MathUtil::add); + // unrecognized amount left for this period (and maybe more) + unrecognizedAmount = MathUtil.subtractToZero(unrecognizedAmount, interestAmount); + return MathUtil.add(waivedAmount, unrecognizedAmount); + } + + @NotNull + private BigDecimal calcInterestAccruedAmount(@NotNull LoanRepaymentScheduleInstallment installment) { + return installment.getLoanTransactionToRepaymentScheduleMappings().stream().filter(tm -> { + LoanTransaction t = tm.getLoanTransaction(); + return !t.isReversed() && (t.isAccrual() || t.isAccrualAdjustment()); + }).map(tm -> tm.getLoanTransaction().isAccrual() ? tm.getInterestPortion() : MathUtil.negate(tm.getInterestPortion())) + .reduce(BigDecimal.ZERO, MathUtil::add); + } + + private void addChargeAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + AccrualPeriodData period = accrualPeriods.getPeriodByInstallmentNumber(installment.getInstallmentNumber()); + LocalDate dueDate = installment.getDueDate(); + List loanCharges = loan + .getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate) + : isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod()))); + for (LoanCharge loanCharge : loanCharges) { + addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods); + } + } + + private void addChargeAccrual(@NotNull LoanCharge loanCharge, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + MonetaryCurrency currency = accrualPeriods.getCurrency(); + Integer firstInstallmentNumber = accrualPeriods.getFirstInstallmentNumber(); + boolean installmentFee = loanCharge.isInstalmentFee(); + LoanRepaymentScheduleInstallment dueInstallment = (installmentFee || chargeOnDueDate) ? installment + : loanCharge.getLoan().getRepaymentScheduleInstallment( + i -> isInPeriod(loanCharge.getDueDate(), i, i.getInstallmentNumber().equals(firstInstallmentNumber))); + AccrualPeriodData duePeriod = accrualPeriods.getPeriodByInstallmentNumber(dueInstallment.getInstallmentNumber()); + boolean isFullPeriod = isFullPeriod(tillDate, dueInstallment); + + Money chargeAmount; + Money waived; + Collection paidBys; + Long installmentChargeId = null; + if (installmentFee) { + LoanInstallmentCharge installmentCharge = loanCharge.getInstallmentLoanCharge(dueInstallment.getInstallmentNumber()); + if (installmentCharge == null) { + return; + } + chargeAmount = installmentCharge.getAmount(currency); + paidBys = loanCharge.getLoanChargePaidBy(pb -> dueInstallment.getInstallmentNumber().equals(pb.getInstallmentNumber())); + waived = isFullPeriod ? installmentCharge.getAmountWaived(currency) + : Money.of(currency, calcChargeWaivedAmount(paidBys, tillDate)); + installmentChargeId = installmentCharge.getId(); + } else { + chargeAmount = loanCharge.getAmount(currency); + paidBys = loanCharge.getLoanChargePaidBySet(); + waived = isFullPeriod ? loanCharge.getAmountWaived(currency) : Money.of(currency, calcChargeWaivedAmount(paidBys, tillDate)); + } + AccrualChargeData chargeData = new AccrualChargeData(loanCharge.getId(), installmentChargeId, loanCharge.isPenaltyCharge()) + .setChargeAmount(chargeAmount); + chargeData.setChargeAccruable(MathUtil.minusToZero(chargeAmount, waived)); + chargeData.setChargeAccrued(Money.of(currency, calcChargeAccruedAmount(paidBys))); + + duePeriod.addCharge(chargeData); + } + + @NotNull + private BigDecimal calcChargeWaivedAmount(@NotNull Collection loanChargePaidBy, @NotNull LocalDate tillDate) { + return loanChargePaidBy.stream().filter(pb -> { + LoanTransaction t = pb.getLoanTransaction(); + return !t.isReversed() && t.isWaiveCharge() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + }).map(LoanChargePaidBy::getAmount).reduce(BigDecimal.ZERO, MathUtil::add); + } + + @NotNull + private BigDecimal calcChargeAccruedAmount(@NotNull Collection loanChargePaidBy) { + return loanChargePaidBy.stream().filter(pb -> { + LoanTransaction t = pb.getLoanTransaction(); + return !t.isReversed() && (t.isAccrual() || t.isAccrualAdjustment()); + }).map(pb -> pb.getLoanTransaction().isAccrual() ? pb.getAmount() : MathUtil.negate(pb.getAmount())).reduce(BigDecimal.ZERO, + MathUtil::add); + } + + private boolean isChargeDue(@NotNull LoanCharge loanCharge, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) { + LocalDate fromDate = installment.getFromDate(); + LocalDate dueDate = installment.getDueDate(); + LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; + return chargeOnDueDate ? loanCharge.isDueInPeriod(fromDate, toDate, isFirstPeriod) + : isInPeriod(loanCharge.getSubmittedOnDate(), fromDate, toDate, isFirstPeriod); + } + + private LoanTransaction addAccrualTransaction(@NotNull Loan loan, @NotNull LocalDate transactionDate, AccrualPeriodData accrualAmounts, + Money interestPortion, Money feePortion, Money penaltyPortion, boolean adjustment) { + interestPortion = MathUtil.negativeToZero(interestPortion); + BigDecimal interest = MathUtil.toBigDecimal(interestPortion); + feePortion = MathUtil.negativeToZero(feePortion); + BigDecimal fee = MathUtil.toBigDecimal(feePortion); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + BigDecimal penalty = MathUtil.toBigDecimal(penaltyPortion); + BigDecimal amount = MathUtil.add(interest, fee, penalty); + if (!MathUtil.isGreaterThanZero(amount)) { + return null; + } + LoanTransaction transaction = adjustment + ? accrualAdjustment(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()) + : accrueTransaction(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()); + loan.addLoanTransaction(transaction); + + // update repayment schedule portions + addTransactionMappings(transaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, adjustment); + return transaction; + } + + private void mergeAccrualTransaction(@NotNull LoanTransaction transaction, AccrualPeriodData accrualAmounts, Money interestPortion, + Money feePortion, Money penaltyPortion, boolean adjustment) { + interestPortion = MathUtil.negativeToZero(interestPortion); + feePortion = MathUtil.negativeToZero(feePortion); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + if (MathUtil.isEmpty(interestPortion) && MathUtil.isEmpty(feePortion) && MathUtil.isEmpty(penaltyPortion)) { + return; + } + + transaction.updateComponentsAndTotal(null, interestPortion, feePortion, penaltyPortion); + // update repayment schedule portions + addTransactionMappings(transaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, adjustment); + } + + private void addTransactionMappings(@NotNull LoanTransaction transaction, AccrualPeriodData accrualAmounts, Money interestPortion, + Money feePortion, Money penaltyPortion, boolean adjustment) { + Loan loan = transaction.getLoan(); + // update repayment schedule portions + Integer installmentNumber = accrualAmounts.getInstallmentNumber(); + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(installmentNumber); + + // add installment mapping + LoanTransactionToRepaymentScheduleMapping installmentMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, + installment, null, interestPortion, feePortion, penaltyPortion); + installment.getLoanTransactionToRepaymentScheduleMappings().add(installmentMapping); + transaction.getLoanTransactionToRepaymentScheduleMappings().add(installmentMapping); + + // add charges paid by mappings + addPaidByMappings(transaction, installment, accrualAmounts, adjustment); + } + + private void addPaidByMappings(@NotNull LoanTransaction transaction, LoanRepaymentScheduleInstallment installment, + AccrualPeriodData accrualAmounts, boolean adjustment) { + Loan loan = installment.getLoan(); + MonetaryCurrency currency = loan.getCurrency(); + for (AccrualChargeData accrualCharge : accrualAmounts.getCharges()) { + Money chargeAccruable = MathUtil.nullToZero(accrualCharge.getChargeAccruable(), currency); + Money chargePortion = MathUtil.minus(chargeAccruable, accrualCharge.getChargeAccrued()); + chargePortion = MathUtil.negativeToZero(adjustment ? MathUtil.negate(chargePortion) : chargePortion); + if (MathUtil.isEmpty(chargePortion)) { + continue; + } + BigDecimal chargeAmount = MathUtil.toBigDecimal(chargePortion); + LoanCharge loanCharge = loan.fetchLoanChargesById(accrualCharge.getLoanChargeId()); + LoanChargePaidBy paidBy = new LoanChargePaidBy(transaction, loanCharge, chargeAmount, installment.getInstallmentNumber()); + loanCharge.getLoanChargePaidBySet().add(paidBy); + transaction.getLoanChargesPaid().add(paidBy); + Long installmentChargeId = accrualCharge.getLoanInstallmentChargeId(); + if (installmentChargeId != null) { + LoanInstallmentCharge installmentCharge = new LoanInstallmentCharge(chargeAmount, loanCharge, installment); + loanCharge.getLoanInstallmentCharge().add(installmentCharge); + installment.getInstallmentCharges().add(installmentCharge); } - final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); - final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - processIncomePostingAndAccruals(loan); - this.loanRepositoryWrapper.saveAndFlush(loan); - postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); } } + private boolean isFullPeriod(@NotNull LocalDate tillDate, @NotNull LoanRepaymentScheduleInstallment installment) { + return isAfterPeriod(tillDate, installment) || DateUtils.isEqual(tillDate, installment.getDueDate()); + } + /** * method updates accrual derived fields on installments and reverse the unprocessed transactions for loan * reschedule */ @Override - public void reprocessExistingAccruals(Loan loan) { - Collection accruals = retrieveListOfAccrualTransactions(loan); - if (!accruals.isEmpty()) { + public void reprocessExistingAccruals(@NotNull Loan loan) { + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + if (!accrualTransactions.isEmpty()) { + accrualTransactions.forEach(this::reprocessInstallmentMappings); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - reprocessPeriodicAccruals(loan, accruals); + reprocessPeriodicAccruals(loan, accrualTransactions); } else if (loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - reprocessNonPeriodicAccruals(loan, accruals); + reprocessNonPeriodicAccruals(loan, accrualTransactions); } } } @@ -244,62 +496,79 @@ public void reprocessExistingAccruals(Loan loan) { */ @Override @Transactional - public void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled) { + public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) { + if (isProgressiveAccrual(loan)) { + return; + } LocalDate accruedTill = loan.getAccruedTill(); - if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestRecalculationEnabled || accruedTill == null - || loan.isNpa() || !loan.getStatus().isActive() || loan.isChargedOff()) { + if (!isInterestRecalculationEnabled || accruedTill == null) { return; } - - Collection loanScheduleAccrualList = new ArrayList<>(); - accruedTill = createLoanScheduleAccrualDataList(loan, accruedTill, loanScheduleAccrualList); - - if (!loanScheduleAccrualList.isEmpty()) { - try { - addPeriodicAccruals(accruedTill, loanScheduleAccrualList); - } catch (MultiException e) { - String globalisationMessageCode = "error.msg.accrual.exception"; - throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); - } + try { + addPeriodicAccruals(accruedTill, loan); + } catch (Exception e) { + String globalisationMessageCode = "error.msg.accrual.exception"; + throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); } - } /** * method calculates accruals for loan with interest recalculation and compounding to be posted as income */ @Override - public void processIncomePostingAndAccruals(Loan loan) { - if (loan.getLoanInterestRecalculationDetails() != null - && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()) { - LocalDate lastCompoundingDate = loan.getDisbursementDate(); - List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); - List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); - List accrualTransactions = retrieveListOfAccrualTransactions(loan); - for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { - if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { - break; - } - LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); - LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); - addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); - lastCompoundingDate = compoundingDetail.getEffectiveDate(); + public void processIncomePostingAndAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; + } + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails == null || !recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + LocalDate lastCompoundingDate = loan.getDisbursementDate(); + List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); + List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { + if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { + break; } - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment - .getLastNonDownPaymentInstallment(installments); - reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); - reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); + LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); + LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); + addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); + lastCompoundingDate = compoundingDetail.getEffectiveDate(); } + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments); + reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); + reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); + } + + @Transactional + @Override + public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException { + if (loanId == null) { + return; + } + Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + if (isProgressiveAccrual(loan)) { + return; + } + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + processIncomePostingAndAccruals(loan); + this.loanRepositoryWrapper.saveAndFlush(loan); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); } /** * method calculates accruals for loan on loan closure */ @Override - public void processAccrualsForLoanClosure(Loan loan) { + public void processAccrualsForLoanClosure(@NotNull Loan loan) { + reprocessExistingAccruals(loan); // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income - processAccrualTransactionsOnLoanClosure(loan); + addAccruals(loan, loan.getLastLoanRepaymentScheduleInstallment().getDueDate(), false, true, false); // check and process accruals for loan WITH interest recalculation details and compounding posted as income processIncomeAndAccrualTransactionOnLoanClosure(loan); @@ -309,8 +578,9 @@ public void processAccrualsForLoanClosure(Loan loan) { * method calculates accruals for loan on loan fore closure */ @Override - public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions) { + public void processAccrualsForLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate, + @NotNull List newAccrualTransactions) { + // TODO implement progressive accrual case if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); @@ -333,492 +603,22 @@ public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDa } } - private void calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(final LocalDate tillDate, - final LoanScheduleAccrualData accrualData) { - - BigDecimal amount = BigDecimal.ZERO; - BigDecimal feePortion = accrualData.getDueDateFeeIncome(); - BigDecimal penaltyPortion = accrualData.getDueDatePenaltyIncome(); - BigDecimal interestPortion = getInterestAccruedTillDate(tillDate, accrualData); - - BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); - BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); - BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty(); - BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); - BigDecimal totalCreditedFee = accrualData.getCreditedFee(); - - // interest - if (totalAccInterest == null) { - totalAccInterest = BigDecimal.ZERO; - } - interestPortion = interestPortion.subtract(totalAccInterest); - amount = amount.add(interestPortion); - totalAccInterest = totalAccInterest.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; - } - - // fee - if (feePortion != null) { - if (totalAccFee == null) { - totalAccFee = BigDecimal.ZERO; - } - if (totalCreditedFee == null) { - totalCreditedFee = BigDecimal.ZERO; - } - feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee); - amount = amount.add(feePortion); - totalAccFee = totalAccFee.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; - } - } - - // penalty - if (penaltyPortion != null) { - if (totalAccPenalty == null) { - totalAccPenalty = BigDecimal.ZERO; - } - if (totalCreditedPenalty == null) { - totalCreditedPenalty = BigDecimal.ZERO; - } - penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty); - amount = amount.add(penaltyPortion); - totalAccPenalty = totalAccPenalty.add(penaltyPortion); - if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { - penaltyPortion = null; - } - } - - if (amount.compareTo(BigDecimal.ZERO) > 0) { - addAccrualAccounting(accrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion, - totalAccPenalty, tillDate); - } - } - - private BigDecimal getInterestAccruedTillDate(LocalDate tillDate, LoanScheduleAccrualData accrualData) { - BigDecimal interestPortion; - LocalDate interestStartDate = accrualData.getFromDateAsLocaldate(); - if (DateUtils.isBefore(accrualData.getFromDateAsLocaldate(), accrualData.getInterestCalculatedFrom())) { - if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), accrualData.getDueDateAsLocaldate())) { - interestStartDate = accrualData.getInterestCalculatedFrom(); - } else { - interestStartDate = accrualData.getDueDateAsLocaldate(); - } - } - - int totalNumberOfDays = DateUtils.getExactDifferenceInDays(interestStartDate, accrualData.getDueDateAsLocaldate()); - LocalDate startDate = accrualData.getFromDateAsLocaldate(); - if (DateUtils.isBefore(startDate, accrualData.getInterestCalculatedFrom())) { - if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), tillDate)) { - startDate = accrualData.getInterestCalculatedFrom(); - } else { - startDate = tillDate; - } - } - int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, tillDate); - double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays; - - if (daysToBeAccrued >= totalNumberOfDays) { - interestPortion = accrualData.getAccruableIncome(); - } else { - interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); - } - interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode()); - return interestPortion; - } - - private void calculateFinalAccrualsForScheduleAndAddAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { - - BigDecimal amount = BigDecimal.ZERO; - BigDecimal interestPortion = null; - BigDecimal totalAccInterest = null; - - // interest - if (scheduleAccrualData.getAccruableIncome() != null) { - interestPortion = scheduleAccrualData.getAccruableIncome(); - totalAccInterest = interestPortion; - if (scheduleAccrualData.getAccruedInterestIncome() != null) { - interestPortion = interestPortion.subtract(scheduleAccrualData.getAccruedInterestIncome()); - } - amount = amount.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; - } - } - - // fee - BigDecimal feePortion = null; - BigDecimal totalAccFee = null; - if (scheduleAccrualData.getDueDateFeeIncome() != null) { - feePortion = scheduleAccrualData.getDueDateFeeIncome(); - totalAccFee = feePortion; - if (scheduleAccrualData.getAccruedFeeIncome() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome()); - } - if (scheduleAccrualData.getCreditedFee() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee()); - } - amount = amount.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; - } - } - - // penalty - BigDecimal penaltyPortion = null; - BigDecimal totalAccPenalty = null; - if (scheduleAccrualData.getDueDatePenaltyIncome() != null) { - penaltyPortion = scheduleAccrualData.getDueDatePenaltyIncome(); - totalAccPenalty = penaltyPortion; - if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); - } - if (scheduleAccrualData.getCreditedPenalty() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty()); - } - amount = amount.add(penaltyPortion); - if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { - penaltyPortion = null; - } - } - - if (amount.compareTo(BigDecimal.ZERO) > 0) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate()); - } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate()); - } - } - } - - private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestPortion, - BigDecimal totalAccInterest, BigDecimal feePortion, BigDecimal totalAccFee, BigDecimal penaltyPortion, - BigDecimal totalAccPenalty, final LocalDate accruedTill) throws DataAccessException { - - AppUser user = context.authenticatedUser(); - Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId()); - Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId()); - MonetaryCurrency currency = loan.getCurrency(); - - // create accrual Transaction - LoanTransaction loanTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, - externalIdFactory.create()); - - // update charges paid by - Map applicableCharges = scheduleAccrualData.getApplicableCharges(); - - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - // - LoanCharge loanCharge = loanChargeRepository.getReferenceById(chargeData.getId()); - loanTransaction.getLoanChargesPaid() - .add(new LoanChargePaidBy(loanTransaction, loanCharge, entry.getValue(), scheduleAccrualData.getInstallmentNumber())); - - } - - loanTransactionRepository.saveAndFlush(loanTransaction); - loan.addLoanTransaction(loanTransaction); - - Map transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion, - scheduleAccrualData, accruedTill); - - // update repayment schedule portions - - LoanRepaymentScheduleInstallment loanScheduleInstallment = loan - .fetchLoanRepaymentScheduleInstallmentByDueDate(scheduleAccrualData.getDueDate()); - loanScheduleInstallment.updateAccrualPortion(Money.of(currency, totalAccInterest), Money.of(currency, totalAccFee), - Money.of(currency, totalAccPenalty)); - - // update loan accrued till date - loan.setAccruedTill(accruedTill); - loan.setLastModifiedBy(user.getId()); - loan.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); - - loanRepository.saveAndFlush(loan); - - businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction)); - - final Map accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); - this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); - } - - private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, - final Map transactionMap) { - + private Map deriveAccountingBridgeData(@NotNull Loan loan, List> newLoanTransactions) { final Map accountingBridgeData = new LinkedHashMap<>(); - accountingBridgeData.put("loanId", loanScheduleAccrualData.getLoanId()); - accountingBridgeData.put("loanProductId", loanScheduleAccrualData.getLoanProductId()); - accountingBridgeData.put("officeId", loanScheduleAccrualData.getOfficeId()); - accountingBridgeData.put("currencyCode", loanScheduleAccrualData.getCurrencyData().getCode()); - accountingBridgeData.put("cashBasedAccountingEnabled", false); - accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", false); - accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", true); + accountingBridgeData.put("loanId", loan.getId()); + accountingBridgeData.put("loanProductId", loan.getLoanProduct().getId()); + accountingBridgeData.put("officeId", loan.getOfficeId()); + accountingBridgeData.put("currencyCode", loan.getCurrencyCode()); + accountingBridgeData.put("cashBasedAccountingEnabled", loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()); + accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", loan.isUpfrontAccrualAccountingEnabledOnLoanProduct()); + accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()); accountingBridgeData.put("isAccountTransfer", false); accountingBridgeData.put("isChargeOff", false); accountingBridgeData.put("isFraud", false); - - final List> newLoanTransactions = new ArrayList<>(); - newLoanTransactions.add(transactionMap); - accountingBridgeData.put("newLoanTransactions", newLoanTransactions); return accountingBridgeData; } - public Map toMapData(final Long id, final BigDecimal amount, final BigDecimal interestPortion, - final BigDecimal feePortion, final BigDecimal penaltyPortion, final LoanScheduleAccrualData loanScheduleAccrualData, - final LocalDate accruedTill) { - final Map thisTransactionData = new LinkedHashMap<>(); - - final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL); - - thisTransactionData.put("id", id); - thisTransactionData.put("officeId", loanScheduleAccrualData.getOfficeId()); - thisTransactionData.put("type", transactionType); - thisTransactionData.put("reversed", false); - thisTransactionData.put("date", accruedTill); - thisTransactionData.put("currency", loanScheduleAccrualData.getCurrencyData()); - thisTransactionData.put("amount", amount); - thisTransactionData.put("principalPortion", null); - thisTransactionData.put("interestPortion", interestPortion); - thisTransactionData.put("feeChargesPortion", feePortion); - thisTransactionData.put("penaltyChargesPortion", penaltyPortion); - thisTransactionData.put("overPaymentPortion", null); - - Map applicableCharges = loanScheduleAccrualData.getApplicableCharges(); - if (applicableCharges != null && !applicableCharges.isEmpty()) { - final List> loanChargesPaidData = new ArrayList<>(); - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - final Map loanChargePaidData = new LinkedHashMap<>(); - loanChargePaidData.put("chargeId", chargeData.getChargeId()); - loanChargePaidData.put("isPenalty", chargeData.isPenalty()); - loanChargePaidData.put("loanChargeId", chargeData.getId()); - loanChargePaidData.put("amount", entry.getValue()); - - loanChargesPaidData.add(loanChargePaidData); - } - thisTransactionData.put("loanChargesPaid", loanChargesPaidData); - } - - return thisTransactionData; - } - - private void updateCharges(final Collection chargesData, final LoanScheduleAccrualData accrualData, - final LocalDate startDate, final LocalDate endDate) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { - updateChargeForDueDate(chargesData, accrualData, startDate, endDate); - } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate); - } - - } - - private void updateChargeForSubmittedOnDate(Collection chargesData, LoanScheduleAccrualData accrualData, - LocalDate startDate, LocalDate endDate) { - final Map applicableCharges = new HashMap<>(); - BigDecimal submittedDateFeeIncome = BigDecimal.ZERO; - BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO; - LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate(); - for (LoanChargeData loanCharge : chargesData) { - BigDecimal chargeAmount = BigDecimal.ZERO; - if (isChargeSubmittedDateAndDueDateInRange(accrualData, startDate, endDate, scheduleEndDate, loanCharge)) { - chargeAmount = loanCharge.getAmount(); - chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); - } - if (loanCharge.isPenalty()) { - submittedDatePenaltyIncome = submittedDatePenaltyIncome.add(chargeAmount); - } else { - submittedDateFeeIncome = submittedDateFeeIncome.add(chargeAmount); - } - - } - - if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDateFeeIncome = null; - } - - if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDatePenaltyIncome = null; - } - - accrualData.updateChargeDetails(applicableCharges, submittedDateFeeIncome, submittedDatePenaltyIncome); - } - - private boolean isChargeSubmittedDateAndDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, - LocalDate scheduleEndDate, LoanChargeData loanCharge) { - return ((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(startDate, loanCharge.getSubmittedOnDate()) - && DateUtils.isEqual(startDate, loanCharge.getDueDate())) || DateUtils.isBefore(startDate, loanCharge.getDueDate())) - && !DateUtils.isBefore(endDate, loanCharge.getSubmittedOnDate()) - && !DateUtils.isBefore(scheduleEndDate, loanCharge.getDueDate()); - } - - private void updateChargeForDueDate(Collection chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, - LocalDate endDate) { - final Map applicableCharges = new HashMap<>(); - BigDecimal dueDateFeeIncome = BigDecimal.ZERO; - BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; - for (LoanChargeData loanCharge : chargesData) { - BigDecimal chargeAmount = BigDecimal.ZERO; - if (loanCharge.getDueDate() == null) { - if (loanCharge.isInstallmentFee() && DateUtils.isEqual(endDate, accrualData.getDueDateAsLocaldate())) { - chargeAmount = calculateInstallmentFeeCharges(accrualData, applicableCharges, loanCharge, chargeAmount); - } - } else if (isChargeDueDateInRange(accrualData, startDate, endDate, loanCharge)) { - chargeAmount = loanCharge.getAmount(); - chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); - } - - if (loanCharge.isPenalty()) { - dueDatePenaltyIncome = dueDatePenaltyIncome.add(chargeAmount); - } else { - dueDateFeeIncome = dueDateFeeIncome.add(chargeAmount); - } - } - - if (dueDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDateFeeIncome = null; - } - - if (dueDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDatePenaltyIncome = null; - } - - accrualData.updateChargeDetails(applicableCharges, dueDateFeeIncome, dueDatePenaltyIncome); - } - - private boolean isChargeDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, - LoanChargeData loanCharge) { - return ((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(loanCharge.getDueDate(), startDate)) - || DateUtils.isAfter(loanCharge.getDueDate(), startDate)) && !DateUtils.isAfter(loanCharge.getDueDate(), endDate); - } - - private BigDecimal calculateDueDateCharges(Map applicableCharges, LoanChargeData loanCharge, - BigDecimal chargeAmount) { - BigDecimal dueDateChargeAmount = chargeAmount; - if (loanCharge.getAmountUnrecognized() != null) { - dueDateChargeAmount = dueDateChargeAmount.subtract(loanCharge.getAmountUnrecognized()); - } - boolean canAddCharge = dueDateChargeAmount.compareTo(BigDecimal.ZERO) > 0; - if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) { - BigDecimal amountForAccrual = dueDateChargeAmount; - if (loanCharge.getAmountAccrued() != null) { - amountForAccrual = dueDateChargeAmount.subtract(loanCharge.getAmountAccrued()); - } - applicableCharges.put(loanCharge, amountForAccrual); - } - return dueDateChargeAmount; - } - - private BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrualData, - Map applicableCharges, LoanChargeData loanCharge, BigDecimal chargeAmount) { - BigDecimal installmentFeeChargeAmount = chargeAmount; - Collection installmentData = loanCharge.getInstallmentChargeData(); - for (LoanInstallmentChargeData installmentChargeData : installmentData) { - - if (installmentChargeData.getInstallmentNumber().equals(accrualData.getInstallmentNumber())) { - BigDecimal accruableForInstallment = installmentChargeData.getAmount(); - if (installmentChargeData.getAmountUnrecognized() != null) { - accruableForInstallment = accruableForInstallment.subtract(installmentChargeData.getAmountUnrecognized()); - } - installmentFeeChargeAmount = accruableForInstallment; - boolean canAddCharge = installmentFeeChargeAmount.compareTo(BigDecimal.ZERO) > 0; - if (canAddCharge && (installmentChargeData.getAmountAccrued() == null - || installmentFeeChargeAmount.compareTo(installmentChargeData.getAmountAccrued()) != 0)) { - BigDecimal amountForAccrual = installmentFeeChargeAmount; - if (installmentChargeData.getAmountAccrued() != null) { - amountForAccrual = installmentFeeChargeAmount.subtract(installmentChargeData.getAmountAccrued()); - } - applicableCharges.put(loanCharge, amountForAccrual); - BigDecimal amountAccrued = installmentFeeChargeAmount; - if (loanCharge.getAmountAccrued() != null) { - amountAccrued = amountAccrued.add(loanCharge.getAmountAccrued()); - } - loanCharge.updateAmountAccrued(amountAccrued); - } - break; - } - } - return installmentFeeChargeAmount; - } - - private void updateInterestIncome(final LoanScheduleAccrualData accrualData, - final Collection loanWaiverTransactions, - final Collection loanSchedulePeriodDataList, final LocalDate tillDate) { - - BigDecimal interestIncome = BigDecimal.ZERO; - if (accrualData.getInterestIncome() != null) { - interestIncome = accrualData.getInterestIncome(); - } - if (accrualData.getWaivedInterestIncome() != null) { - Collection loanTransactionDatas = new ArrayList<>(); - - getLoanWaiverTransactionsInRange(accrualData, loanWaiverTransactions, tillDate, loanTransactionDatas); - - BigDecimal recognized = getWaivedInterestIncome(accrualData, loanSchedulePeriodDataList, loanTransactionDatas); - - BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); - if (interestWaived.compareTo(recognized) > 0) { - interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); - } - } - - accrualData.updateAccruableIncome(interestIncome); - } - - private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, - Collection loanSchedulePeriodDataList, Collection loanTransactionDatas) { - BigDecimal recognized = BigDecimal.ZERO; - BigDecimal unrecognized = BigDecimal.ZERO; - BigDecimal remainingAmt = BigDecimal.ZERO; - - Iterator iterator = loanTransactionDatas.iterator(); - for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) { - if (MathUtil.isLessThanOrEqualZero(recognized) && MathUtil.isLessThanOrEqualZero(unrecognized) && iterator.hasNext()) { - LoanTransactionData loanTransactionData = iterator.next(); - recognized = recognized.add(loanTransactionData.getInterestPortion()); - unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion()); - } - if (DateUtils.isBefore(loanSchedulePeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) { - remainingAmt = remainingAmt.add(loanSchedulePeriodData.getInterestWaived()); - if (recognized.compareTo(remainingAmt) > 0) { - recognized = recognized.subtract(remainingAmt); - remainingAmt = BigDecimal.ZERO; - } else { - remainingAmt = remainingAmt.subtract(recognized); - recognized = BigDecimal.ZERO; - if (unrecognized.compareTo(remainingAmt) >= 0) { - unrecognized = unrecognized.subtract(remainingAmt); - remainingAmt = BigDecimal.ZERO; - } else if (iterator.hasNext()) { - remainingAmt = remainingAmt.subtract(unrecognized); - unrecognized = BigDecimal.ZERO; - } - } - - } - } - return recognized; - } - - private void getLoanWaiverTransactionsInRange(LoanScheduleAccrualData accrualData, - Collection loanWaiverTransactions, LocalDate tillDate, - Collection loanTransactionDatas) { - for (LoanTransactionData loanTransactionData : loanWaiverTransactions) { - LocalDate transactionDate = loanTransactionData.getDate(); - if (!DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate()) - || (DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate()) - && !DateUtils.isAfter(transactionDate, accrualData.getDueDateAsLocaldate()) - && !DateUtils.isAfter(transactionDate, tillDate))) { - loanTransactionDatas.add(loanTransactionData); - } - } - } - private void postJournalEntries(final Loan loan, final List existingTransactionIds, final List existingReversedTransactionIds) { final MonetaryCurrency currency = loan.getCurrency(); @@ -828,45 +628,87 @@ private void postJournalEntries(final Loan loan, final List existingTransa journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - private void reprocessPeriodicAccruals(Loan loan, final Collection accruals) { - if (!loan.isChargedOff()) { - List installments = loan.getRepaymentScheduleInstallments(); - boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge() - .equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE); - for (LoanRepaymentScheduleInstallment installment : installments) { - checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment); - } - // reverse accruals after last installment - LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); - reverseTransactionsPostEffectiveDate(accruals, lastInstallment.getDueDate()); + private void reprocessPeriodicAccruals(Loan loan, final List accrualTransactions) { + if (loan.isChargedOff()) { + return; } + List installments = loan.getRepaymentScheduleInstallments(); + boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate(); + for (LoanRepaymentScheduleInstallment installment : installments) { + checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment); + } + // reverse accruals after last installment + LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); + reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); } - private void checkAndUpdateAccrualsForInstallment(Loan loan, Collection accruals, + private void checkAndUpdateAccrualsForInstallment(Loan loan, List accrualTransactions, List installments, boolean isBasedOnSubmittedOnDate, LoanRepaymentScheduleInstallment installment) { - Money interest = Money.zero(loan.getCurrency()); - Money fee = Money.zero(loan.getCurrency()); - Money penalty = Money.zero(loan.getCurrency()); - for (LoanTransaction loanTransaction : accruals) { - LocalDate transactionDateForRange = getDateForRangeCalculation(loanTransaction, isBasedOnSubmittedOnDate); - boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); - if (isInPeriod) { - interest = interest.plus(loanTransaction.getInterestPortion(loan.getCurrency())); - fee = fee.plus(loanTransaction.getFeeChargesPortion(loan.getCurrency())); - penalty = penalty.plus(loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); - if (hasIncomeAmountChangedForInstallment(loan, installment, interest, fee, penalty, loanTransaction)) { - interest = interest.minus(loanTransaction.getInterestPortion(loan.getCurrency())); - fee = fee.minus(loanTransaction.getFeeChargesPortion(loan.getCurrency())); - penalty = penalty.minus(loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); - loanTransaction.reverse(); + MonetaryCurrency currency = loan.getCurrency(); + Money zero = Money.zero(currency); + Money interest = zero; + Money fee = zero; + Money penalty = zero; + if (isProgressiveAccrual(loan)) { + List mappings = accrualTransactions.stream() + .flatMap(t -> t.getLoanTransactionToRepaymentScheduleMappings().stream().filter( + m -> m.getLoanRepaymentScheduleInstallment().getInstallmentNumber().equals(installment.getInstallmentNumber()))) + .toList(); + for (LoanTransactionToRepaymentScheduleMapping mapping : mappings) { + boolean accrual = mapping.getLoanTransaction().isAccrual(); + Money interestPortion = mapping.getInterestPortion(currency); + interest = accrual ? MathUtil.plus(interest, interestPortion) : MathUtil.minus(interest, interestPortion); + Money feePortion = mapping.getFeeChargesPortion(currency); + fee = accrual ? MathUtil.plus(fee, feePortion) : MathUtil.minus(fee, feePortion); + Money penaltyPortion = mapping.getPenaltyChargesPortion(currency); + penalty = accrual ? MathUtil.plus(penalty, penaltyPortion) : MathUtil.minus(penalty, penaltyPortion); + } + interest = MathUtil.negativeToZero(interest); + fee = MathUtil.negativeToZero(fee); + penalty = MathUtil.negativeToZero(penalty); + } else { + for (LoanTransaction accrualTransaction : accrualTransactions) { + LocalDate transactionDateForRange = getDateForRangeCalculation(accrualTransaction, isBasedOnSubmittedOnDate); + boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); + if (isInPeriod) { + interest = MathUtil.plus(interest, accrualTransaction.getInterestPortion(currency)); + fee = MathUtil.plus(fee, accrualTransaction.getFeeChargesPortion(currency)); + penalty = MathUtil.plus(penalty, accrualTransaction.getPenaltyChargesPortion(currency)); + if (hasIncomeAmountChangedForInstallment(loan, installment, interest, fee, penalty, accrualTransaction)) { + interest = interest.minus(accrualTransaction.getInterestPortion(currency)); + fee = fee.minus(accrualTransaction.getFeeChargesPortion(currency)); + penalty = penalty.minus(accrualTransaction.getPenaltyChargesPortion(currency)); + accrualTransaction.reverse(); + } } - } } installment.updateAccrualPortion(interest, fee, penalty); } + private void reprocessInstallmentMappings(LoanTransaction accrualTransaction) { + Set mappings = accrualTransaction.getLoanTransactionToRepaymentScheduleMappings(); + List newMappings = new ArrayList<>(mappings.size()); + for (LoanTransactionToRepaymentScheduleMapping mapping : mappings) { + LoanRepaymentScheduleInstallment mappedInstallment = mapping.getLoanRepaymentScheduleInstallment(); + LoanRepaymentScheduleInstallment loanInstallment = accrualTransaction.getLoan() + .fetchRepaymentScheduleInstallment(mappedInstallment.getInstallmentNumber()); + if (mappedInstallment == loanInstallment) { + newMappings.add(mapping); + } else { + LoanTransactionToRepaymentScheduleMapping newMapping = LoanTransactionToRepaymentScheduleMapping + .createFrom(accrualTransaction, loanInstallment, null, null, null, null); + newMapping.setComponents(mapping.getPrincipalPortion(), mapping.getInterestPortion(), mapping.getFeeChargesPortion(), + mapping.getPenaltyChargesPortion()); + newMappings.add(newMapping); + loanInstallment.getLoanTransactionToRepaymentScheduleMappings().add(newMapping); + } + } + mappings.clear(); + mappings.addAll(newMappings); + } + private boolean hasIncomeAmountChangedForInstallment(Loan loan, LoanRepaymentScheduleInstallment installment, Money interest, Money fee, Money penalty, LoanTransaction loanTransaction) { // if installment income amount is changed or if loan is interest bearing and interest income not accrued @@ -884,7 +726,10 @@ private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, bo : loanTransaction.getTransactionDate(); } - private void reprocessNonPeriodicAccruals(Loan loan, final Collection accruals) { + private void reprocessNonPeriodicAccruals(Loan loan, final List accruals) { + if (isProgressiveAccrual(loan)) { + return; + } final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); ExternalId externalId = ExternalId.empty(); boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); @@ -914,82 +759,8 @@ private void reprocessNonPeriodicAccruals(Loan loan, final Collection loanScheduleAccrualList) { - boolean isOrganisationDateEnabled = configurationDomainService.isOrganisationstartDateEnabled(); - LocalDate organisationStartDate = DateUtils.getBusinessLocalDate(); - if (isOrganisationDateEnabled) { - organisationStartDate = configurationDomainService.retrieveOrganisationStartDate(); - } - List installments = loan.getRepaymentScheduleInstallments(); - Long loanId = loan.getId(); - Long officeId = loan.getOfficeId(); - LocalDate accrualStartDate = null; - PeriodFrequencyType repaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType(); - Integer repayEvery = loan.repaymentScheduleDetail().getRepayEvery(); - LocalDate interestCalculatedFrom = loan.getInterestChargedFromDate(); - Long loanProductId = loan.productId(); - MonetaryCurrency currency = loan.getCurrency(); - ApplicationCurrency applicationCurrency = applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - CurrencyData currencyData = applicationCurrency.toData(); - Set loanCharges = loan.getActiveCharges(); - int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); - - for (LoanRepaymentScheduleInstallment installment : installments) { - if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) { - accruedTill = DateUtils.getBusinessLocalDate(); - } - if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) { - boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); - generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate, - repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges, - installment, isFirstNormalInstallment); - } - } - return accruedTill; - } - - private void generateLoanScheduleAccrualData(final LocalDate accruedTill, - final Collection loanScheduleAccrualDatas, final Long loanId, Long officeId, - final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, - final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency, - final CurrencyData currencyData, final Set loanCharges, final LoanRepaymentScheduleInstallment installment, - boolean isFirstNormalInstallment) { - - if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate()) - && !DateUtils.isAfter(accruedTill, installment.getDueDate()))) { - BigDecimal dueDateFeeIncome = BigDecimal.ZERO; - BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; - LocalDate chargesTillDate = installment.getDueDate(); - if (!DateUtils.isAfter(accruedTill, installment.getDueDate())) { - chargesTillDate = accruedTill; - } - - for (final LoanCharge loanCharge : loanCharges) { - boolean isDue = loanCharge.isDueInPeriod(installment.getFromDate(), chargesTillDate, isFirstNormalInstallment); - if (isDue) { - if (loanCharge.isFeeCharge()) { - dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount()); - } else if (loanCharge.isPenaltyCharge()) { - dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount()); - } - } - } - LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(), - accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(), - installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(), - installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), - installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), - installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, - installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(), - installment.getCreditedPenalty(currency).getAmount()); - loanScheduleAccrualDatas.add(accrualData); - - } - } - private void createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions, MonetaryCurrency currency, Money interestPortion, Money feePortion, + List newAccrualTransactions, MonetaryCurrency currency, Money interestPortion, Money feePortion, Money penaltyPortion, Money total) { ExternalId accrualExternalId = externalIdFactory.create(); LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate, total.getAmount(), @@ -1024,7 +795,7 @@ private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate til receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) { + } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment() || transaction.isAccrualAdjustment()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); @@ -1047,7 +818,8 @@ private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate til } private List retrieveListOfAccrualTransactions(Loan loan) { - return loan.getLoanTransactions().stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) + return loan.getLoanTransactions().stream() + .filter(transaction -> transaction.isNotReversed() && (transaction.isAccrual() || transaction.isAccrualAdjustment())) .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } @@ -1122,13 +894,11 @@ private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtion } if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - if (existingAccrualTransaction == null) { - LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties, externalId); - updateLoanChargesPaidBy(loan, accrual, feeDetails, null); - loan.addLoanTransaction(accrual); - } else if (existingAccrualTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { - existingAccrualTransaction.reverse(); + if (existingAccrualTransaction == null + || !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) { + if (existingAccrualTransaction != null) { + existingAccrualTransaction.reverse(); + } LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), compoundingDetail.getAmount(), interest, fee, penalties, externalId); updateLoanChargesPaidBy(loan, accrual, feeDetails, null); @@ -1217,7 +987,7 @@ private void updateLoanChargesPaidBy(Loan loan, LoanTransaction accrual, Map transactions, LocalDate effectiveDate) { + private void reverseTransactionsPostEffectiveDate(List transactions, LocalDate effectiveDate) { for (LoanTransaction loanTransaction : transactions) { if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { loanTransaction.reverse(); @@ -1225,154 +995,8 @@ private void reverseTransactionsPostEffectiveDate(Collection tr } } - private void processAccrualTransactionsOnLoanClosure(Loan loan) { - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() - // to avoid collision with processIncomeAccrualTransactionOnLoanClosure() - && !(loan.getLoanInterestRecalculationDetails() != null - && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()) - && !loan.isNpa() && !loan.isChargedOff()) { - HashMap incomeDetails = new HashMap<>(); - MonetaryCurrency currency = loan.getCurrency(); - Money interestPortion = Money.zero(currency); - Money feePortion = Money.zero(currency); - Money penaltyPortion = Money.zero(currency); - - determineReceivableIncomeDetailsForLoanClosure(loan, incomeDetails); - - interestPortion = interestPortion.plus((Money) incomeDetails.get(Loan.INTEREST)); - feePortion = feePortion.plus((Money) incomeDetails.get(Loan.FEE)); - penaltyPortion = penaltyPortion.plus((Money) incomeDetails.get(Loan.PENALTIES)); - - Money total = interestPortion.plus(feePortion).plus(penaltyPortion); - - if (total.isGreaterThanZero()) { - LocalDate accrualTransactionDate = getFinalAccrualTransactionDate(loan); - LoanTransaction accrualTransaction = createAccrualTransaction(loan, interestPortion, feePortion, penaltyPortion, total, - accrualTransactionDate); - updateLoanChargesAndInstallmentChargesPaidBy(loan, accrualTransaction); - // TODO check if this is required - // saveLoanTransactionWithDataIntegrityViolationChecks(accrualTransaction); - accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction); - loan.addLoanTransaction(accrualTransaction); - businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); - - updateLoanInstallmentAccruedPortion(loan); - } - } - } - - private void updateLoanInstallmentAccruedPortion(Loan loan) { - MonetaryCurrency currency = loan.getCurrency(); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - installment.updateAccrualPortion(installment.getInterestCharged(currency).minus(installment.getInterestWaived(currency)), - installment.getFeeChargesCharged(currency).minus(installment.getFeeChargesWaived(currency)), - installment.getPenaltyChargesCharged(currency).minus(installment.getPenaltyChargesWaived(currency))); - }); - } - - private void updateLoanChargesAndInstallmentChargesPaidBy(Loan loan, LoanTransaction accrualTransaction) { - MonetaryCurrency currency = loan.getCurrency(); - Set accrualCharges = accrualTransaction.getLoanChargesPaid(); - - Map accrualDetails = loan.getActiveCharges().stream() - .collect(Collectors.toMap(LoanCharge::getId, v -> Money.zero(currency))); - - loan.getLoanTransactions(LoanTransaction::isAccrual).forEach(transaction -> { - transaction.getLoanChargesPaid().forEach(loanChargePaid -> { - accrualDetails.computeIfPresent(loanChargePaid.getLoanCharge().getId(), - (mappedKey, mappedValue) -> mappedValue.add(Money.of(currency, loanChargePaid.getAmount()))); - }); - }); - - loan.getActiveCharges().forEach(loanCharge -> { - Money amount = loanCharge.getAmount(currency).minus(loanCharge.getAmountWaived(currency)); - if (!loanCharge.isInstalmentFee() && loanCharge.isActive() && accrualDetails.get(loanCharge.getId()).isLessThan(amount)) { - Money amountToBeAccrued = amount.minus(accrualDetails.get(loanCharge.getId())); - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, - amountToBeAccrued.getAmount(), null); - accrualCharges.add(loanChargePaidBy); - loanCharge.getLoanChargePaidBySet().add(loanChargePaidBy); - } - }); - - for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) { - for (LoanInstallmentCharge installmentCharge : loanRepaymentScheduleInstallment.getInstallmentCharges()) { - if (installmentCharge.getLoanCharge().isActive()) { - Money notWaivedAmount = installmentCharge.getAmount(currency).minus(installmentCharge.getAmountWaived(currency)); - if (notWaivedAmount.isGreaterThanZero()) { - Money amountToBeAccrued = notWaivedAmount.minus(accrualDetails.get(installmentCharge.getLoanCharge().getId())); - if (amountToBeAccrued.isGreaterThanZero()) { - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, - installmentCharge.getLoanCharge(), amountToBeAccrued.getAmount(), - installmentCharge.getInstallment().getInstallmentNumber()); - accrualCharges.add(loanChargePaidBy); - installmentCharge.getLoanCharge().getLoanChargePaidBySet().add(loanChargePaidBy); - accrualDetails.computeIfPresent(installmentCharge.getLoanCharge().getId(), - (mappedKey, mappedValue) -> mappedValue.add(amountToBeAccrued)); - } - accrualDetails.computeIfPresent(installmentCharge.getLoanCharge().getId(), (mappedKey, mappedValue) -> MathUtil - .negativeToZero(mappedValue.minus(Money.of(currency, installmentCharge.getAmount())))); - } - } - } - } - } - - private LoanTransaction createAccrualTransaction(Loan loan, Money interestPortion, Money feePortion, Money penaltyPortion, Money total, - LocalDate accrualTransactionDate) { - ExternalId externalId = externalIdFactory.create(); - LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate, - total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId); - return accrualTransaction; - } - - private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map incomeDetails) { - MonetaryCurrency currency = loan.getCurrency(); - Money interestPortion = Money.zero(currency); - Money feePortion = Money.zero(currency); - Money penaltyPortion = Money.zero(currency); - - for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) { - // TODO: test with interest waiving - interestPortion = interestPortion.add(loanRepaymentScheduleInstallment.getInterestCharged(currency)) - .minus(loanRepaymentScheduleInstallment.getInterestAccrued(currency)) - .minus(loanRepaymentScheduleInstallment.getInterestWaived(currency)); - } - - for (LoanCharge loanCharge : loan.getLoanCharges()) { - if (!loanCharge.isActive()) { - continue; - } - BigDecimal accruedAmount = BigDecimal.ZERO; - BigDecimal waivedAmount = BigDecimal.ZERO; - for (LoanTransaction loanTransaction : loan.getLoanTransactions()) { - if (loanTransaction.isAccrual() || loanTransaction.isChargesWaiver()) { - for (LoanChargePaidBy loanChargePaidBy : loanTransaction.getLoanChargesPaid()) { - if (loanChargePaidBy.getLoanCharge().getId().equals(loanCharge.getId())) { - if (loanTransaction.isAccrual()) { - accruedAmount = accruedAmount.add(loanTransaction.getAmount()); - } else if (loanTransaction.isChargesWaiver()) { - waivedAmount = waivedAmount.add(loanTransaction.getAmount()); - } - } - } - } - } - Money needToAccrueAmount = MathUtil.negativeToZero(loanCharge.getAmount(currency).minus(accruedAmount).minus(waivedAmount)); - if (loanCharge.isPenaltyCharge()) { - penaltyPortion = penaltyPortion.add(needToAccrueAmount); - } else if (loanCharge.isFeeCharge()) { - feePortion = feePortion.add(needToAccrueAmount); - } - } - - incomeDetails.put(Loan.INTEREST, interestPortion); - incomeDetails.put(Loan.FEE, feePortion); - incomeDetails.put(Loan.PENALTIES, penaltyPortion); - - } - private void processIncomeAndAccrualTransactionOnLoanClosure(Loan loan) { + // TODO analyze progressive accrual case if (loan.getLoanInterestRecalculationDetails() != null && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction() && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { @@ -1451,7 +1075,7 @@ private void determineCumulativeIncomeFromInstallments(Loan loan, HashMap transactions, + private void determineCumulativeIncomeDetails(Loan loan, List transactions, HashMap incomeDetailsMap) { BigDecimal interest = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO; @@ -1474,4 +1098,16 @@ private LocalDate getFinalAccrualTransactionDate(Loan loan) { }; } + public boolean isProgressiveAccrual(@NotNull Loan loan) { + return loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE; + } + + private boolean isChargeOnDueDate() { + final String chargeAccrualDateType = configurationDomainService.getAccrualDateConfigForCharge(); + return !ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE.equalsIgnoreCase(chargeAccrualDateType); + } + + private void setSetHelpers(Loan loan) { + loan.setHelpers(null, null, transactionProcessorFactory); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 34cf975ba93..214bb16d3d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -104,7 +104,7 @@ import org.apache.fineract.useradministration.domain.AppUser; @RequiredArgsConstructor -public class LoanAssembler { +public class LoanAssemblerImpl implements LoanAssembler { private final FromJsonHelper fromApiJsonHelper; private final LoanRepositoryWrapper loanRepository; @@ -136,6 +136,7 @@ public class LoanAssembler { private final LoanCollateralManagementMapper loanCollateralManagementMapper; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + @Override public Loan assembleFrom(final Long accountId) { final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, @@ -144,11 +145,13 @@ public Loan assembleFrom(final Long accountId) { return loanAccount; } + @Override public void setHelpers(final Loan loanAccount) { loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); } + @Override public Loan assembleFrom(final JsonCommand command) { final JsonElement element = command.parsedJson(); @@ -281,6 +284,7 @@ public Loan assembleFrom(final JsonCommand command) { // TODO: Review... it might be better somewhere else and rethink due to the account number generation logic is // intertwined with GLIM logic + @Override public void accountNumberGeneration(JsonCommand command, Loan loan) { if (loan.isAccountNumberRequiresAutoGeneration()) { JsonElement element = command.parsedJson(); @@ -371,6 +375,7 @@ private void topUpLoanConfiguration(JsonElement element, Loan loan) { } } + @Override public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { CodeValue codeValue = null; if (codeValueId != null) { @@ -379,6 +384,7 @@ public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { return codeValue; } + @Override public Fund findFundByIdIfProvided(final Long fundId) { Fund fund = null; if (fundId != null) { @@ -387,6 +393,7 @@ public Fund findFundByIdIfProvided(final Long fundId) { return fund; } + @Override public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) { Staff staff = null; if (loanOfficerId != null) { @@ -419,6 +426,7 @@ private void copyAdvancedPaymentRulesIfApplicable(String transactionProcessingSt } } + @Override public Map updateFrom(JsonCommand command, Loan loan) { final Map changes = new HashMap<>(); LoanProduct loanProduct; @@ -844,6 +852,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { return changes; } + @Override public Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser) { final Map actualChanges = new LinkedHashMap<>(); @@ -868,6 +877,7 @@ public Map updateLoanApplicationAttributesForWithdrawal(Loan loa return actualChanges; } + @Override public Map updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser) { final Map actualChanges = new LinkedHashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java index 2ebaaf1f824..efe69029951 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java @@ -24,9 +24,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -214,7 +212,7 @@ public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") fin } @Override - public Collection retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) { + public List retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) { final LoanInstallmentChargeMapper rm = new LoanInstallmentChargeMapper(); String sql = "select " + rm.schema() + "where lic.loan_charge_id= ? "; if (onlyPaymentPendingCharges) { @@ -263,244 +261,6 @@ public Collection retrieveOverdueInstallmentChargeFrequencyNumber(final return frequencyNumbers; } - @Override - public Collection retrieveLoanChargesForAccrual(final Long loanId) { - - final LoanChargeAccrualMapper rm = new LoanChargeAccrualMapper(); - - final String sql = "select " + rm.schema() + " where lc.loan_id=? AND lc.is_active = true group by lc.id " - + " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC"; - - Collection charges = this.jdbcTemplate.query(sql, rm, // NOSONAR - LoanTransactionType.ACCRUAL.getValue(), loanId, loanId); - charges = updateLoanChargesWithUnrecognizedIncome(loanId, charges); - - Collection removeCharges = new ArrayList<>(); - for (LoanChargeData loanChargeData : charges) { - if (loanChargeData.isInstallmentFee()) { - removeCharges.add(loanChargeData); - } - } - charges.removeAll(removeCharges); - for (LoanChargeData loanChargeData : removeCharges) { - if (loanChargeData.isInstallmentFee()) { - Collection installmentChargeDatas = retrieveInstallmentLoanChargesForAccrual( - loanChargeData.getId()); - LoanChargeData modifiedChargeData = new LoanChargeData(loanChargeData, installmentChargeDatas); - charges.add(modifiedChargeData); - } - } - - return charges; - } - - private static final class LoanChargeAccrualMapper implements RowMapper { - - private final String schemaSql; - - LoanChargeAccrualMapper() { - StringBuilder sb = new StringBuilder(50); - sb.append(" lc.id as id, lc.charge_id as chargeId, lc.external_id as externalId, "); - sb.append(" lc.amount as amountDue, "); - sb.append(" lc.amount_waived_derived as amountWaived, "); - sb.append(" lc.charge_time_enum as chargeTime, "); - sb.append(" sum(cp.amount) as amountAccrued, "); - sb.append(" lc.is_penalty as penalty, "); - sb.append(" lc.due_for_collection_as_of_date as dueAsOfDate, "); - sb.append(" lc.submitted_on_date as submittedOnDate "); - sb.append(" from m_loan_charge lc "); - sb.append(" left join ( "); - sb.append(" select lcp.loan_charge_id, lcp.amount "); - sb.append(" from m_loan_charge_paid_by lcp "); - sb.append( - " inner join m_loan_transaction lt on lt.id = lcp.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ? and lt.loan_id = ? "); - sb.append(" ) cp on cp.loan_charge_id= lc.id "); - - schemaSql = sb.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long chargeId = rs.getLong("chargeId"); - final BigDecimal amount = rs.getBigDecimal("amountDue"); - final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued"); - final BigDecimal amountWaived = rs.getBigDecimal("amountWaived"); - - final int chargeTime = rs.getInt("chargeTime"); - final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(chargeTime); - - final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate"); - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); - final boolean penalty = rs.getBoolean("penalty"); - - final String externalIdStr = rs.getString("externalId"); - final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - - return new LoanChargeData(id, chargeId, dueAsOfDate, submittedOnDate, chargeTimeType, amount, amountAccrued, amountWaived, - penalty, externalId); - } - } - - private Collection updateLoanChargesWithUnrecognizedIncome(final Long loanId, - Collection loanChargeDatas) { - - final LoanChargeUnRecognizedIncomeMapper rm = new LoanChargeUnRecognizedIncomeMapper(loanChargeDatas); - - final String sql = "select " + rm.schema() + " where lc.loan_id=? AND lc.is_active = true group by lc.id " - + " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC"; - - return this.jdbcTemplate.query(sql, rm, LoanTransactionType.WAIVE_CHARGES.getValue(), loanId, loanId); // NOSONAR - } - - private static final class LoanChargeUnRecognizedIncomeMapper implements RowMapper { - - private final String schemaSql; - private final Map chargeDataMap; - - LoanChargeUnRecognizedIncomeMapper(final Collection datas) { - this.chargeDataMap = new HashMap<>(); - for (LoanChargeData chargeData : datas) { - this.chargeDataMap.put(chargeData.getId(), chargeData); - } - - StringBuilder sb = new StringBuilder(50); - sb.append("lc.id as id, "); - sb.append(" sum(wt.unrecognized_income_portion) as amountUnrecognized "); - sb.append(" from m_loan_charge lc "); - sb.append("left join ("); - sb.append("select cpb.loan_charge_id, lt.unrecognized_income_portion"); - sb.append(" from m_loan_charge_paid_by cpb "); - sb.append( - "inner join m_loan_transaction lt on lt.id = cpb.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ? and lt.loan_id = ? "); - sb.append(") wt on wt.loan_charge_id= lc.id "); - - schemaSql = sb.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized"); - - LoanChargeData chargeData = this.chargeDataMap.get(id); - return new LoanChargeData(amountUnrecognized, chargeData); - } - } - - private Collection retrieveInstallmentLoanChargesForAccrual(Long loanChargeId) { - final LoanInstallmentChargeAccrualMapper rm = new LoanInstallmentChargeAccrualMapper(); - String sql = "select " + rm.schema() - + " where lic.loan_charge_id= ? group by lsi.installment, lsi.duedate, lic.amount_outstanding_derived, lic.amount, lic.is_paid_derived, lic.amount_waived_derived, lic.waived"; - Collection chargeDatas = this.jdbcTemplate.query(sql, rm, // NOSONAR - LoanTransactionType.ACCRUAL.getValue(), loanChargeId); - final Map installmentChargeDatas = new HashMap<>(); - for (LoanInstallmentChargeData installmentChargeData : chargeDatas) { - installmentChargeDatas.put(installmentChargeData.getInstallmentNumber(), installmentChargeData); - } - chargeDatas = updateInstallmentLoanChargesWithUnrecognizedIncome(loanChargeId, installmentChargeDatas); - for (LoanInstallmentChargeData installmentChargeData : chargeDatas) { - installmentChargeDatas.put(installmentChargeData.getInstallmentNumber(), installmentChargeData); - } - return installmentChargeDatas.values(); - - } - - private static final class LoanInstallmentChargeAccrualMapper implements RowMapper { - - private final String schemaSql; - - LoanInstallmentChargeAccrualMapper() { - StringBuilder sb = new StringBuilder(50); - sb.append(" lsi.installment as installmentNumber, lsi.duedate as dueAsOfDate, "); - sb.append("lic.amount_outstanding_derived as amountOutstanding,"); - sb.append("lic.amount as amount, "); - sb.append("lic.is_paid_derived as paid, "); - sb.append("lic.amount_waived_derived as amountWaived, "); - sb.append(" sum(cp.amount) as amountAccrued, "); - sb.append("lic.waived as waived "); - sb.append("from m_loan_installment_charge lic "); - sb.append("join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id "); - sb.append("left join ("); - sb.append("select lcp.loan_charge_id, lcp.amount as amount, lcp.installment_number "); - sb.append(" from m_loan_charge_paid_by lcp "); - sb.append( - "inner join m_loan_transaction lt on lt.id = lcp.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ?"); - sb.append(") cp on cp.loan_charge_id= lic.loan_charge_id and cp.installment_number = lsi.installment "); - schemaSql = sb.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Integer installmentNumber = rs.getInt("installmentNumber"); - final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate"); - final BigDecimal amountOutstanding = rs.getBigDecimal("amountOutstanding"); - final BigDecimal amount = rs.getBigDecimal("amount"); - final BigDecimal amountWaived = rs.getBigDecimal("amountWaived"); - final boolean paid = rs.getBoolean("paid"); - final boolean waived = rs.getBoolean("waived"); - final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued"); - - return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount) - .amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).amountAccrued(amountAccrued) - .build(); - } - } - - private Collection updateInstallmentLoanChargesWithUnrecognizedIncome(final Long loanChargeId, - final Map installmentChargeDatas) { - final LoanInstallmentChargeUnRecognizedIncomeMapper rm = new LoanInstallmentChargeUnRecognizedIncomeMapper(installmentChargeDatas); - String sql = "select " + rm.schema() + " where cpb.loan_charge_id = ? group by cpb.installment_number "; - return this.jdbcTemplate.query(sql, rm, LoanTransactionType.WAIVE_CHARGES.getValue(), loanChargeId); // NOSONAR - } - - private static final class LoanInstallmentChargeUnRecognizedIncomeMapper implements RowMapper { - - private final String schemaSql; - private final Map installmentChargeDatas; - - LoanInstallmentChargeUnRecognizedIncomeMapper(final Map installmentChargeDatas) { - this.installmentChargeDatas = installmentChargeDatas; - StringBuilder sb = new StringBuilder(50); - sb.append(" cpb.installment_number as installmentNumber, "); - sb.append(" sum(lt.unrecognized_income_portion) as amountUnrecognized "); - sb.append(" from m_loan_charge_paid_by cpb "); - sb.append( - "inner join m_loan_transaction lt on lt.id = cpb.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ?"); - schemaSql = sb.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Integer installmentNumber = rs.getInt("installmentNumber"); - final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized"); - LoanInstallmentChargeData installmentChargeData = this.installmentChargeDatas.get(installmentNumber); - return LoanInstallmentChargeData.builder().installmentNumber(installmentChargeData.getInstallmentNumber()) - .dueDate(installmentChargeData.getDueDate()).amount(installmentChargeData.getAmount()) - .amountOutstanding(installmentChargeData.getAmountOutstanding()).amountWaived(installmentChargeData.getAmountWaived()) - .paid(installmentChargeData.isPaid()).waived(installmentChargeData.isWaived()) - .amountAccrued(installmentChargeData.getAmountAccrued()).amountUnrecognized(amountUnrecognized).build(); - } - } - @Override public Collection retrieveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType, final Integer installmentNumber) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 75f86c50a25..a13bf928c23 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -842,9 +842,7 @@ private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge .determineProcessor(loan.transactionProcessingStrategy()); loan.addLoanTransaction(loanChargeAdjustmentTransaction); if (loan.isInterestBearing() && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), - loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(), loan.getRepaymentScheduleInstallments(), - loan.getActiveCharges()); + loan.reprocessTransactions(); } else { loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 8f39c262e02..e983488586e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -31,14 +31,12 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; -import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -98,16 +96,10 @@ LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelated DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disbursementId); - Collection retriveScheduleAccrualData(); - LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId); LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId); - Collection retrievePeriodicAccrualData(LocalDate tillDate); - - Collection retrievePeriodicAccrualData(LocalDate tillDate, Loan loan); - LoanTransactionData retrieveLoanChargeOffTemplate(Long loanId); Collection fetchLoansForInterestRecalculation(); @@ -116,10 +108,6 @@ LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelated LoanTransactionData retrieveLoanPrePaymentTemplate(LoanTransactionType repaymentTransactionType, Long loanId, LocalDate onDate); - Collection retrieveWaiverLoanTransactions(Long loanId); - - Collection fetchWaiverInterestRepaymentData(Long loanId); - boolean isGuaranteeRequired(Long loanId); LocalDate retrieveMinimumDateOfRepaymentTransaction(Long loanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 652fe67f03a..55adaa9ca3f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -29,18 +29,15 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -77,7 +74,6 @@ import org.apache.fineract.portfolio.client.data.ClientData; import org.apache.fineract.portfolio.client.domain.ClientEnumerations; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -96,7 +92,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; -import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; @@ -1737,258 +1732,6 @@ public DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disburs return this.jdbcTemplate.queryForObject(sql, rm, loanId, disbursementId); // NOSONAR } - @Override - public Collection retriveScheduleAccrualData() { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - return retrieveScheduleAccrualDataForChargeSubmittedDateProcessing(); - } - return retrieveScheduleAccrualDataForDefaultProcessing(); - } - - private Collection retrieveScheduleAccrualDataForDefaultProcessing() { - LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); - LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper(); - Map paramMap = new HashMap<>(3); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select ").append(mapper.schema()).append( - " where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ") - .append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))") - .append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))") - .append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))") - .append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false and loan.is_charged_off = false and ls.duedate <= :currentDate) "); - - if (organisationStartDate != null) { - sqlBuilder.append(" and ls.duedate > :organisationStartDate "); - } - sqlBuilder.append(" order by loan.id,ls.duedate "); - - paramMap.put("active", LoanStatus.ACTIVE.getValue()); - paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); - paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate); - paramMap.put("currentDate", DateUtils.getBusinessLocalDate()); - return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); - - } - - private Collection retrieveScheduleAccrualDataForChargeSubmittedDateProcessing() { - LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); - LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper(); - Map paramMap = new HashMap<>(3); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select ").append(mapper.schema()).append( - " where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ") - .append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))") - .append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))") - .append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))") - .append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false and loan.is_charged_off = false) "); - - if (organisationStartDate != null) { - sqlBuilder.append(" and ls.duedate > :organisationStartDate "); - } - sqlBuilder.append(" order by loan.id,ls.duedate "); - - paramMap.put("active", LoanStatus.ACTIVE.getValue()); - paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); - paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate); - return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); - } - - @Override - public Collection retrievePeriodicAccrualData(final LocalDate tillDate) { - return retrievePeriodicAccrualData(tillDate, null); - } - - @Override - public Collection retrievePeriodicAccrualData(final LocalDate tillDate, final Loan loan) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - return retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(tillDate, loan); - } - return retrievePeriodicAccrualDataForDefaultProcessing(tillDate, loan); - } - - private Collection retrievePeriodicAccrualDataForDefaultProcessing(final LocalDate tillDate, final Loan loan) { - LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper(); - LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select ").append(mapper.schema()).append( - " where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ") - .append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))") - .append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))") - .append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))") - .append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and (loan.closedon_date <= :tillDate or loan.closedon_date is null)") - .append(" and loan.is_npa=false and loan.is_charged_off = false and (ls.duedate <= :tillDate or (ls.duedate > :tillDate and ls.fromdate < :tillDate)") - .append(" or (ls.installment = 1 and ls.fromdate = :tillDate))) "); - Map paramMap = new HashMap<>(5); - if (organisationStartDate != null) { - sqlBuilder.append(" and ls.duedate > :organisationStartDate "); - paramMap.put("organisationStartDate", organisationStartDate); - } - if (loan != null) { - sqlBuilder.append(" and loan.id= :loanId "); - paramMap.put("loanId", loan.getId()); - } - sqlBuilder.append(" order by loan.id,ls.duedate "); - paramMap.put("active", LoanStatus.ACTIVE.getValue()); - paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); - paramMap.put("tillDate", tillDate); - return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); - } - - private Collection retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(final LocalDate tillDate, - final Loan loan) { - LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper(); - LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select ").append(mapper.schema()).append( - " where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ") - .append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))") - .append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))") - .append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))") - .append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and (loan.closedon_date <= :tillDate or loan.closedon_date is null)") - .append(" and loan.is_npa=false and loan.is_charged_off = false)"); - Map paramMap = new HashMap<>(5); - if (organisationStartDate != null) { - sqlBuilder.append(" and ls.duedate > :organisationStartDate "); - paramMap.put("organisationStartDate", organisationStartDate); - } - if (loan != null) { - sqlBuilder.append(" and loan.id= :loanId "); - paramMap.put("loanId", loan.getId()); - } - sqlBuilder.append(" order by loan.id,ls.duedate "); - paramMap.put("active", LoanStatus.ACTIVE.getValue()); - paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue()); - paramMap.put("tillDate", tillDate); - return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); - } - - private static final class LoanSchedulePeriodicAccrualMapper implements RowMapper { - - public String schema() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("loan.id as loanId , (CASE WHEN loan.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId,") - .append("loan.accrued_till as accruedTill, loan.repayment_period_frequency_enum as frequencyEnum, ") - .append("loan.interest_calculated_from_date as interestCalculatedFrom, ").append("loan.repay_every as repayEvery,") - .append("ls.installment as installmentNumber, ") - .append("ls.duedate as duedate,ls.fromdate as fromdate ,ls.id as scheduleId,loan.product_id as productId,") - .append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,") - .append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ") - .append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ") - .append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,") - .append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,") - .append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode") - .append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ") - .append(" left join m_product_loan mpl on mpl.id = loan.product_id") - .append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id") - .append(" left join m_currency curr on curr.code = loan.currency_code") - .append(" left join m_loan_recalculation_details as recaldet on loan.id = recaldet.loan_id "); - return sqlBuilder.toString(); - } - - @Override - public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { - - final Long loanId = rs.getLong("loanId"); - final Long officeId = rs.getLong("officeId"); - final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); - final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom"); - final Integer installmentNumber = JdbcSupport.getInteger(rs, "installmentNumber"); - - final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum"); - final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery"); - final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum); - final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate"); - final LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromdate"); - final Long repaymentScheduleId = rs.getLong("scheduleId"); - final Long loanProductId = rs.getLong("productId"); - final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest"); - final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges"); - final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty"); - final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived"); - final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); - final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); - final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty"); - final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee"); - final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty"); - - 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 currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, - currencyDisplaySymbol, currencyNameCode); - - return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromDate, - repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, - accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty); - } - - } - - private static final class LoanScheduleAccrualMapper implements RowMapper { - - public String schema() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("loan.id as loanId, (CASE WHEN loan.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId,") - .append("ls.duedate as duedate,ls.fromdate as fromdate,ls.id as scheduleId,loan.product_id as productId,") - .append("ls.installment as installmentNumber, ") - .append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,") - .append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ") - .append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ") - .append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,") - .append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,") - .append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode") - .append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ") - .append(" left join m_product_loan mpl on mpl.id = loan.product_id") - .append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id") - .append(" left join m_currency curr on curr.code = loan.currency_code") - .append(" left join m_loan_recalculation_details as recaldet on loan.id = recaldet.loan_id "); - return sqlBuilder.toString(); - } - - @Override - public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { - - final Long loanId = rs.getLong("loanId"); - final Long officeId = rs.getLong("officeId"); - final Integer installmentNumber = JdbcSupport.getInteger(rs, "installmentNumber"); - final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate"); - final LocalDate fromdate = JdbcSupport.getLocalDate(rs, "fromdate"); - final Long repaymentScheduleId = rs.getLong("scheduleId"); - final Long loanProductId = rs.getLong("productId"); - final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest"); - final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges"); - final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty"); - final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee"); - final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty"); - - final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived"); - final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest"); - final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege"); - final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty"); - - 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 currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, - currencyDisplaySymbol, currencyNameCode); - final LocalDate accruedTill = null; - final PeriodFrequencyType frequency = null; - final Integer repayEvery = null; - final LocalDate interestCalculatedFrom = null; - return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate, - repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome, - accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty); - } - } - @Override public LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId) { final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); @@ -2129,20 +1872,6 @@ public List fetchLoansForInterestRecalculation(Integer pageSize, Long maxL } } - @Override - public Collection retrieveWaiverLoanTransactions(final Long loanId) { - try { - - final LoanTransactionDerivedComponentMapper rm = new LoanTransactionDerivedComponentMapper(sqlGenerator); - - final String sql = "select " + rm.schema() - + " where tr.loan_id = ? and tr.transaction_type_enum = ? and tr.is_reversed=false order by tr.transaction_date, tr.created_on_utc, tr.id "; - return this.jdbcTemplate.query(sql, rm, loanId, LoanTransactionType.WAIVE_INTEREST.getValue()); // NOSONAR - } catch (final EmptyResultDataAccessException e) { - return null; - } - } - @Override public boolean isGuaranteeRequired(final Long loanId) { final String sql = "select pl.hold_guarantee_funds from m_loan ml inner join m_product_loan pl on pl.id = ml.product_id where ml.id=?"; @@ -2195,89 +1924,6 @@ public LoanTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused" } } - @Override - public Collection fetchWaiverInterestRepaymentData(final Long loanId) { - try { - - final LoanRepaymentWaiverMapper rm = new LoanRepaymentWaiverMapper(); - - final String sql = "select " + rm.getSchema() - + " where lrs.loan_id = ? and lrs.interest_waived_derived is not null order by lrs.installment ASC "; - return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR - } catch (final EmptyResultDataAccessException e) { - return null; - } - - } - - private static final class LoanRepaymentWaiverMapper implements RowMapper { - - private final String sqlSchema; - - public String getSchema() { - return this.sqlSchema; - } - - LoanRepaymentWaiverMapper() { - StringBuilder sb = new StringBuilder(); - sb.append("lrs.duedate as dueDate,lrs.interest_waived_derived interestWaived, lrs.installment as installment"); - sb.append(" from m_loan_repayment_schedule lrs "); - sqlSchema = sb.toString(); - } - - @Override - public LoanSchedulePeriodData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { - - final Integer period = JdbcSupport.getInteger(rs, "installment"); - final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); - final BigDecimal interestWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWaived"); - - final LocalDate fromDate = null; - final LocalDate obligationsMetOnDate = null; - final Boolean complete = false; - final BigDecimal principalOriginalDue = null; - final BigDecimal principalPaid = null; - final BigDecimal principalWrittenOff = null; - final BigDecimal principalOutstanding = null; - final BigDecimal interestPaid = null; - final BigDecimal interestWrittenOff = null; - final BigDecimal interestOutstanding = null; - final BigDecimal feeChargesDue = null; - final BigDecimal feeChargesPaid = null; - final BigDecimal feeChargesWaived = null; - final BigDecimal feeChargesWrittenOff = null; - final BigDecimal feeChargesOutstanding = null; - final BigDecimal penaltyChargesDue = null; - final BigDecimal penaltyChargesPaid = null; - final BigDecimal penaltyChargesWaived = null; - final BigDecimal penaltyChargesWrittenOff = null; - final BigDecimal penaltyChargesOutstanding = null; - - final BigDecimal totalDueForPeriod = null; - final BigDecimal totalPaidInAdvanceForPeriod = null; - final BigDecimal totalPaidLateForPeriod = null; - final BigDecimal totalActualCostOfLoanForPeriod = null; - final BigDecimal outstandingPrincipalBalanceOfLoan = null; - final BigDecimal interestDueOnPrincipalOutstanding = null; - Long loanId = null; - final BigDecimal totalWaived = null; - final BigDecimal totalWrittenOff = null; - final BigDecimal totalOutstanding = null; - final BigDecimal totalPaid = null; - final BigDecimal totalInstallmentAmount = null; - final BigDecimal totalCredits = null; - final BigDecimal totalAccruedInterest = null; - - return LoanSchedulePeriodData.periodWithPayments(loanId, period, fromDate, dueDate, obligationsMetOnDate, complete, - principalOriginalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan, - interestDueOnPrincipalOutstanding, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesDue, - feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid, - penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid, - totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding, - totalActualCostOfLoanForPeriod, totalInstallmentAmount, totalCredits, false, totalAccruedInterest); - } - } - @Override public LocalDate retrieveMinimumDateOfRepaymentTransaction(Long loanId) { return this.jdbcTemplate.queryForObject( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java index ba6284515df..3ec62c3296d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java @@ -49,6 +49,7 @@ public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { log.debug("Loan Status change for loan {}", loan.getId()); if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId()); + loan.updateLoanSummaryDerivedFields(); loanAccrualsProcessingService.processAccrualsForLoanClosure(loan); } if (loan.isOpen()) { 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 fe2b4b60473..29cd6a0caca 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 @@ -597,15 +597,7 @@ private ChangedTransactionDetail disburseLoan(JsonCommand command, boolean isPay if (loan.getLoanProduct().isMultiDisburseLoan()) { final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); if (!allNonContraTransactionsPostDisbursement.isEmpty()) { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory - .determineProcessor(loan.getTransactionProcessingStrategyCode()); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), - allNonContraTransactionsPostDisbursement, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), - loan.getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(loan); - } - loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values()); + loan.reprocessTransactions(); } loan.updateLoanSummaryDerivedFields(); } @@ -1023,7 +1015,7 @@ private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate tra // FIXME - kw - update account balance to negative amount. handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine); statusChanged = true; - } else if (loan.getSummary().isRepaidInFull(loan.loanCurrency())) { + } else if (loan.getSummary().isRepaidInFull(loan.getCurrency())) { handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine); statusChanged = true; } else { @@ -1077,7 +1069,7 @@ private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loa final boolean isTransactionChronologicallyLatest = loan .isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction); - if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) { + if (newInterestPaymentWaiverTransaction.isNotZero()) { loan.addLoanTransaction(newInterestPaymentWaiverTransaction); } @@ -1133,19 +1125,7 @@ private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan, loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); } - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( - loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(loan); - } - /*** - * Commented since throwing exception if external id present for one of the transactions. for this need to save - * the reversed transactions first and then new transactions. - */ - loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values()); - return changedTransactionDetail; + return loan.reprocessTransactions(); } @Transactional @@ -1498,7 +1478,7 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); } - boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); + boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(); if (thereIsNewTransaction) { if (paymentDetail != null) { this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java index 8dc0ab31bb4..ae8d835dec6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java @@ -29,6 +29,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; +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.ChangedTransactionDetail; @@ -59,7 +60,7 @@ private static void simulateRepaymentForDisbursements(LoanTransaction lt, final List collect) { collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(), lt.getAmount(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null)); - if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) { + if (lt.getTypeOf().isDisbursement() && MathUtil.isGreaterThanZero(refundFinal.get())) { if (lt.getAmount().compareTo(refundFinal.get()) <= 0) { collect.add( new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), REPAYMENT.getValue(), lt.getDateOf(), lt.getAmount(), @@ -96,7 +97,7 @@ public boolean canHandle(Loan loan) { } private boolean isTransactionNeededForInterestRefundCalculations(LoanTransaction lt) { - return lt.isNotReversed() && !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund(); + return lt.isNotReversed() && !lt.isAccrualRelated() && !lt.isInterestRefund(); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java index 5fccb83f95e..5f2728e7735 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java @@ -24,7 +24,6 @@ import java.time.LocalDate; import java.util.Comparator; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -122,8 +121,7 @@ public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) { } reverseReAgeTransaction(reAgeTransaction, command); loanTransactionRepository.saveAndFlush(reAgeTransaction); - - reProcessLoanTransactions(reAgeTransaction.getLoan()); + loan.reprocessTransactions(); loan.updateLoanScheduleDependentDerivedFields(); persistNote(loan, command, changes); @@ -179,15 +177,6 @@ private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType, periodFrequencyNumber, startDate, numberOfInstallments); } - private void reProcessLoanTransactions(Loan loan) { - final List filteredTransactions = loan.retrieveListOfTransactionsForReprocessing(); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.transactionProcessingStrategy()); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions, - loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - } - private void persistNote(Loan loan, JsonCommand command, Map changes) { if (command.hasParameter("note")) { final String note = command.stringValueOfParameterNamed("note"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java index 1546f6c93f5..10a73d3dd7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java @@ -24,7 +24,6 @@ import java.time.LocalDate; import java.util.Comparator; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -129,16 +128,7 @@ private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, LoanReAmortizationApiConstants.externalIdParameterName); reAmortizeTransaction.reverse(reversalExternalId); reAmortizeTransaction.manuallyAdjustedOrReversed(); - reProcessLoanTransactions(reAmortizeTransaction.getLoan()); - } - - private void reProcessLoanTransactions(Loan loan) { - final List filteredTransactions = loan.retrieveListOfTransactionsForReprocessing(); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.transactionProcessingStrategy()); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions, - loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + reAmortizeTransaction.getLoan().reprocessTransactions(); } private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) { 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 269452c3e2d..0f7ce982fa5 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 @@ -20,6 +20,7 @@ import java.util.List; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; 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; @@ -107,8 +108,8 @@ public LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTra @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, + LoanRepositoryWrapper loanRepositoryWrapper, @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService) { - return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService); + return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService); } - } 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 d045c091731..bd5e5d17d18 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 @@ -102,6 +102,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService; import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService; @@ -217,7 +218,7 @@ public LoanAssembler loanAssembler(FromJsonHelper fromApiJsonHelper, LoanReposit LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) { - return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, + return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml index 9fffdcaecf6..46aa479d066 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml @@ -28,4 +28,10 @@ + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 47e57821fd9..d1e501c5893 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -105,7 +105,7 @@ public void givenAllConfigurationWhenValidatedThenValidationSuccessful() throws "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent", "LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent", "LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent", - "LoanTransactionInterestRefundPreBusinessEvent"); + "LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); @@ -191,7 +191,7 @@ public void givenMissingEventConfigurationWhenValidatedThenThrowException() thro "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent", "LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent", "LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent", - "LoanTransactionInterestRefundPreBusinessEvent"); + "LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java index a62e1f2dcca..6b3d6054c99 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java @@ -177,13 +177,20 @@ public void testGetLastUserTransaction() { final LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class); when(loanTransaction.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction.isAccrualTransaction()).thenReturn(Boolean.FALSE); + when(loanTransaction.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction.isAccrualAdjustment()).thenReturn(Boolean.FALSE); final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class); when(loanTransaction2.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction2.isAccrualTransaction()).thenReturn(Boolean.FALSE); + when(loanTransaction2.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction2.isAccrualAdjustment()).thenReturn(Boolean.FALSE); final LoanTransaction loanTransaction3 = Mockito.mock(LoanTransaction.class); when(loanTransaction3.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction3.isAccrualTransaction()).thenReturn(Boolean.TRUE); + when(loanTransaction3.isAccrual()).thenReturn(Boolean.TRUE); + when(loanTransaction3.isAccrualAdjustment()).thenReturn(Boolean.FALSE); + final LoanTransaction loanTransaction4 = Mockito.mock(LoanTransaction.class); + when(loanTransaction4.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction4.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction4.isAccrualAdjustment()).thenReturn(Boolean.TRUE); ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3)); final LoanTransaction userTransaction = loan.getLastUserTransaction(); assertNotNull(userTransaction); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index a7490ce99f8..679d0aca8bc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -5145,7 +5145,8 @@ public void testLoanScheduleWithInterestRecalculationMakeAdvancePaymentTillSettl for (int i = 1; i < loanSchedule.size(); i++) { retrieveDueDate = dateFormat.format(repaymentDate.getTime()); - amount = (Float) loanSchedule.get(i).get("principalOriginalDue") + (Float) loanSchedule.get(i).get("interestOriginalDue"); + amount = ((Number) loanSchedule.get(i).get("principalOriginalDue")).floatValue() + + ((Number) loanSchedule.get(i).get("interestOriginalDue")).floatValue(); if (currentDate.after(repaymentDate)) { LOAN_TRANSACTION_HELPER.makeRepayment(retrieveDueDate, amount, loanID); } else { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java index 9047fb7d50e..57cc4d6dcfd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java @@ -310,7 +310,7 @@ public void verifyUC02b() { logLoanTransactions(loanId); verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), transaction(87.89, "Repayment", "01 February 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"), - transaction(10.49, "Interest Refund", "09 February 2021")); + transaction(10.49, "Interest Refund", "09 February 2021"), transaction(10.49, "Accrual", "09 February 2021")); }); } @@ -430,7 +430,8 @@ public void verifyUC05() { logLoanTransactions(loanId); verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), transaction(500.0, "Disbursement", "07 January 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"), - transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021")); + transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021"), + transaction(9.67, "Accrual", "09 February 2021")); }); } @@ -627,7 +628,8 @@ public void verifyUC10() { transaction(171.29, "Repayment", "01 April 2021"), // transaction(171.29, "Repayment", "01 May 2021"), // transaction(171.29, "Repayment", "01 June 2021"), // - transaction(171.32, "Repayment", "01 July 2021") // + transaction(171.32, "Repayment", "01 July 2021"), // + transaction(27.77, "Accrual", "01 July 2021") // ); // }); runAt("11 July 2021", () -> { @@ -647,7 +649,9 @@ public void verifyUC10() { transaction(171.29, "Repayment", "01 June 2021"), // transaction(171.32, "Repayment", "01 July 2021"), // transaction(500.0, "Payout Refund", "11 July 2021"), // - transaction(20.41, "Interest Refund", "11 July 2021")); // + transaction(20.41, "Interest Refund", "11 July 2021"), // + transaction(27.77, "Accrual", "01 July 2021") // + ); // }); } @@ -685,7 +689,8 @@ public void verifyUC11() { transaction(500.0, "Merchant Issued Refund", "14 January 2021"), // transaction(1.78, "Interest Refund", "14 January 2021"), // transaction(500.0, "Payout Refund", "22 January 2021"), // - transaction(2.88, "Interest Refund", "22 January 2021") // + transaction(2.88, "Interest Refund", "22 January 2021"), // + transaction(4.66, "Accrual", "22 January 2021") // ); }); } @@ -985,7 +990,8 @@ public void verifyUC16() { transaction(250.0, "Payout Refund", "13 February 2021"), // transaction(2.96, "Interest Refund", "13 February 2021"), // transaction(400.0, "Merchant Issued Refund", "06 April 2021"), // - transaction(10.11, "Interest Refund", "06 April 2021") // + transaction(10.11, "Interest Refund", "06 April 2021"), // + transaction(17.14, "Accrual", "06 April 2021") // ); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); @@ -1102,8 +1108,11 @@ public void verifyUC18S1() { verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // transaction(85.63, "Repayment", "10 January 2021"), // + transaction(5.48, "Accrual Adjustment", "10 January 2021"), // + transaction(5.2, "Accrual", "10 January 2021"), // transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), // - transaction(5.42, "Interest Refund", "22 January 2021") // + transaction(5.42, "Interest Refund", "22 January 2021"), // + transaction(5.7, "Accrual", "22 January 2021") // ); }); } @@ -1147,7 +1156,8 @@ public void verifyUC18S2() { verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // transaction(85.63, "Repayment", "10 January 2021"), // transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), // - transaction(5.42, "Interest Refund", "22 January 2021") // + transaction(5.42, "Interest Refund", "22 January 2021"), // + transaction(5.42, "Accrual", "22 January 2021") // ); Long repaymentId = repaymentIdRef.get(); @@ -1156,9 +1166,11 @@ public void verifyUC18S2() { verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // reversedTransaction(85.63, "Repayment", "10 January 2021"), // + transaction(5.48, "Accrual", "10 January 2021"), // + transaction(5.2, "Accrual Adjustment", "10 January 2021"), // transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), // transaction(5.70, "Interest Refund", "22 January 2021"), // - transaction(5.70, "Accrual", "10 January 2021") // + transaction(5.42, "Accrual", "22 January 2021") // ); }); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index 9c35d61e4bd..908f1b70f65 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -465,6 +465,11 @@ public static ArrayList> getDefaultExternalEventConfiguratio loanAccrualTransactionCreatedBusinessEvent.put("enabled", false); defaults.add(loanAccrualTransactionCreatedBusinessEvent); + Map loanAccrualAdjustmentTransactionBusinessEvent = new HashMap<>(); + loanAccrualAdjustmentTransactionBusinessEvent.put("type", "LoanAccrualAdjustmentTransactionBusinessEvent"); + loanAccrualAdjustmentTransactionBusinessEvent.put("enabled", false); + defaults.add(loanAccrualAdjustmentTransactionBusinessEvent); + Map loanRescheduledDueAdjustScheduleBusinessEvent = new HashMap<>(); loanRescheduledDueAdjustScheduleBusinessEvent.put("type", "LoanRescheduledDueAdjustScheduleBusinessEvent"); loanRescheduledDueAdjustScheduleBusinessEvent.put("enabled", false); @@ -571,7 +576,6 @@ public static ArrayList> getDefaultExternalEventConfiguratio defaults.add(loanTransactionInterestRefundPreBusinessEvent); return defaults; - } public static String getExternalEventConfigurationsForUpdateJSON() {