From a01353d20bad346f7f9c6be5dceadb765ac3ab14 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 | 21 +- .../test/stepdef/loan/LoanStepDef.java | 1 + .../org/apache/fineract/test/TestRunner.java | 2 +- .../resources/features/EMICalculation.feature | 277 +-- .../src/test/resources/features/Loan.feature | 2 + .../features/LoanAccrualActivity.feature | 48 +- .../features/LoanAccrualTransaction.feature | 11 +- .../resources/features/LoanChargeOff.feature | 4 - .../resources/features/LoanChargeback.feature | 28 +- .../resources/features/LoanRepayment.feature | 55 +- ...erLoanStatusChangePlatformServiceImpl.java | 2 +- .../loan/LoanStatusChangedBusinessEvent.java | 9 +- ...ualAdjustmentTransactionBusinessEvent.java | 35 + .../loanaccount/data/AccrualChargeData.java | 37 + .../loanaccount/data/AccrualPeriodData.java | 87 + .../loanaccount/data/AccrualPeriodsData.java | 64 + .../loanaccount/data/LoanChargeData.java | 18 +- .../data/LoanTransactionEnumData.java | 2 + .../DefaultLoanLifecycleStateMachine.java | 2 +- .../portfolio/loanaccount/domain/Loan.java | 261 ++- .../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 | 39 + .../domain/LoanScheduleGenerator.java | 4 + .../LoanAccrualsProcessingService.java | 25 +- .../loanaccount/service/LoanAssembler.java | 50 + .../LoanChargeReadPlatformService.java | 5 +- .../loanproduct/service/LoanEnumerations.java | 2 + ...edPaymentScheduleTransactionProcessor.java | 387 +++-- .../loanschedule/data/InterestPeriod.java | 18 +- .../ProgressiveLoanInterestScheduleModel.java | 22 +- .../loanschedule/data/RepaymentPeriod.java | 58 +- .../ProgressiveLoanScheduleGenerator.java | 105 +- .../loanproduct/calc/EMICalculator.java | 20 +- .../calc/ProgressiveEMICalculator.java | 124 +- ...ymentScheduleTransactionProcessorTest.java | 8 +- .../calc/ProgressiveEMICalculatorTest.java | 109 +- ...ccrualBasedAccountingProcessorForLoan.java | 56 +- ...nInterestRecalculationCOBBusinessStep.java | 2 +- .../ChargeReadPlatformServiceImpl.java | 2 +- .../api/LoanChargesApiResource.java | 5 +- .../domain/LoanAccountDomainServiceJpa.java | 12 +- .../AddAccrualEntriesTasklet.java | 40 +- ...heduleRequestWritePlatformServiceImpl.java | 2 +- .../LoanApplicationValidator.java | 6 +- .../LoanAccrualsProcessingServiceImpl.java | 1518 +++++++---------- ...nAssembler.java => LoanAssemblerImpl.java} | 12 +- .../LoanChargeReadPlatformServiceImpl.java | 242 +-- .../LoanChargeWritePlatformServiceImpl.java | 4 +- .../service/LoanReadPlatformService.java | 12 - .../service/LoanReadPlatformServiceImpl.java | 400 ----- .../LoanStatusChangePlatformServiceImpl.java | 21 +- ...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 +- 75 files changed, 2175 insertions(+), 2652 deletions(-) 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..99365b6c235 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 @@ -232,7 +232,7 @@ public static BigDecimal abs(BigDecimal value) { * if true then null parameter is omitted, otherwise returns null */ public static BigDecimal min(BigDecimal first, BigDecimal second, boolean notNull) { - return notNull ? first == null ? second : second == null ? first : min(first, second, false) + return notNull ? (first == null ? second : (second == null ? first : min(first, second, false))) : isLessThan(first, second) ? first : second; } @@ -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/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 64fc54e3b72..ae533b18548 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -1613,6 +1613,7 @@ public void loanLastPaymentAmount(double lastPaymentAmountExpected) throws IOExc @Then("Loan Repayment schedule has {int} periods, with the following data for periods:") public void loanRepaymentSchedulePeriodsCheck(int linesExpected, DataTable table) throws IOException { + Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); long loanId = loanCreateResponse.body().getLoanId(); diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/TestRunner.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/TestRunner.java index c07c0349da2..d8dbc23422b 100644 --- a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/TestRunner.java +++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/TestRunner.java @@ -25,5 +25,5 @@ @RunWith(Cucumber.class) @CucumberOptions(features = "src/test/resources/features", glue = { "org.apache.fineract.test.stepdef", "org.apache.fineract.test.stepdef.common", "org.apache.fineract.test.stepdef.hook", "org.apache.fineract.test.stepdef.loan", - "org.apache.fineract.test.stepdef.saving", "org.apache.fineract.test.config" }) + "org.apache.fineract.test.stepdef.saving", "org.apache.fineract.test.config" }, tags = "not @Skip") public class TestRunner {} 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 632bd2004e6..b9e5644ffa0 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 @TestRailId:C3215 @@ -2074,6 +2078,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 | # --- 1st and 2nd installment overdue --- + When Admin sets the business date to "01 Feb 2024" + When Admin runs inline COB job for Loan When Admin sets the business date to "10 March 2024" When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: @@ -2121,6 +2127,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 | # --- 1st and 2nd installment overdue --- + When Admin sets the business date to "01 Feb 2024" + When Admin runs inline COB job for Loan When Admin sets the business date to "10 March 2024" When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: @@ -2168,6 +2176,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 | # --- 1st and 2nd installment overdue --- + When Admin sets the business date to "01 Feb 2024" + When Admin runs inline COB job for Loan When Admin sets the business date to "10 March 2024" When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: @@ -2215,6 +2225,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 | # --- 1st and 2nd installment overdue --- + When Admin sets the business date to "01 Feb 2024" + When Admin runs inline COB job for Loan When Admin sets the business date to "10 March 2024" When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: @@ -2583,8 +2595,6 @@ 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 | @@ -2670,8 +2680,6 @@ 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 | @@ -2757,8 +2765,6 @@ 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 | @@ -2844,8 +2850,6 @@ 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 | @@ -2992,6 +2996,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 @@ -3037,6 +3042,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 @@ -3082,8 +3088,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 @@ -3129,12 +3134,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 @@ -3165,10 +3168,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: @@ -3177,13 +3180,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 @@ -3208,31 +3209,45 @@ 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 | # --- lesser than emi amount is paid --- + When Admin sets the business date to "01 February 2024" + When Admin runs inline COB job for Loan When Admin sets the business date to "15 February 2024" When Admin runs inline COB job for Loan And Customer makes "AUTOPAY" repayment on "15 February 2024" with 34.02 EUR transaction amount 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.59 | 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 | + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.00 | 1.64 | 0.0 | 0.0 | 101.64 | 34.02 | 17.01 | 17.01 | 67.62 | 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 | + | 31 January 2024 | Accrual | 0.56 | 0.0 | 0.56 | 0.0 | 0.0 | 0.0 | false | false | + | 01 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 06 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 14 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 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 | 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3292,8 +3307,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3353,8 +3367,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3400,22 +3413,21 @@ 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3460,21 +3472,22 @@ 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3524,22 +3537,27 @@ 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3584,21 +3602,22 @@ 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3643,10 +3662,22 @@ 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 | -# 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 When Admin sets the business date to "01 January 2024" When Admin creates a client with random data @@ -3683,8 +3714,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 | @@ -3695,18 +3726,29 @@ 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 | -# 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 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 @@ -3734,22 +3776,60 @@ 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 | + | 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 | @TestRailId:C3269 @@ -3824,6 +3904,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: @@ -3842,8 +3923,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 @@ -3960,6 +4041,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: @@ -3980,8 +4062,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 @@ -4047,6 +4129,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" @@ -4115,6 +4198,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 @@ -4187,6 +4271,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 @@ -4245,6 +4330,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 @@ -4302,6 +4388,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" @@ -4325,10 +4412,10 @@ 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 @@ -4364,7 +4451,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 e07a9f22450..7aa33418df9 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -5989,6 +5989,7 @@ Feature: Loan | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | | 15 February 2024 | Repayment | 83.81 | 83.57 | 0.24 | 0.0 | 0.0 | 0.0 | false | + | 15 February 2024 | Accrual | 0.82 | 0.0 | 0.82 | 0.0 | 0.0 | 0.0 | false | Then Loan's all installments have obligations met Scenario: Early pay-off loan with interest, TILL_REST_FREQUENCY_DATE product @@ -6049,6 +6050,7 @@ Feature: Loan | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | | 15 February 2024 | Repayment | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | + | 15 February 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | Then Loan's all installments have obligations met When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_REST_FREQUENCY_DATE" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature index 20b8eb89cdc..53d351225b2 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature @@ -1329,12 +1329,12 @@ Feature: LoanAccrualActivity | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 1000.0 | 4.1 | 0.0 | 0.0 | 1004.1 | 0.0 | 0.0 | 0.0 | 1004.1 | 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 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | - | 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false | - | 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false | + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | + | 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false | + | 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false | # --- Accrual activity --- When Admin sets the business date to "07 January 2024" When Admin runs inline COB job for Loan @@ -1349,15 +1349,15 @@ Feature: LoanAccrualActivity | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 1000.0 | 4.1 | 0.0 | 0.0 | 1004.1 | 0.0 | 0.0 | 0.0 | 1004.1 | 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 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | - | 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false | - | 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false | - | 05 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 06 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | - | 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false | + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | + | 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false | + | 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3188 Scenario: Verify accrual activity - UC16: Preclose, loan account partially paid before first installment due date, fully paid after first installment date, reopen by undo 1st repayment @@ -1775,15 +1775,15 @@ Feature: LoanAccrualActivity When Admin sets the business date to "07 January 2024" When Admin runs inline COB job for Loan Then Loan Repayment schedule has 4 periods, with the following data for periods: - | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 5 | 06 January 2024 | | 750.64 | 249.36 | 1.64 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | - | 2 | 5 | 11 January 2024 | | 500.96 | 249.68 | 1.32 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | - | 3 | 5 | 16 January 2024 | | 250.78 | 250.18 | 0.82 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | - | 4 | 5 | 21 January 2024 | | 0.0 | 250.78 | 0.41 | 0.0 | 0.0 | 251.19| 0.0 | 0.0 | 0.0 | 251.19 | + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 5 | 06 January 2024 | | 750.64 | 249.36 | 1.64 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | + | 2 | 5 | 11 January 2024 | | 500.96 | 249.68 | 1.32 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | + | 3 | 5 | 16 January 2024 | | 250.78 | 250.18 | 0.82 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 | + | 4 | 5 | 21 January 2024 | | 0.0 | 250.78 | 0.41 | 0.0 | 0.0 | 251.19 | 0.0 | 0.0 | 0.0 | 251.19 | Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 1000.0 | 4.19 | 0.0 | 0.0 | 1004.19| 0.0 | 0.0 | 0.0 | 1004.19 | + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 4.19 | 0.0 | 0.0 | 1004.19 | 0.0 | 0.0 | 0.0 | 1004.19 | 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 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | @@ -2045,7 +2045,7 @@ Feature: LoanAccrualActivity Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 22 April 2024 | Disbursement | 400.0 | 0.0 | 0.0 | 0.0 | 0.0 | 400.0 | false | false | - | 22 April 2024 | Accrual | 30.0 | 0.0 | 0.0 | 0.0 | 30.0 | 0.0 | false | false | | 22 April 2024 | Repayment | 600.0 | 400.0 | 0.0 | 0.0 | 30.0 | 0.0 | true | true | | 22 April 2024 | Goodwill Credit | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 400.0 | false | true | + | 09 October 2024 | Accrual | 30.0 | 0.0 | 0.0 | 0.0 | 30.0 | 0.0 | false | false | | 10 October 2024 | Charge Adjustment | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 400.0 | false | true | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index 71a8415abcb..5aa5a76ac22 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -256,8 +256,8 @@ Feature: LoanAccrualTransaction | 01 July 2023 | Disbursement | 5000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 5000.0 | | 05 July 2023 | Repayment | 1000.0 | 990.0 | 0.0 | 10.0 | 0.0 | 4010.0 | | 06 July 2023 | Repayment | 4011.0 | 4010.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 06 July 2023 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | | 07 July 2023 | Repayment | 4011.0 | 4010.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 07 July 2023 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | @TestRailId:C2686 Scenario: Verify that the accrual transaction correctly created (overpay, undo repayment, add charge, overpay) @@ -303,9 +303,8 @@ Feature: LoanAccrualTransaction | 01 July 2023 | Disbursement | 5000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 5000.0 | | 05 July 2023 | Repayment | 1000.0 | 940.0 | 0.0 | 60.0 | 0.0 | 4060.0 | | 06 July 2023 | Repayment | 4011.0 | 4010.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 06 July 2023 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | | 07 July 2023 | Repayment | 4061.0 | 4060.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 07 July 2023 | Accrual | 50.0 | 0.0 | 0.0 | 50.0 | 0.0 | 0.0 | + | 07 July 2023 | Accrual | 60.0 | 0.0 | 0.0 | 60.0 | 0.0 | 0.0 | @TestRailId:C2707 Scenario: Verify that the accrual transaction is not reversed when multi disbursement happens @@ -881,7 +880,8 @@ Feature: LoanAccrualTransaction Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 17 April 2024 | Disbursement | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 750.0 | - | 17 April 2024 | Waive loan charges | 20.0 | 0.0 | 0.0 | 0.0 | 0.0 | 750.0 | + | 17 April 2024 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 17 April 2024 | Waive loan charges | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 750.0 | | 19 April 2024 | Accrual | 55.0 | 0.0 | 0.0 | 0.0 | 55.0 | 0.0 | | 20 April 2024 | Repayment | 55.0 | 55.0 | 0.0 | 0.0 | 0.0 | 695.0 | | 20 April 2024 | Repayment | 810.0 | 695.0 | 0.0 | 0.0 | 115.0 | 0.0 | @@ -921,8 +921,8 @@ Feature: LoanAccrualTransaction Then Loan Transactions tab has the following data: | 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 | | 24 April 2024 | Accrual | 0.66 | 0.0 | 0.66 | 0.0 | 0.0 | 0.0 | - | 25 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 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 | | 500.0 | 5.77 | 0.0 | 0.0 | 505.77 | 0.0 | 0.0 | 0.0 | 505.77 | @@ -940,7 +940,6 @@ Feature: LoanAccrualTransaction | 05 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 500.0 | | 25 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 | | 26 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 26 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 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 | | 1000.0 | 8.06 | 0.0 | 0.0 | 1008.06 | 0.0 | 0.0 | 0.0 | 1008.06 | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature index 09581c36024..0d9b3e74f75 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature @@ -926,7 +926,6 @@ Feature: Charge-off Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 June 2023 | Charge-off | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 0.0 | When Admin sets the business date to "10 June 2023" @@ -942,7 +941,6 @@ Feature: Charge-off Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 June 2023 | Charge-off | 1035.0 | 1000.0 | 0.0 | 20.0 | 15.0 | 0.0 | Then On Loan Transactions tab the "Repayment" Transaction with date "10 February 2023" is reverted @@ -973,7 +971,6 @@ Feature: Charge-off Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 June 2023 | Charge-off | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 0.0 | When Admin sets the business date to "10 June 2023" @@ -989,7 +986,6 @@ Feature: Charge-off Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 June 2023 | Charge-off | 1035.0 | 1000.0 | 0.0 | 20.0 | 15.0 | 0.0 | Then On Loan Transactions tab the "Repayment" Transaction with date "10 February 2023" is reverted diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature index 686ef8d90eb..6495aa2813a 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature @@ -2514,10 +2514,6 @@ Feature: LoanChargeback | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | | 250.0 | | LIABILITY | 145023 | Suspense/Clearing account | 250.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 April 2024" which has the following Journal entries: - | Type | Account code | Account name | Debit | Credit | - | ASSET | 112603 | Interest/Fee Receivable | 30.0 | | - | INCOME | 404007 | Fee Income | | 30.0 | Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 April 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | LIABILITY | 145023 | Suspense/Clearing account | | 280.0 | @@ -2563,10 +2559,6 @@ Feature: LoanChargeback | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | | 250.0 | | LIABILITY | 145023 | Suspense/Clearing account | 250.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 April 2024" which has the following Journal entries: - | Type | Account code | Account name | Debit | Credit | - | ASSET | 112603 | Interest/Fee Receivable | 30.0 | | - | INCOME | 404007 | Fee Income | | 30.0 | Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 April 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | LIABILITY | 145023 | Suspense/Clearing account | | 280.0 | @@ -3041,7 +3033,6 @@ Feature: LoanChargeback | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 01 March 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 | Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -3052,10 +3043,6 @@ Feature: LoanChargeback | ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries: - | Type | Account code | Account name | Debit | Credit | - | ASSET | 112603 | Interest/Fee Receivable | 5.0 | | - | INCOME | 404007 | Fee Income | | 5.0 | Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 March 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | LIABILITY | 145023 | Suspense/Clearing account | | 3.0 | @@ -3079,7 +3066,6 @@ Feature: LoanChargeback | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 01 March 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 | | 05 March 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 8.0 | 0.0 | Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries: @@ -3091,10 +3077,6 @@ Feature: LoanChargeback | ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries: - | Type | Account code | Account name | Debit | Credit | - | ASSET | 112603 | Interest/Fee Receivable | 5.0 | | - | INCOME | 404007 | Fee Income | | 5.0 | Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 March 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | LIABILITY | 145023 | Suspense/Clearing account | | 3.0 | @@ -3156,6 +3138,8 @@ Feature: LoanChargeback # ---Chargeback with amount < fee --- When Admin sets the business date to "15 April 2024" When Admin makes "REPAYMENT_ADJUSTMENT_REFUND" chargeback with 3 EUR transaction amount for Payment nr. 1 + When Admin sets the business date to "16 April 2024" + When Admin runs inline COB job for Loan Then Loan Repayment schedule has 5 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 | | | | @@ -3171,8 +3155,8 @@ Feature: LoanChargeback | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 | + | 15 April 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | 100.0 | | @@ -3182,7 +3166,7 @@ Feature: LoanChargeback | ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries: + Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | ASSET | 112603 | Interest/Fee Receivable | 5.0 | | | INCOME | 404007 | Fee Income | | 5.0 | @@ -3210,8 +3194,8 @@ Feature: LoanChargeback | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 | + | 15 April 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 20 April 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 8.0 | 0.0 | Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -3222,7 +3206,7 @@ Feature: LoanChargeback | ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | - Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries: + Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | ASSET | 112603 | Interest/Fee Receivable | 5.0 | | | INCOME | 404007 | Fee Income | | 5.0 | 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 78fb7bfd37f..ae5583fe14b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -875,9 +875,9 @@ Feature: LoanRepayment Then Loan Repayment schedule has 3 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 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 01 February 2023 | | 666.67 | 333.33 | 10.0 | 103.0 | 10.0 | 456.33 | 400.0 | 400.0 | 0.0 | 56.33 | - | 2 | 28 | 01 March 2023 | | 333.34 | 333.33 | 10.0 | 0.0 | 0.0 | 343.33 | 0.0 | 0.0 | 0.0 | 343.33 | - | 3 | 31 | 01 April 2023 | | 0.0 | 333.34 | 10.0 | 0.0 | 0.0 | 343.34 | 0.0 | 0.0 | 0.0 | 343.34 | + | 1 | 31 | 01 February 2023 | | 667.0 | 333.0 | 10.0 | 103.0 | 10.0 | 456.0 | 400.0 | 400.0 | 0.0 | 56.0 | + | 2 | 28 | 01 March 2023 | | 334.0 | 333.0 | 10.0 | 0.0 | 0.0 | 343.0 | 0.0 | 0.0 | 0.0 | 343.0 | + | 3 | 31 | 01 April 2023 | | 0.0 | 334.0 | 10.0 | 0.0 | 0.0 | 344.0 | 0.0 | 0.0 | 0.0 | 344.0 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 1000 | 30 | 103 | 10 | 1143 | 400 | 400 | 0 | 743 | @@ -2110,7 +2110,6 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 25 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 25 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2123,9 +2122,8 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 25 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 25 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 February 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 01 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 01 February 2023 | Accrual | 40.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 01 February 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2148,9 +2146,7 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 25 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 25 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 February 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 01 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 08 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2164,11 +2160,9 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 25 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 25 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 01 February 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 01 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | | 08 February 2023 | Repayment | 1060.0 | 1000.0 | 0.0 | 20.0 | 40.0 | 0.0 | - | 08 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 08 February 2023 | Accrual | 60.0 | 0.0 | 0.0 | 20.0 | 40.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 08 February 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2358,7 +2352,6 @@ Feature: LoanRepayment Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2372,7 +2365,6 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2393,7 +2385,6 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | | 01 February 2023 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | Then Loan Charges tab has the following data: @@ -2415,11 +2406,10 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | | 01 February 2023 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 05 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 05 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 05 February 2023 | Accrual | 40.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 01 February 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2472,7 +2462,6 @@ Feature: LoanRepayment Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2486,7 +2475,6 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2507,10 +2495,9 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | | 30 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 30 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 30 January 2023 | Accrual | 40.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 01 February 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2563,7 +2550,6 @@ Feature: LoanRepayment Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2577,9 +2563,8 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 28 January 2023 | Accrual | 40.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2602,9 +2587,7 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2618,9 +2601,7 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2642,12 +2623,10 @@ Feature: LoanRepayment | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | - | 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 05 February 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 | - | 05 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 05 February 2023 | Accrual | 60.0 | 0.0 | 0.0 | 20.0 | 40.0 | 0.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 01 February 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | @@ -2751,7 +2730,6 @@ Feature: LoanRepayment | 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 | | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 | - | 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | @@ -2771,7 +2749,6 @@ Feature: LoanRepayment | 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 | | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 | - | 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 | And Customer makes "AUTOPAY" repayment on "01 February 2023" with 1000 EUR transaction amount @@ -2781,7 +2758,6 @@ Feature: LoanRepayment | 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 | | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 | - | 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 | | 01 February 2023 | Repayment | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | @@ -2792,11 +2768,11 @@ Feature: LoanRepayment | 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 | | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 | - | 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 | | 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 | | 01 February 2023 | Repayment | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | | 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 | + | 01 February 2023 | Accrual | 40.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | Then On Loan Transactions tab the "Repayment" Transaction with date "10 January 2023" is reverted Then On Loan Transactions tab the "Repayment" Transaction with date "15 January 2023" is reverted Then On Loan Transactions tab the "Repayment" Transaction with date "25 January 2023" is reverted @@ -3614,10 +3590,11 @@ Feature: LoanRepayment | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 23 August 2024 | 22 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 0.0 | 111.92 | 111.92 | 111.92 | 0.0 | 0.0 | - | 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 | + | 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | + | 2 | 2 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 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 | - | 111.92 | 0.0 | 0.0 | 2.8 | 114.72 | 114.72 | 114.72 | 0.0 | 0.0 | + | 111.92 | 0.0 | 0.0 | 5.6 | 117.52 | 114.72 | 114.72 | 0.0 | 2.8 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false | false | @@ -3642,7 +3619,8 @@ Feature: LoanRepayment | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 23 August 2024 | 22 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 0.0 | 111.92 | 111.92 | 111.92 | 0.0 | 0.0 | - | 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 | + | 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | + | 3 | 2 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 | When Admin sets the business date to "29 August 2024" When Admin runs inline COB job for Loan When Admin adds "LOAN_NSF_FEE" due date charge with "22 August 2024" due date and 5 EUR transaction amount @@ -3655,7 +3633,6 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | - | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | Then Loan Repayment schedule has 2 periods, with the following data for periods: @@ -3675,10 +3652,8 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | - | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | - | 25 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | Then Loan Repayment schedule has 2 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 | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java index efe3d72140e..7674a7777c4 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java @@ -41,7 +41,7 @@ public void addListeners() { businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, event -> { final Loan loan = event.get(); if (configurationReadPlatformService.retrieveGlobalConfiguration(ASSET_EXTERNALIZATION_OF_NON_ACTIVE_LOANS).isEnabled() - && (loan.isClosed() || loan.getStatus().isOverpaid())) { + && (event.getOldStatus().isActive() && (loan.isClosed() || loan.getStatus().isOverpaid()))) { loanAccountOwnerTransferService.handleLoanClosedOrOverpaid(loan); } }); diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java index 0dc2718c286..78c68a2ae9e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java @@ -19,17 +19,24 @@ package org.apache.fineract.infrastructure.event.business.domain.loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; public class LoanStatusChangedBusinessEvent extends LoanBusinessEvent { private static final String TYPE = "LoanStatusChangedBusinessEvent"; + private final LoanStatus oldStatus; - public LoanStatusChangedBusinessEvent(Loan value) { + public LoanStatusChangedBusinessEvent(Loan value, LoanStatus oldStatus) { super(value); + this.oldStatus = oldStatus; } @Override public String getType() { return TYPE; } + + public LoanStatus getOldStatus() { + return oldStatus; + } } 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..d3109bd7ef5 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.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..b9985058f5e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.java @@ -0,0 +1,87 @@ +/** + * 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 Money unrecognizedWaive; + 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/DefaultLoanLifecycleStateMachine.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java index 06d2251e043..ccb056d827a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java @@ -55,7 +55,7 @@ public void transition(final LoanEvent loanEvent, final Loan loan) { if (isNotLoanCreation(loanEvent)) { // in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change - businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan, oldStatus)); } // set mandatory field states based on new status after the transition 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..6ad0d481658 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(); @@ -1289,7 +1282,7 @@ public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { addLoanRepaymentScheduleInstallment(installment); } } - + recalculateTransactionInstallmentMappings(); updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); } @@ -1308,9 +1301,9 @@ public void updateLoanSchedule(final Collection installments, @@ -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())); @@ -5211,7 +5152,7 @@ public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransact } public void validateForForeclosure(final LocalDate transactionDate) { - if (isInterestRecalculationEnabledForProduct()) { + if (getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed."; throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage, getId()); @@ -5304,6 +5245,7 @@ public void updateLoanScheduleOnForeclosure(final Collection 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); } @@ -5482,4 +5423,34 @@ public void setIsTopup(boolean topup) { public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor() { return transactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); } + + private void recalculateTransactionInstallmentMappings() { + List transactions = getLoanTransactions(t -> t.isAccrual() || t.isAccrualAdjustment()); + getRepaymentScheduleInstallments().forEach(i -> i.getLoanTransactionToRepaymentScheduleMappings().clear()); + for (LoanTransaction transaction : transactions) { + Set transactionMappings = transaction + .getLoanTransactionToRepaymentScheduleMappings(); + ArrayList newMappings = new ArrayList<>(); + if (!transaction.isReversed()) { + for (LoanTransactionToRepaymentScheduleMapping transactionMapping : transactionMappings) { + LoanRepaymentScheduleInstallment mappedInstallment = transactionMapping.getLoanRepaymentScheduleInstallment(); + LoanRepaymentScheduleInstallment loanInstallment = transaction.getLoan() + .fetchRepaymentScheduleInstallment(mappedInstallment.getInstallmentNumber()); + Set installmentMappings = loanInstallment + .getLoanTransactionToRepaymentScheduleMappings(); + LoanTransactionToRepaymentScheduleMapping newMapping = transactionMapping; + if (mappedInstallment != loanInstallment) { + newMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, loanInstallment, null, null, null, + null); + newMapping.setComponents(transactionMapping.getPrincipalPortion(), transactionMapping.getInterestPortion(), + transactionMapping.getFeeChargesPortion(), transactionMapping.getPenaltyChargesPortion()); + } + installmentMappings.add(newMapping); + newMappings.add(newMapping); + } + } + transactionMappings.clear(); + transactionMappings.addAll(newMappings); + } + } } 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..3f2b4907ef4 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,39 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency cu .feeCharges(feeCharges) // .penaltyCharges(penaltyCharges); } + + @Override + public Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) { + if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) { + return null; + } + if (isBeforePeriod(targetDate, installment, false)) { + return null; + } + Loan loan = installment.getLoan(); + 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..33666a79762 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,5 @@ OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, Local MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + Money getPeriodInterestTillDate(@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..91addc4f69c 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,33 @@ */ 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 processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled); - void reprocessExistingAccruals(Loan loan); + void processIncomePostingAndAccruals(@NotNull Loan loan); - void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled); + void addIncomeAndAccrualTransactions(Long loanId) throws Exception; - void processIncomePostingAndAccruals(Loan loan); + void processAccrualsOnLoanClosure(@NotNull Loan loan); - void processAccrualsForLoanClosure(Loan loan); + void processAccrualsOnLoanReopen(@NotNull Loan loan); - void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection newAccrualTransactions); + void processAccrualsOnLoanForeClosure(@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..79921586fbd 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().getLoanProductRelatedDetail().isInterestRecalculationEnabled() && !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..82bef12abff 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,61 @@ 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 LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) { + Loan loan = installment.getLoan(); + LoanRepaymentScheduleTransactionProcessor transactionProcessor = loan.getTransactionProcessor(); + if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) { + return Money.zero(loan.getCurrency()); + } + 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 +295,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 +444,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/cob/loan/LoanInterestRecalculationCOBBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java index cc0c401f336..a5f69804c9d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java @@ -36,7 +36,7 @@ public class LoanInterestRecalculationCOBBusinessStep implements LoanCOBBusiness @Override public Loan execute(Loan loan) { if (!loan.isInterestBearing() || !loan.getStatus().isActive() || loan.isNpa() || loan.isChargedOff() - || !loan.isInterestRecalculationEnabledForProduct()) { + || !loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { log.debug("Skip processing loan interest recalculation [{}] - reason: not interest bearing loan or not active.", loan.getId()); return loan; } 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 abe81fc1f36..cb4254bc9aa 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 @@ -759,7 +759,7 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final ScheduleGeneratorDTO scheduleGeneratorDTO = null; final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); - loanAccrualsProcessingService.processAccrualsForLoanForeClosure(loan, foreClosureDate, newTransactions); + loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions); Money interestPayable = foreCloseDetail.getInterestCharged(currency); Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); @@ -922,15 +922,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/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index bfe83b9e1c7..34996041ea4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -635,7 +635,8 @@ private void validateForCreate(final JsonElement element) { "error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); } - if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { + if (loanToClose.isMultiDisburmentLoan() + && !loanToClose.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { throw new GeneralPlatformDomainRuleException( "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); @@ -1346,7 +1347,8 @@ public void validateForModify(final JsonCommand command, final Loan loan) { "error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); } - if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { + if (loanToClose.isMultiDisburmentLoan() + && !loanToClose.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { throw new GeneralPlatformDomainRuleException( "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); 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..cedbbe16119 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 @@ -6,9 +6,9 @@ * 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 @@ -18,68 +18,67 @@ */ 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.LoanAdjustTransactionBusinessEvent; +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 +87,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 +126,25 @@ 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" + */ + @Override + @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,669 +153,531 @@ 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; + } + ensureAccrualTransactionMappings(loan); + + 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); } - } - - @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); - } - 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); + if (!isFinal) { + loan.setAccruedTill(tillDate); } - } - /** - * 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()) { - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - reprocessPeriodicAccruals(loan, accruals); - } else if (loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - reprocessNonPeriodicAccruals(loan, accruals); - } - } - } - - /** - * method calculates accruals for loan with interest recalculation on loan schedule when interest is recalculated - */ - @Override - @Transactional - public void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled) { - LocalDate accruedTill = loan.getAccruedTill(); - if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestRecalculationEnabled || accruedTill == null - || loan.isNpa() || !loan.getStatus().isActive() || loan.isChargedOff()) { + if (accrualTransactions.isEmpty()) { 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); + 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); + } } - /** - * 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(); - } - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment - .getLastNonDownPaymentInstallment(installments); - reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); - reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); + 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; } - /** - * method calculates accruals for loan on loan closure - */ - @Override - public void processAccrualsForLoanClosure(Loan loan) { - // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income - processAccrualTransactionsOnLoanClosure(loan); - - // check and process accruals for loan WITH interest recalculation details and compounding posted as income - processIncomeAndAccrualTransactionOnLoanClosure(loan); + @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 -> !i.isDownPayment() && (!isChargeOnDueDate() + || (periodic ? !isBeforePeriod(tillDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber)) + : isFullPeriod(tillDate, i))) + && !isAfterPeriod(organisationStartDate, i)); } - /** - * method calculates accruals for loan on loan fore closure - */ - @Override - public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions) { - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() - && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); - MonetaryCurrency currency = loan.getCurrency(); - reverseTransactionsPostEffectiveDate(retrieveListOfAccrualTransactions(loan), foreClosureDate); - - HashMap incomeDetails = new HashMap<>(); - - determineReceivableIncomeForeClosure(loan, foreClosureDate, incomeDetails); - - Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus((Money) incomeDetails.get(Loan.INTEREST)); - Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus((Money) incomeDetails.get(Loan.FEE)); - Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus((Money) incomeDetails.get(Loan.PENALTIES)); - Money total = interestPortion.plus(feePortion).plus(penaltyPortion); - - if (total.isGreaterThanZero()) { - createAccrualTransactionAndUpdateChargesPaidBy(loan, foreClosureDate, newAccrualTransactions, currency, interestPortion, - feePortion, penaltyPortion, total); - } + private void addInterestAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, LoanScheduleGenerator scheduleGenerator, + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + if (installment.isAdditional() || installment.isReAged()) { + return; } + 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(installment, tillDate); + } + period.setInterestAmount(interest); + Money accruable = null; + Money transactionWaived = null; + if (!MathUtil.isEmpty(interest)) { + transactionWaived = MathUtil.toMoney(calcInterestTransactionWaivedAmount(installment, tillDate), currency); + Money unrecognizedWaived = MathUtil.toMoney(calcInterestUnrecognizedWaivedAmount(installment, accrualPeriods, tillDate), + currency); + // unrecognized maximum is the waived portion which is not covered by waiver transactions + unrecognizedWaived = MathUtil.min(unrecognizedWaived, + MathUtil.minusToZero(installment.getInterestWaived(currency), transactionWaived), false); + period.setUnrecognizedWaive(unrecognizedWaived); + Money waived = isFullPeriod ? installment.getInterestWaived(currency) : MathUtil.plus(transactionWaived, unrecognizedWaived); + accruable = MathUtil.minusToZero(period.getInterestAmount(), waived); + } + period.setInterestAccruable(accruable); + Money accrued = isFullPeriod ? installment.getInterestAccrued(currency) + : MathUtil.toMoney(calcInterestAccruedAmount(installment), currency); + accrued = MathUtil.minusToZero(accrued, transactionWaived); + period.setInterestAccrued(accrued); } - 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); - } + @NotNull + private BigDecimal calcInterestTransactionWaivedAmount(@NotNull LoanRepaymentScheduleInstallment installment, + @NotNull LocalDate tillDate) { + Predicate transactionPredicate = t -> !t.isReversed() && t.isInterestWaiver() + && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + return installment.getLoanTransactionToRepaymentScheduleMappings().stream() + .filter(tm -> transactionPredicate.test(tm.getLoanTransaction())) + .map(LoanTransactionToRepaymentScheduleMapping::getInterestPortion).reduce(BigDecimal.ZERO, MathUtil::add); } - 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(); - } - } + @NotNull + private BigDecimal calcInterestUnrecognizedWaivedAmount(@NotNull LoanRepaymentScheduleInstallment installment, + @NotNull AccrualPeriodsData accrualPeriods, @NotNull LocalDate tillDate) { + // unrecognized amount of the transaction is not mapped to installments + LocalDate dueDate = installment.getDueDate(); + LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; + Predicate transactionPredicate = t -> !t.isReversed() && t.isInterestWaiver() + && !DateUtils.isAfter(t.getTransactionDate(), toDate); + Loan loan = installment.getLoan(); + BigDecimal totalUnrecognized = loan.getLoanTransactions().stream().filter(transactionPredicate) + .map(LoanTransaction::getUnrecognizedIncomePortion).reduce(BigDecimal.ZERO, MathUtil::add); + // total unrecognized amount from previous periods + BigDecimal prevUnrecognized = accrualPeriods.getPeriods().stream() + .filter(p -> p.getInstallmentNumber() < installment.getInstallmentNumber()) + .map(p -> MathUtil.toBigDecimal(p.getUnrecognizedWaive())).reduce(BigDecimal.ZERO, MathUtil::add); + // unrecognized amount left for this period (and maybe more) + return MathUtil.min(installment.getInterestWaived(), MathUtil.subtractToZero(totalUnrecognized, prevUnrecognized), false); + } - 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; + @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); + } - if (daysToBeAccrued >= totalNumberOfDays) { - interestPortion = accrualData.getAccruableIncome(); - } else { - interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + 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); } - 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; - } + 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) + : MathUtil.toMoney(calcChargeWaivedAmount(paidBys, tillDate), currency); + installmentChargeId = installmentCharge.getId(); + } else { + chargeAmount = loanCharge.getAmount(currency); + paidBys = loanCharge.getLoanChargePaidBySet(); + waived = isFullPeriod ? loanCharge.getAmountWaived(currency) + : MathUtil.toMoney(calcChargeWaivedAmount(paidBys, tillDate), currency); } + AccrualChargeData chargeData = new AccrualChargeData(loanCharge.getId(), installmentChargeId, loanCharge.isPenaltyCharge()) + .setChargeAmount(chargeAmount); + chargeData.setChargeAccruable(MathUtil.minusToZero(chargeAmount, waived)); - // 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; - } - } + Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency); + Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived); + chargeData.setChargeAccrued(MathUtil.minusToZero(MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency), transactionWaived)); - // 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; - } - } + duePeriod.addCharge(chargeData); + } - 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()); - } - } + @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); } - private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestPortion, - BigDecimal totalAccInterest, BigDecimal feePortion, BigDecimal totalAccFee, BigDecimal penaltyPortion, - BigDecimal totalAccPenalty, final LocalDate accruedTill) throws DataAccessException { + @NotNull + private BigDecimal calcChargeUnrecognizedWaivedAmount(@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(pb -> pb.getLoanTransaction().getUnrecognizedIncomePortion()).reduce(BigDecimal.ZERO, MathUtil::add); + } - AppUser user = context.authenticatedUser(); - Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId()); - Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId()); - MonetaryCurrency currency = loan.getCurrency(); + @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); + } - // create accrual Transaction - LoanTransaction loanTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, - externalIdFactory.create()); + 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); + } - // update charges paid by - Map applicableCharges = scheduleAccrualData.getApplicableCharges(); + 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); - 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())); + // 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; } - loanTransactionRepository.saveAndFlush(loanTransaction); - loan.addLoanTransaction(loanTransaction); - - Map transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion, - scheduleAccrualData, accruedTill); - + transaction.updateComponentsAndTotal(null, interestPortion, feePortion, penaltyPortion); // 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); + addTransactionMappings(transaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, adjustment); } - private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, - final Map transactionMap) { + private void addTransactionMappings(@NotNull LoanTransaction transaction, AccrualPeriodData accrualAmounts, Money interestPortion, + Money feePortion, Money penaltyPortion, boolean adjustment) { + Loan loan = transaction.getLoan(); + Integer installmentNumber = accrualAmounts.getInstallmentNumber(); + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(installmentNumber); - 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("isAccountTransfer", false); - accountingBridgeData.put("isChargeOff", false); - accountingBridgeData.put("isFraud", false); - - final List> newLoanTransactions = new ArrayList<>(); - newLoanTransactions.add(transactionMap); + // add installment mapping + LoanTransactionToRepaymentScheduleMapping installmentMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, + installment, null, interestPortion, feePortion, penaltyPortion); + installment.getLoanTransactionToRepaymentScheduleMappings().add(installmentMapping); + transaction.getLoanTransactionToRepaymentScheduleMappings().add(installmentMapping); - accountingBridgeData.put("newLoanTransactions", newLoanTransactions); - return accountingBridgeData; + // add charges paid by mappings + addPaidByMappings(transaction, installment, accrualAmounts, adjustment); } - 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); + 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); } - 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 boolean isFullPeriod(@NotNull LocalDate tillDate, @NotNull LoanRepaymentScheduleInstallment installment) { + return isAfterPeriod(tillDate, installment) || DateUtils.isEqual(tillDate, installment.getDueDate()); } - 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); + /** + * method updates accrual derived fields on installments and reverse the unprocessed transactions for loan + * reschedule + */ + @Override + public void reprocessExistingAccruals(@NotNull Loan loan) { + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + if (!accrualTransactions.isEmpty()) { + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + reprocessPeriodicAccruals(loan, accrualTransactions); + } else if (loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + reprocessNonPeriodicAccruals(loan, accrualTransactions); } - } + } - if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDateFeeIncome = null; + /** + * method calculates accruals for loan with interest recalculation on loan schedule when interest is recalculated + */ + @Override + @Transactional + public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) { + if (isProgressiveAccrual(loan)) { + return; } - - if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDatePenaltyIncome = null; + LocalDate accruedTill = loan.getAccruedTill(); + if (!isInterestRecalculationEnabled || accruedTill == null) { + return; + } + try { + addPeriodicAccruals(accruedTill, loan); + } catch (Exception e) { + String globalisationMessageCode = "error.msg.accrual.exception"; + throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); } - - 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); - } + /** + * method calculates accruals for loan with interest recalculation and compounding to be posted as income + */ + @Override + public void processIncomePostingAndAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; } - - if (dueDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDateFeeIncome = null; + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails == null || !recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; } - - if (dueDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDatePenaltyIncome = null; + 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(); } - - 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); + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments); + reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); + reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); } - private BigDecimal calculateDueDateCharges(Map applicableCharges, LoanChargeData loanCharge, - BigDecimal chargeAmount) { - BigDecimal dueDateChargeAmount = chargeAmount; - if (loanCharge.getAmountUnrecognized() != null) { - dueDateChargeAmount = dueDateChargeAmount.subtract(loanCharge.getAmountUnrecognized()); + @Transactional + @Override + public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException { + if (loanId == null) { + return; } - 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); + Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + if (isProgressiveAccrual(loan)) { + return; } - return dueDateChargeAmount; + 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 BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrualData, - Map applicableCharges, LoanChargeData loanCharge, BigDecimal chargeAmount) { - BigDecimal installmentFeeChargeAmount = chargeAmount; - Collection installmentData = loanCharge.getInstallmentChargeData(); - for (LoanInstallmentChargeData installmentChargeData : installmentData) { + /** + * method calculates accruals for loan on loan closure + */ + @Override + public void processAccrualsOnLoanClosure(@NotNull Loan loan) { + reprocessExistingAccruals(loan); + // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income + addAccruals(loan, loan.getLastLoanRepaymentScheduleInstallment().getDueDate(), false, true, false); - 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; + // check and process accruals for loan WITH interest recalculation details and compounding posted as income + processIncomeAndAccrualTransactionOnLoanClosure(loan); } - 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(); + @Override + public void processAccrualsOnLoanReopen(@NotNull Loan loan) { + LocalDate transactionDate = loan.getOverpaidOnDate() != null ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); + if (transactionDate == null) { + return; } - if (accrualData.getWaivedInterestIncome() != null) { - Collection loanTransactionDatas = new ArrayList<>(); - - getLoanWaiverTransactionsInRange(accrualData, loanWaiverTransactions, tillDate, loanTransactionDatas); + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + LocalDate accruedTill = loan.getAccruedTill(); + LoanTransaction closureAccrual = loan.getLoanTransactions().stream().filter(transaction -> transaction.isNotReversed() // + && transaction.isAccrual() // + && DateUtils.isEqual(transaction.getTransactionDate(), transactionDate) // + && transaction.getLoanTransactionToRepaymentScheduleMappings().stream().anyMatch(tm -> { + LoanRepaymentScheduleInstallment installment = tm.getLoanRepaymentScheduleInstallment(); + return isBeforePeriod(accruedTill, installment, installment.getInstallmentNumber().equals(firstInstallmentNumber)); + })).sorted(LoanTransactionComparator.INSTANCE).reduce((first, second) -> second).orElse(null); + if (closureAccrual != null) { + closureAccrual.reverse(); + LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(closureAccrual); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); + } + } - BigDecimal recognized = getWaivedInterestIncome(accrualData, loanSchedulePeriodDataList, loanTransactionDatas); + /** + * method calculates accruals for loan on loan fore closure + */ + @Override + public void processAccrualsOnLoanForeClosure(@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); + MonetaryCurrency currency = loan.getCurrency(); + reverseTransactionsPostEffectiveDate(retrieveListOfAccrualTransactions(loan), foreClosureDate); - BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); - if (interestWaived.compareTo(recognized) > 0) { - interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); - } - } + HashMap incomeDetails = new HashMap<>(); - accrualData.updateAccruableIncome(interestIncome); - } + determineReceivableIncomeForeClosure(loan, foreClosureDate, incomeDetails); - 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; - } - } + Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus((Money) incomeDetails.get(Loan.INTEREST)); + Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus((Money) incomeDetails.get(Loan.FEE)); + Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus((Money) incomeDetails.get(Loan.PENALTIES)); + Money total = interestPortion.plus(feePortion).plus(penaltyPortion); + if (total.isGreaterThanZero()) { + createAccrualTransactionAndUpdateChargesPaidBy(loan, foreClosureDate, newAccrualTransactions, currency, interestPortion, + feePortion, penaltyPortion, total); } } - 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 Map deriveAccountingBridgeData(@NotNull Loan loan, List> newLoanTransactions) { + final Map accountingBridgeData = new LinkedHashMap<>(); + 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); + accountingBridgeData.put("newLoanTransactions", newLoanTransactions); + return accountingBridgeData; } private void postJournalEntries(final Loan loan, final List existingTransactionIds, @@ -828,45 +689,116 @@ 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; } + ensureAccrualTransactionMappings(loan); + 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 ensureAccrualTransactionMappings(Loan loan) { + boolean chargeOnDueDate = isChargeOnDueDate(); + MonetaryCurrency currency = loan.getCurrency(); + List transactions = loan.getLoanTransactions(t -> !t.isReversed() && (t.isAccrual() || t.isAccrualAdjustment())); + for (LoanTransaction transaction : transactions) { + if (transaction.getLoanTransactionToRepaymentScheduleMappings().isEmpty()) { + HashMap newMappings = new HashMap<>(); + LocalDate transactionDate = transaction.getTransactionDate(); + LoanRepaymentScheduleInstallment installment = loan + .getRepaymentScheduleInstallment(i -> isInPeriod(transactionDate, i, false)); + LoanTransactionToRepaymentScheduleMapping mapping; + if (installment != null) { + mapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, installment, null, + transaction.getInterestPortion(currency), null, null); + newMappings.put(installment.getInstallmentNumber(), mapping); + } + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + for (LoanChargePaidBy paidBy : transaction.getLoanChargesPaid()) { + LoanCharge loanCharge = paidBy.getLoanCharge(); + if (paidBy.getInstallmentNumber() == null) { + LocalDate chargeDate = (chargeOnDueDate || loanCharge.isInstalmentFee()) ? transaction.getTransactionDate() + : loanCharge.getDueDate(); + installment = loan.getRepaymentScheduleInstallment( + i -> isInPeriod(chargeDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber))); + paidBy.setInstallmentNumber(installment.getInstallmentNumber()); + } else { + installment = loan.fetchRepaymentScheduleInstallment(paidBy.getInstallmentNumber()); + } + Money amount = MathUtil.toMoney(paidBy.getAmount(), currency); + boolean isFee = loanCharge.isFeeCharge(); + Money feePortion = isFee ? amount : null; + Money penaltyPortion = isFee ? null : amount; + Integer installmentNumber = installment.getInstallmentNumber(); + mapping = newMappings.get(installmentNumber); + if (mapping == null) { + mapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, installment, null, null, feePortion, + penaltyPortion); + newMappings.put(installmentNumber, mapping); + } else { + mapping.updateComponents(null, mapping.getInterestPortion(currency), feePortion, penaltyPortion); + } + } + for (LoanTransactionToRepaymentScheduleMapping newMapping : newMappings.values()) { + newMapping.getLoanRepaymentScheduleInstallment().getLoanTransactionToRepaymentScheduleMappings().add(newMapping); + transaction.getLoanTransactionToRepaymentScheduleMappings().add(newMapping); + } + } + } + } + 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 +816,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 +849,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 +885,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 +908,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 +984,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 +1077,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 +1085,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 +1165,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 +1188,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..0792bcd602f 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,155 +1872,12 @@ 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=?"; return TRUE.equals(this.jdbcTemplate.queryForObject(sql, Boolean.class, loanId)); } - private static final class LoanTransactionDerivedComponentMapper implements RowMapper { - - private final DatabaseSpecificSQLGenerator sqlGenerator; - - LoanTransactionDerivedComponentMapper(DatabaseSpecificSQLGenerator sqlGenerator) { - this.sqlGenerator = sqlGenerator; - } - - public String schema() { - - return " tr.id as id, tr.transaction_type_enum as transactionType, tr.transaction_date as " + sqlGenerator.escape("date") - + ", tr.amount as total, tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, " - + " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, " - + " tr.overpayment_portion_derived as overpayment, tr.outstanding_loan_balance_derived as outstandingLoanBalance, " - + " tr.unrecognized_income_portion as unrecognizedIncome, tr.loan_id as loanId, l.external_id as externalLoanId, " - + " tr.external_id as externalId from m_loan_transaction tr " + " left join m_loan l on tr.loan_id = l.id"; - } - - @Override - public LoanTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long loanId = rs.getLong("loanId"); - final String externalLoanIdStr = rs.getString("externalLoanId"); - final ExternalId externalLoanId = ExternalIdFactory.produce(externalLoanIdStr); - final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); - final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(transactionTypeInt); - - final LocalDate date = JdbcSupport.getLocalDate(rs, "date"); - final BigDecimal totalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "total"); - final BigDecimal principalPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principal"); - final BigDecimal interestPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interest"); - final BigDecimal feeChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "fees"); - final BigDecimal penaltyChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalties"); - final BigDecimal overPaymentPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overpayment"); - final BigDecimal unrecognizedIncomePortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "unrecognizedIncome"); - final BigDecimal outstandingLoanBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "outstandingLoanBalance"); - final String externalIdStr = rs.getString("externalId"); - final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - - return new LoanTransactionData(id, transactionType, date, totalAmount, null, principalPortion, interestPortion, - feeChargesPortion, penaltyChargesPortion, overPaymentPortion, unrecognizedIncomePortion, outstandingLoanBalance, false, - externalId, loanId, externalLoanId); - } - } - - @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..06f402c41cd 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 @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; @Slf4j @RequiredArgsConstructor @@ -47,12 +48,15 @@ private final class LoanStatusChangedListener implements BusinessEventListener 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 8b9490f07a1..77509f46496 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 @@ -30,6 +30,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; @@ -60,7 +61,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(), @@ -97,7 +98,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 8f8b7a6ff59..59f48876ff5 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 f3c75450827..097ac87a04a 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") // ); }); } @@ -1176,7 +1185,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(); @@ -1185,9 +1195,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() {