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 fbec2a84f..5030f2912 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 c7cc68239..abd567514 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 final class DateUtils { } 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 final class DateUtils { } 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) { @@ -325,6 +353,10 @@ public final class DateUtils { return getExactDifference(first, second, DAYS); } + public static LocalDate minusDays(LocalDate first, int days) { + return first == null ? null : first.minusDays(days); + } + // Parse, format public static LocalDate parseLocalDate(String stringDate) { @@ -426,23 +458,4 @@ public final class DateUtils { } 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 4e2178d9a..99365b6c2 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 final class MathUtil { * 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 final class MathUtil { 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 final class MathUtil { 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 final class MathUtil { 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/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java index 78223141a..0951115bb 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java @@ -61,6 +61,7 @@ public enum DefaultLoanProduct implements LoanProduct { LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SARP_TILL_PRECLOSE, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_REST_FREQUENCY_DATE, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SARP_TILL_REST_FREQUENCY_DATE, // + LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT, // diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java index 03000bbea..08bd21ff5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java @@ -447,13 +447,26 @@ public final class ErrorMessageHelper { } public static String wrongNumberOfLinesInRepaymentSchedule(int actual, int expected) { + return wrongNumberOfLinesInRepaymentSchedule(null, actual, expected); + } + + public static String wrongNumberOfLinesInRepaymentSchedule(String resourceId, int actual, int expected) { String actualStr = String.valueOf(actual); String expectedStr = String.valueOf(expected); - return String.format("Number of lines in Repayment schedule is not correct. Actual value is: %s - Expected value is: %s", actualStr, - expectedStr); + String prefx = "Number of lines in Repayment schedule"; + String postfx = " is not correct. Actual value is: %s - Expected value is: %s"; + if (resourceId != null) { + return String.format(prefx + " of resource %s" + postfx, resourceId, actualStr, expectedStr); + } + return String.format(prefx + postfx, actualStr, expectedStr); } public static String wrongValueInLineInRepaymentSchedule(int line, List> actual, List expected) { + return wrongValueInLineInRepaymentSchedule(null, line, actual, expected); + } + + public static String wrongValueInLineInRepaymentSchedule(String resourceId, int line, List> actual, + List expected) { String lineStr = String.valueOf(line); String expectedStr = expected.toString(); StringBuilder sb = new StringBuilder(); @@ -461,13 +474,19 @@ public final class ErrorMessageHelper { sb.append(innerList.toString()); sb.append(System.lineSeparator()); } - - return String.format( - "%nWrong value in Repayment schedule tab line %s. %nActual values in line (with the same due date) are: %n%s %nExpected values in line: %n%s", - lineStr, sb.toString(), expectedStr); + String prefx = "%nWrong value in Repayment schedule"; + String postfx = " tab line %s. %nActual values in line (with the same due date) are: %n%s %nExpected values in line: %n%s"; + if (resourceId != null) { + return String.format(prefx + " of resource %s" + postfx, resourceId, lineStr, sb.toString(), expectedStr); + } + return String.format(prefx + postfx, lineStr, sb.toString(), expectedStr); } public static String wrongValueInLineInTransactionsTab(int line, List> actual, List expected) { + return wrongValueInLineInTransactionsTab(null, line, actual, expected); + } + + public static String wrongValueInLineInTransactionsTab(String resourceId, int line, List> actual, List expected) { String lineStr = String.valueOf(line); String expectedStr = expected.toString(); StringBuilder sb = new StringBuilder(); @@ -475,19 +494,28 @@ public final class ErrorMessageHelper { sb.append(innerList.toString()); sb.append(System.lineSeparator()); } - - return String.format( - "%nWrong value in Transactions tab line %s. %nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s", - lineStr, sb.toString(), expectedStr); + String prefx = "%nWrong value in Transactions tab"; + String postfx = " line %s. %nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s"; + if (resourceId != null) { + return String.format(prefx + " of resource %s" + postfx, resourceId, lineStr, sb.toString(), expectedStr); + } + return String.format(prefx + postfx, lineStr, sb.toString(), expectedStr); } public static String nrOfLinesWrongInTransactionsTab(int actual, int expected) { + return nrOfLinesWrongInTransactionsTab(null, actual, expected); + } + + public static String nrOfLinesWrongInTransactionsTab(String resourceId, int actual, int expected) { String actualStr = String.valueOf(actual); String expectedStr = String.valueOf(expected); - return String.format( - "%nNumber of lines does not match in Transactions tab and expected datatable. %nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n", - actualStr, expectedStr); + String prefx = "%nNumber of lines does not match in Transactions tab and expected datatable"; + String postfx = ". %nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n"; + if (resourceId != null) { + return String.format(prefx + " of resource %s" + postfx, resourceId, actualStr, expectedStr); + } + return String.format(prefx + postfx, actualStr, expectedStr); } public static String wrongValueInLineInChargesTab(int line, List> actual, List expected) { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index dea144118..6200b34ef 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -1045,6 +1045,49 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ TestContext.INSTANCE.set( TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT, responseLoanProductsRequestLP2AdvancedpaymentInterestRecalculation36030MultiDisburseDownPayment); + + // LP2 with progressive loan schedule + horizontal + interest recalculation daily EMI + 360/30 + multi + // disbursement + custom default payment allocation order + // (LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE) + String name50 = DefaultLoanProduct.LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE.getName(); + PostLoanProductsRequest loanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDailyEmi36030MultiDisburse = loanProductsRequestFactory + .defaultLoanProductsRequestLP2Emi()// + .name(name50)// + .daysInYearType(DaysInYearType.DAYS360.value)// + .daysInMonthType(DaysInMonthType.DAYS30.value)// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(4)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT", + LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST, // + LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL), // + createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), // + createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), // + createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")))// + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0);// + Response responseLoanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDaily36030MultiDisburse = loanProductsApi + .createLoanProduct(loanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDailyEmi36030MultiDisburse) + .execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE, + responseLoanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDaily36030MultiDisburse); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, 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 64fc54e3b..20d50d2af 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,8 +1613,10 @@ public class LoanStepDef extends AbstractStepDef { @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(); + String resourceId = String.valueOf(loanId); Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "repaymentSchedule", "", "").execute(); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); @@ -1634,9 +1636,9 @@ public class LoanStepDef extends AbstractStepDef { boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues)); assertThat(containsExpectedValues) - .as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(i, actualValuesList, expectedValues)).isTrue(); + .as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(resourceId, i, actualValuesList, expectedValues)).isTrue(); - assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(linesActual, linesExpected)) + assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(resourceId, linesActual, linesExpected)) .isEqualTo(linesExpected); } } @@ -1660,6 +1662,7 @@ public class LoanStepDef extends AbstractStepDef { public void loanTransactionsTransactionWithGivenDateDataCheck(String date, DataTable table) throws IOException { Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); long loanId = loanCreateResponse.body().getLoanId(); + String resourceId = String.valueOf(loanId); Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); @@ -1673,14 +1676,16 @@ public class LoanStepDef extends AbstractStepDef { .map(t -> fetchValuesOfTransaction(data.get(0), t)).collect(Collectors.toList()); boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues)); - assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(1, actualValuesList, expectedValues)) - .isTrue(); + assertThat(containsExpectedValues) + .as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, 1, actualValuesList, expectedValues)).isTrue(); } @Then("Loan Transactions tab has the following data:") public void loanTransactionsTabCheck(DataTable table) throws IOException { Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); long loanId = loanCreateResponse.body().getLoanId(); + String resourceId = String.valueOf(loanId); + Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); List transactions = loanDetailsResponse.body().getTransactions(); @@ -1694,10 +1699,11 @@ public class LoanStepDef extends AbstractStepDef { .collect(Collectors.toList());// boolean containsExpectedValues = actualValuesList.stream()// .anyMatch(actualValues -> actualValues.equals(expectedValues));// - assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(i, actualValuesList, expectedValues)) - .isTrue(); + assertThat(containsExpectedValues) + .as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, i, actualValuesList, expectedValues)).isTrue(); } - assertThat(transactions.size()).as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(transactions.size(), data.size() - 1)) + assertThat(transactions.size()) + .as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(resourceId, transactions.size(), data.size() - 1)) .isEqualTo(data.size() - 1); } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 6cc09c694..81f3aa120 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -96,6 +96,7 @@ public abstract class TestContextKey { public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SAME_AS_REP_TILL_REST_FREQUENCY_DATE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationSameAsRepTillRestFrequencyDate"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRefundFull"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030MultiDisburse"; + public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedCustomPaymentInterestRecalculationDailyEmi36030MultiDisburse"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030MultiDisburseDownPayment"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_365_ACTUAL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi365Actual"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_DOWNPAYMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030DownPayment"; 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 632bd2004..ece05f178 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 @@ -1911,8 +1915,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 | @TestRailId:C3227 Scenario: Verify interest recalculation in case of overdue installments: UC2 - 1st installment overdue, interest recalculation: daily, till rest frequency date @@ -1956,8 +1959,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 | @TestRailId:C3228 Scenario: Verify interest recalculation in case of overdue installments: UC3 - 1st installment overdue, interest recalculation: same as repayment period, till preclose @@ -2001,8 +2003,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.26 | 0.0 | 0.26 | 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 | @TestRailId:C3229 Scenario: Verify interest recalculation in case of overdue installments: UC4 - 1st installment overdue, interest recalculation: same as repayment period, till rest frequency date @@ -2046,8 +2047,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.26 | 0.0 | 0.26 | 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 | @TestRailId:C3230 Scenario: Verify interest recalculation in case of overdue installments: UC5 - 1st and 2nd installment overdue, interest recalculation: daily, till preclose @@ -2074,6 +2074,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: @@ -2091,10 +2093,45 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 0.0 | false | false | + | 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 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 18 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 20 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 22 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 23 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 24 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 25 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 26 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 27 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 28 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 29 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 March 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3231 Scenario: Verify interest recalculation in case of overdue installments: UC6 - 1st and 2nd installment overdue, interest recalculation: daily, till rest frequency date @@ -2121,6 +2158,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: @@ -2138,10 +2177,45 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 0.0 | false | false | + | 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 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 18 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 20 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 22 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 23 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 24 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 25 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 26 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 27 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 28 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 29 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 March 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3232 Scenario: Verify interest recalculation in case of overdue installments: UC7 - 1st and 2nd installment overdue, interest recalculation: same as repayment period, till preclose @@ -2168,6 +2242,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: @@ -2185,10 +2261,45 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false | + | 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 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 18 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 20 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 22 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 23 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 24 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 25 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 26 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 27 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 28 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 29 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 March 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3233 Scenario: Verify interest recalculation in case of overdue installments: UC8 - 1st and 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date @@ -2215,6 +2326,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: @@ -2232,10 +2345,45 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false | + | 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 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 18 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 20 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 22 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 23 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 24 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 25 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 26 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 27 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 28 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 29 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 01 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 March 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3234 Scenario: Verify interest recalculation in case of overdue installments: UC9 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: daily, till preclose @@ -2299,9 +2447,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 | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3235 Scenario: Verify interest recalculation in case of overdue installments: UC10 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: daily, till rest frequency date @@ -2365,9 +2511,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 | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3236 Scenario: Verify interest recalculation in case of overdue installments: UC11 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till preclose @@ -2431,9 +2575,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 | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3237 Scenario: Verify interest recalculation in case of overdue installments: UC12 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date @@ -2497,9 +2639,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 | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3238 Scenario: Verify interest recalculation in case of overdue installments: UC13 - 1st installment paid on due date, 2nd installment overdue with partial late repayment, interest recalculation: daily, till preclose @@ -2583,10 +2723,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.18 | 0.0 | 1.18 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3239 Scenario: Verify interest recalculation in case of overdue installments: UC14 - 1st installment paid on due date, 2nd installment overdue with partial late repayment, interest recalculation: daily, till rest frequency date @@ -2670,10 +2808,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.18 | 0.0 | 1.18 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3240 Scenario: Verify interest recalculation in case of overdue installments: UC15 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till preclose @@ -2757,10 +2893,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3241 Scenario: Verify interest recalculation in case of overdue installments: UC16 - 1st installment paid on due date, 2nd installment overdue, interest recalculation: same as repayment period, till rest frequency date @@ -2844,10 +2978,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | - | 01 February 2024 | Accrual | 0.58 | 0.0 | 0.58 | 0.0 | 0.0 | 0.0 | false | false | - | 01 March 2024 | Accrual | 0.49 | 0.0 | 0.49 | 0.0 | 0.0 | 0.0 | false | false | | 05 March 2024 | Repayment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 73.57 | false | false | - | 09 March 2024 | Accrual | 0.13 | 0.0 | 0.13 | 0.0 | 0.0 | 0.0 | false | false | + | 09 March 2024 | Accrual | 1.2 | 0.0 | 1.2 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3244 Scenario: Verify support of interest rate calculation with frequency Whole term configured for progressive loan @@ -2992,6 +3124,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 +3170,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 +3216,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 +3262,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 +3296,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 +3308,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 +3337,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 +3435,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 +3495,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 +3541,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 +3600,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 +3665,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 +3730,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 +3790,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 +3842,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 +3854,30 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | | 20 January 2024 | Repayment | 83.33 | 83.25 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.1 | 0.0 | 0.1 | 0.0 | 0.0 | 0.0 | false | false | -# TODO unskip and check when PS-2076 and PS-2106 is done -# TODO is it till preclose or tillrestperiod? - @Skip @TestRailId:C3259 + @TestRailId:C3259 Scenario: Verify Interest recalculation - EARLY repayment, adjust NEXT installment - UC9: 360/30, interest modification after early repayment 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,23 +3905,71 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan When Admin sets the business date to "20 January 2024" When Admin creates and approves Loan reschedule with the following data: | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | - | 01 March 2024 | 20 January 2024 | | | | | 4 | + | 21 January 2024 | 20 January 2024 | | | | | 4 | + When Admin runs inline COB job for Loan Then Loan Repayment schedule has 6 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 1 | 31 | 01 February 2024 | 15 January 2024 | 83.25 | 16.75 | 0.26 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | - | 2 | 29 | 01 March 2024 | | 66.88 | 16.37 | 0.47 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 3 | 31 | 01 April 2024 | | 50.26 | 16.62 | 0.22 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 4 | 30 | 01 May 2024 | | 33.59 | 16.67 | 0.17 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 5 | 31 | 01 June 2024 | | 16.86 | 16.73 | 0.11 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.86 | 0.06 | 0.0 | 0.0 | 16.92 | 0.0 | 0.0 | 0.0 | 16.92 | + | 2 | 29 | 01 March 2024 | | 66.86 | 16.39 | 0.47 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 3 | 31 | 01 April 2024 | | 50.22 | 16.64 | 0.22 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 4 | 30 | 01 May 2024 | | 33.53 | 16.69 | 0.17 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 5 | 31 | 01 June 2024 | | 16.78 | 16.75 | 0.11 | 0.0 | 0.0 | 16.86 | 0.0 | 0.0 | 0.0 | 16.86 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.78 | 0.06 | 0.0 | 0.0 | 16.84 | 0.0 | 0.0 | 0.0 | 16.84 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 100.0 | 1.29 | 0.0 | 0.0 | 101.29 | 17.01 | 17.01 | 0.0 | 84.28 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | + | 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "25 January 2024" + When Admin runs inline COB job for Loan + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 13 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | + | 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 21 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 22 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 23 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 24 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3269 Scenario: UC1 - Single disbursement, full refund within first installment period @@ -3824,6 +4043,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 +4062,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 +4180,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 +4201,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 +4268,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 +4337,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 +4410,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 +4469,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 +4527,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 +4551,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 +4590,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 | @@ -4447,18 +4673,18 @@ 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 | 15.0 | 0.0 | 0.0 | 0.0 | 85.0 | false | false | When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -4508,18 +4734,18 @@ 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 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 82.99 | 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 @@ -4569,15 +4795,15 @@ 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.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.01 | 0.0 | 0.01 | 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.01 | 0.0 | 0.01 | 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 | @@ -4631,18 +4857,18 @@ 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 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 82.99 | false | false | # --- Early repayment with 83.07 EUR on 20 Jan --- When Admin sets the business date to "20 January 2024" @@ -4665,20 +4891,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 | + | 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 | 17.01 | 0.0 | 0.0 | 0.0 | 82.99 | false | false | | 20 January 2024 | Repayment | 83.33 | 82.99 | 0.34 | 0.0 | 0.0 | 0.0 | false | false | - | 20 January 2024 | Accrual | 0.12 | 0.0 | 0.12 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.1 | 0.0 | 0.1 | 0.0 | 0.0 | 0.0 | false | false | When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @TestRailId:C3297 @@ -4748,7 +4975,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 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | - | 18 January 2024 | Accrual | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.31 | 0.0 | 0.31 | 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 | @TestRailId:C3298 @@ -4818,7 +5045,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 January 2024 | Repayment | 17.0 | 16.75 | 0.25 | 0.0 | 0.0 | 83.25 | false | false | - | 18 January 2024 | Accrual | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.31 | 0.0 | 0.31 | 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 | @TestRailId:C3311 @@ -4889,8 +5116,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 | | 15 January 2024 | Repayment | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 82.99 | false | false | - | 18 January 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | - | 19 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.31 | 0.0 | 0.31 | 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 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 @TestRailId:C3299 @@ -4961,8 +5188,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 January 2024 | Repayment | 17.01 | 16.75 | 0.26 | 0.0 | 0.0 | 83.25 | false | false | - | 01 February 2024 | Accrual | 0.26 | 0.0 | 0.26 | 0.0 | 0.0 | 0.0 | false | false | - | 14 February 2024 | Accrual | 0.29 | 0.0 | 0.29 | 0.0 | 0.0 | 0.0 | false | false | + | 14 February 2024 | Accrual | 0.75 | 0.0 | 0.75 | 0.0 | 0.0 | 0.0 | false | false | | 15 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | @TestRailId:C3301 @@ -5002,6 +5228,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 2021 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 10 January 2021 | Repayment | 85.63 | 85.63 | 0.0 | 0.0 | 0.0 | 914.37 | false | false | + | 22 January 2021 | Accrual | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false | | 22 January 2021 | Merchant Issued Refund | 1000.0 | 914.37 | 5.42 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Interest Refund | 5.42 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | @@ -5058,7 +5285,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 2021 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 10 January 2021 | Repayment | 85.63 | 85.63 | 0.0 | 0.0 | 0.0 | 914.37 | true | false | - | 22 January 2021 | Accrual | 5.42 | 0.0 | 5.42 | 0.0 | 0.0 | 0.0 | false | false | | 22 January 2021 | Merchant Issued Refund | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Interest Refund | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | true | | 23 January 2021 | Credit Balance Refund | 85.63 | 85.63 | 0.0 | 0.0 | 0.0 | 85.63 | false | true | @@ -5090,6 +5316,8 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 22 January 2021 | Interest Refund | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false | | 22 January 2021 | Accrual | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false | When Customer undo "1"th "Merchant Issued Refund" transaction made on "22 January 2021" + When Admin sets the business date to "23 January 2021" + When Admin runs inline COB job for Loan Then Loan Repayment schedule has 1 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 2021 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | 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 e07a9f224..7aa33418d 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 20b8eb89c..5bf616d7d 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 | + | 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 | # --- 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 | + | 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 | @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,175 @@ 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 | + + @TestRailId:C3274 + Scenario: Verify accrual activity for repayment reversal on the progressive loan + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE | 01 January 2024 | 1000 | 26 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + When Admin sets the business date to "14 January 2024" + And 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 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "15 January 2024" + And 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 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + When Customer makes "AUTOPAY" repayment on "15 January 2024" with 263.69 EUR transaction amount + And 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 | 31 | 01 February 2024 | 15 January 2024 | 746.1 | 253.9 | 9.79 | 0.0 | 0.0 | 263.69 | 263.69 | 263.69 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | | 507.44 | 238.66 | 25.03 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 254.74 | 252.7 | 10.99 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 254.74 | 5.52 | 0.0 | 0.0 | 260.26 | 0.0 | 0.0 | 0.0 | 260.26 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 51.33 | 0.0 | 0.0 | 1051.33 | 263.69 | 263.69 | 0.0 | 787.64 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | false | false | + When Admin sets the business date to "21 January 2024" + And 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 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | false | false | + | 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false | + When Customer undo "1"th repayment on "15 January 2024" + 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 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false | + | 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "23 January 2024" + And 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 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false | + | 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false | + | 21 January 2024 | Accrual | 1.58 | 0.0 | 1.58 | 0.0 | 0.0 | 0.0 | false | false | + | 22 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "28 January 2024" + And 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 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 | + | 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 | + 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 | + | 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false | + | 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false | + | 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false | + | 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false | + | 21 January 2024 | Accrual | 1.58 | 0.0 | 1.58 | 0.0 | 0.0 | 0.0 | false | false | + | 22 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 23 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 24 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 25 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 26 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false | + | 27 January 2024 | Accrual | 0.69 | 0.0 | 0.69 | 0.0 | 0.0 | 0.0 | false | false | \ No newline at end of file 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 71a8415ab..537142ad0 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -881,7 +881,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 +922,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 | @@ -938,9 +939,9 @@ 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 | - | 25 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 | + | 20 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 | + | 24 April 2024 | Accrual | 1.05 | 0.0 | 1.05 | 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 09581c360..87596c48f 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature @@ -926,8 +926,8 @@ 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 | + | 10 February 2023 | Accrual | 20.0 | 0.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" When Customer undo "1"th "Repayment" transaction made on "10 February 2023" @@ -942,8 +942,8 @@ 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 | + | 10 February 2023 | Accrual | 20.0 | 0.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,8 +973,8 @@ 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 | + | 10 February 2023 | Accrual | 20.0 | 0.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" When Customer undo "1"th "Repayment" transaction made on "10 February 2023" @@ -989,8 +989,8 @@ 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 | + | 10 February 2023 | Accrual | 20.0 | 0.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 686ef8d90..ee224ea57 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 | @@ -2967,7 +2959,6 @@ Feature: LoanChargeback | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 19 January 2024 | Merchant Issued Refund | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 90.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 90.0 | 0.0 | 0.0 | 5.0 | 0.0 | 10.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 15 April 2024 | Chargeback | 7.0 | 2.0 | 0.0 | 0.0 | 5.0 | 0.0 | 7.0 | | 16 April 2024 | Chargeback | 7.0 | 7.0 | 0.0 | 0.0 | 0.0 | 4.0 | 3.0 | Then Loan Transactions tab has a "CHARGEBACK" transaction with date "16 April 2024" which has the following Journal entries: @@ -3041,7 +3032,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 +3042,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 +3065,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 +3076,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 +3137,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 +3154,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 +3165,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 +3193,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 +3205,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 | @@ -3370,7 +3353,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 | | 19 January 2024 | Merchant Issued Refund | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 90.0 | 0.0 | - | 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | | 01 February 2024 | Repayment | 105.0 | 90.0 | 0.0 | 0.0 | 5.0 | 0.0 | 10.0 | | 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 3.0 | | 20 April 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 3.0 | 7.0 | @@ -3382,10 +3364,6 @@ Feature: LoanChargeback | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | | 10.0 | | LIABILITY | 145023 | Suspense/Clearing account | 10.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 "REPAYMENT" transaction with date "01 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | | 90.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 78fb7bfd3..4e206c9a8 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 | @@ -1281,6 +1281,7 @@ Feature: LoanRepayment | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 10 January 2023 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | 0.0 | 500.0 | | 17 January 2023 | Repayment | 550.0 | 500.0 | 0.0 | 0.0 | 50.0 | 0.0 | + | 17 January 2023 | Accrual | 50.0 | 0.0 | 0.0 | 0.0 | 50.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 | 20 January 2023 | Flat | 50.0 | 50.0 | 0.0 | 0.0 | @@ -1887,15 +1888,15 @@ 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 | | 04 January 2023 | Repayment | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 800.0 | - | 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 500.0 | + | 12 January 2023 | Repayment | 300.0 | 290.0 | 0.0 | 10.0 | 0.0 | 510.0 | And Customer makes "AUTOPAY" repayment on "12 January 2023" with 510 EUR transaction amount 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 | | 04 January 2023 | Repayment | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 800.0 | - | 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 500.0 | + | 12 January 2023 | Repayment | 300.0 | 290.0 | 0.0 | 10.0 | 0.0 | 510.0 | | 12 January 2023 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | - | 12 January 2023 | Repayment | 510.0 | 500.0 | 0.0 | 10.0 | 0.0 | 0.0 | + | 12 January 2023 | Repayment | 510.0 | 510.0 | 0.0 | 0.0 | 0.0 | 0.0 | @TestRailId:C2694 @PaymentStrategyDueInAdvancePenaltyInterestPrincipalFee Scenario: Verify the due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee-strategy payment strategy: UC1 - no fees or penalties, due payment @@ -2358,8 +2359,8 @@ 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 | + | 10 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 | | NSF fee | true | Specified due date | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2472,8 +2473,8 @@ 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 | + | 10 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 | | NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2563,8 +2564,8 @@ 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 | + | 10 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 | | NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2601,10 +2602,10 @@ 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 | 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 | + | 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 28 January 2023 | Repayment | 1040.0 | 1000.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 | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | @@ -2750,8 +2751,8 @@ Feature: LoanRepayment | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 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 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.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 | @@ -2770,8 +2771,8 @@ Feature: LoanRepayment | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 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 | + | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.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 @@ -2780,8 +2781,8 @@ Feature: LoanRepayment | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 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 | + | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.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 | @@ -2791,8 +2792,8 @@ Feature: LoanRepayment | 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 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 | + | 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.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 | @@ -2830,15 +2831,15 @@ 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 | | 04 January 2023 | Repayment | 200.0 | 190.0 | 10.0 | 0.0 | 0.0 | 810.0 | - | 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 510.0 | + | 12 January 2023 | Repayment | 300.0 | 289.9 | 0.0 | 10.1 | 0.0 | 520.1 | And Customer makes "AUTOPAY" repayment on "12 January 2023" with 520.10 EUR transaction amount 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 | | 04 January 2023 | Repayment | 200.0 | 190.0 | 10.0 | 0.0 | 0.0 | 810.0 | - | 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 510.0 | + | 12 January 2023 | Repayment | 300.0 | 289.9 | 0.0 | 10.1 | 0.0 | 520.1 | + | 12 January 2023 | Repayment | 520.1 | 520.1 | 0.0 | 0.0 | 0.0 | 0.0 | | 12 January 2023 | Accrual | 20.1 | 0.0 | 10.0 | 10.1 | 0.0 | 0.0 | - | 12 January 2023 | Repayment | 520.1 | 510.0 | 0.0 | 10.1 | 0.0 | 0.0 | @TestRailId:C2810 Scenario: As a user I would like to adjust an existing repayment and validate the event 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 efe3d7214..7674a7777 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 class ExternalAssetOwnerLoanStatusChangePlatformServiceImpl implements Ex 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 0dc2718c2..78c68a2ae 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 000000000..73cb36192 --- /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 000000000..d3109bd7e --- /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 000000000..7c39384eb --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualPeriodData.java @@ -0,0 +1,88 @@ +/** + * 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 Money transactionAccrued; + 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 000000000..b4bdf67e2 --- /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 5ed0085b5..4c9a4c5bd 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 @@ package org.apache.fineract.portfolio.loanaccount.data; 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 class LoanChargeData { 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 class LoanChargeData { 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 @@ public class LoanChargeData { 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 class 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 b3e34c744..82b95d667 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 class LoanTransactionEnumData { 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 06d2251e0..ccb056d82 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 class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi 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 d607f2764..be4c52a47 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { * 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 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { * 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { // 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 class Loan extends AbstractAuditableWithUTCDateTimeCustom { * 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,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanRepaymentScheduleInstallment(installment); } } - updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); } @@ -1310,7 +1302,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); - } private LoanRepaymentScheduleInstallment findByInstallmentNumber(Collection installments, @@ -1347,8 +1338,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +1913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) { - repaymentPeriod.updateObligationsMet(loanCurrency(), actualDisbursementDate); + repaymentPeriod.updateObligationsMet(getCurrency(), actualDisbursementDate); } } @@ -2002,7 +1992,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { * 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 +2211,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (charge.isOverdueInstallmentCharge()) { charge.setActive(false); } else { - charge.resetToOriginal(loanCurrency()); + charge.resetToOriginal(getCurrency()); } } List installments = getRepaymentScheduleInstallments(); @@ -2354,7 +2344,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanTransaction.updateLoan(this); - if (loanTransaction.isNotZero(loanCurrency())) { + if (loanTransaction.isNotZero()) { addLoanTransaction(loanTransaction); } updateLoanSummaryDerivedFields(); @@ -2371,7 +2361,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 +2370,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction); - if (loanTransaction.isNotZero(loanCurrency())) { + if (loanTransaction.isNotZero()) { addLoanTransaction(loanTransaction); } @@ -2400,12 +2390,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +2440,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { && !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()); + changedTransactionDetail = reprocessTransactions(); } updateLoanSummaryDerivedFields(); @@ -2531,7 +2511,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { // 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 +2625,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LocalDate lastTransactionDate = null; for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepaymentLikeType() && transaction.isNonZero()) { + if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) { lastTransactionDate = transaction.getTransactionDate(); } } @@ -2669,7 +2649,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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(); @@ -2692,16 +2672,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanTransaction writeOffTransaction = findWriteOffTransaction(); writeOffTransaction.reverse(); loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, this); - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); - 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 +2689,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +2730,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +2789,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(), + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); updateLoanSummaryDerivedFields(); @@ -2844,14 +2811,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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(); + changedTransactionDetail = reprocessTransactions(); LocalDate lastLoanTransactionDate = getLatestTransactionDate(); doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); } @@ -2904,7 +2864,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +2883,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(), + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); updateLoanSummaryDerivedFields(); @@ -3123,11 +3083,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 +3095,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 +3108,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +3129,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +3475,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { && !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 +3801,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +4090,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +4168,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } else { regenerateRepaymentSchedule(generatorDTO); } - return processTransactions(); + return reprocessTransactions(); } @@ -4229,33 +4178,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 +4346,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 +4462,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +4630,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement; } - public Collection getLoanCharges(LocalDate dueDate) { - Collection loanCharges = new ArrayList<>(); + public List getLoanChargesByDueDate(LocalDate dueDate) { + return getLoanCharges( + loanCharge -> loanCharge.getDueLocalDate() != null && DateUtils.isEqual(loanCharge.getDueLocalDate(), dueDate)); + } - for (LoanCharge loanCharge : charges) { - - if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) { - loanCharges.add(loanCharge); - } - } - - 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 +4736,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +4768,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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); - } - + changedTransactionDetail = reprocessTransactions(); } updateLoanSummaryDerivedFields(); @@ -4869,7 +4803,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +4913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 +5145,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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()); @@ -5461,8 +5395,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public LoanTransaction getLastUserTransaction() { return getLoanTransactions().stream() // - .filter(LoanTransaction::isNotReversed) // - .filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) // + .filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) // .reduce((first, second) -> second) // .orElse(null); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 9e3dcfdb1..9058b7f77 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.OneToMany; 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.List; 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.LoanChargeData; 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { } - 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 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { } 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 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { } 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { } public BigDecimal amountOrPercentage() { - return this.amountOrPercentage; + return this.amountOrPercentage; // TODO delete duplicated method } public BigDecimal chargeAmount() { @@ -906,12 +881,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { } - 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { this.waived = false; } - public ExternalId getExternalId() { - return externalId; - } - public ChargeTimeType getChargeTimeType() { return ChargeTimeType.fromInt(this.chargeTime); } @@ -1013,6 +972,11 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { .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 454e524e0..7823850ea 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 @@ -188,7 +188,7 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom< } 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 51a3dacbd..00e2687e7 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 java.util.Set; 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 class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa 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 643c114f1..c14074199 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 class LoanRepaymentScheduleProcessingWrapper { : 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 07263eeb7..0ddebb1a6 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 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @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 959d71eda..3c0f7526e 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 class LoanRepositoryWrapper { 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 98b5166c8..074693e52 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 class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom 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; + return isGreaterThanZero() && !MathUtil.isGreaterThan(amount, totalOverpaid); } - public boolean isNotZero(final MonetaryCurrency currency) { - return !getAmount(currency).isZero(); - } - - 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 class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom 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 2af4b1d90..967f24a2f 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 enum LoanTransactionType { 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 enum LoanTransactionType { 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 e31f6c113..d12449b89 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 class SingleLoanChargeRepaymentScheduleProcessingWrapper { 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 class SingleLoanChargeRepaymentScheduleProcessingWrapper { 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 class SingleLoanChargeRepaymentScheduleProcessingWrapper { 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 3b40c7cb3..059f93e58 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 abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen // 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 abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen // 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 916ebfef6..3f2b4907e 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 abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch .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 9cb841f5f..33666a797 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 @@ public interface LoanScheduleGenerator { 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 cf32cbf76..91addc4f6 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 reprocessExistingAccruals(@NotNull Loan loan); + + void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled); + + void processIncomePostingAndAccruals(@NotNull Loan loan); void addIncomeAndAccrualTransactions(Long loanId) throws Exception; - void reprocessExistingAccruals(Loan loan); + void processAccrualsOnLoanClosure(@NotNull Loan loan); - void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled); + void processAccrualsOnLoanReopen(@NotNull Loan loan); - void processIncomePostingAndAccruals(Loan loan); - - void processAccrualsForLoanClosure(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 000000000..d8c12da83 --- /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 fb2588fb4..30e62a15d 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 4db9c5cdc..acf6abb04 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 final class LoanEnumerations { 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-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index 098e59db5..49f9f17fc 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -46,4 +46,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1024_accrual_adjustment_transaction_type.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1024_accrual_adjustment_transaction_type.xml new file mode 100644 index 000000000..a1dadc5d9 --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1024_accrual_adjustment_transaction_type.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + 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 8c180124c..b88f6b3cb 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.LoanCreditAllocationRule 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.FutureInstallmentAllocat 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 class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // 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 class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep }); } - @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 class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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); + 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); + // 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()); + // 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)); + 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; + 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; - // 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; - } + // 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); } - - // 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); - } - } - allocateOverpayment(loanTransaction, ctx); - } else { - throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); + 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 { + 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep processTransaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings); if (processTransaction.isInterestWaiver()) { - processTransaction.adjustInterestComponent(currency); + processTransaction.adjustInterestComponent(); } if (isNew) { processTransaction = checkRegisteredNewTransaction(transaction, ctx); @@ -810,51 +827,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return changeOperations; } - private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx; - if (progressiveTransactionCtx.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()); - 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); - } - - Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount); - - emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(), - amortizableAmount); - - 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())) - .findFirst().orElse(null); - if (installment != null) { - installment.updatePrincipal(rm.getDuePrincipal().getAmount()); - installment.updateInterestCharged(rm.getDueInterest().getAmount()); - installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate()); - } - }); - } - - 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()) { @@ -864,43 +836,90 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - disbursementTransaction.resetDerivedComponents(); + private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { + ProgressiveLoanInterestScheduleModel model; + if (!(transactionCtx instanceof ProgressiveTransactionCtx) + || (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) { + throw new IllegalStateException("TransactionCtx has no model"); + } final MathContext mc = MoneyHelper.getMathContext(); - List candidateRepaymentInstallments = transactionCtx.getInstallments().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()); + 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 = transactionCtx.getInstallments().stream() - .filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.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); + 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(currency).minus(downPaymentAmount); + emiCalculator.addDisbursement(model, transactionDate, amortizableAmount); + + disbursementTransaction.resetDerivedComponents(); + if (amortizableAmount.isGreaterThanZero()) { + 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(currency, transactionDate); + } + }); + } + allocateOverpayment(disbursementTransaction, transactionCtx); + } + + private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { + disbursementTransaction.resetDerivedComponents(); + final MathContext mc = MoneyHelper.getMathContext(); + 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(currency); + if (loanProductRelatedDetail.isEnableDownPayment()) { + 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(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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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); } } } @@ -1078,15 +1099,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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); @@ -1109,7 +1130,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, loanTransaction.getLoan().loanCurrency()); + currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return portion; } @@ -1130,13 +1151,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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())); } } @@ -1210,11 +1232,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - 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, @@ -1284,13 +1308,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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()) { @@ -1401,7 +1428,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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, @@ -1413,7 +1440,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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()) { @@ -1421,8 +1447,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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; } @@ -1446,7 +1472,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, List transactionMappings, Set charges, Balances balances) { - if (transactionAmountUnprocessed.isZero()) { + if (MathUtil.isEmpty(transactionAmountUnprocessed)) { return transactionAmountUnprocessed; } @@ -1589,7 +1615,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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, @@ -1597,17 +1623,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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) { @@ -1619,8 +1644,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } 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()); @@ -1637,6 +1662,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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() // @@ -1655,10 +1681,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .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(); @@ -1823,6 +1850,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep 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 1b7c10f30..c950bc687 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 class InterestPeriod implements Comparable { } 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 class InterestPeriod implements Comparable { 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 class InterestPeriod implements Comparable { } } - 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 ad7acca6f..0dec32dcd 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 class ProgressiveLoanInterestScheduleModel { 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 class ProgressiveLoanInterestScheduleModel { 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 class ProgressiveLoanInterestScheduleModel { return repaymentPeriods; } return repaymentPeriods.stream()// - .filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))// + .filter(period -> !DateUtils.isBefore(period.getDueDate(), calculateFromRepaymentPeriodDueDate))// .toList();// } @@ -120,6 +123,10 @@ public class ProgressiveLoanInterestScheduleModel { 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 @@ public class ProgressiveLoanInterestScheduleModel { 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 class ProgressiveLoanInterestScheduleModel { 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 6a531a902..5ecd47ca6 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 @@ public class RepaymentPeriod { 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 @@ public class RepaymentPeriod { 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 class RepaymentPeriod { 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 class RepaymentPeriod { (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 c11ee1fa9..82bef12ab 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.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.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 class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { // 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 class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { 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 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { } } - @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 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { } return interestCharges; } - } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index f701cdfca..10f971448 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanproduct.calc; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -34,11 +35,14 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail public interface EMICalculator { - ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List 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 @@ public interface EMICalculator { 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 87a6e8c06..25205779c 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.ProgressiveLo 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 final class ProgressiveEMICalculator implements EMICalculator { if (scheduleModel == null) { return Optional.empty(); } - return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate); + return scheduleModel.findRepaymentPeriodByDueDate(repaymentPeriodDueDate); } /** @@ -139,13 +156,14 @@ public final class ProgressiveEMICalculator implements EMICalculator { @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 final class ProgressiveEMICalculator implements EMICalculator { } @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 final class ProgressiveEMICalculator implements EMICalculator { } @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 final class ProgressiveEMICalculator implements EMICalculator { 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 final class ProgressiveEMICalculator implements EMICalculator { } @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 @@ public final class ProgressiveEMICalculator implements EMICalculator { 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 @@ public final class ProgressiveEMICalculator implements EMICalculator { 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 @@ public final class ProgressiveEMICalculator implements EMICalculator { 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 cb1c285fd..2d23c01e0 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.LoanChargePaidBy; 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { @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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 @@ class AdvancedPaymentScheduleTransactionProcessorTest { 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 8c0686acf..a49824b7b 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 @@ class ProgressiveEMICalculatorTest { @Test public void test_generateInterestScheduleModel() { - final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -169,8 +168,8 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { */ @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { @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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 @@ class ProgressiveEMICalculatorTest { 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 b9209d3ca..0a79bcacf 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 @@ -36,6 +36,7 @@ import org.apache.fineract.accounting.journalentry.data.LoanDTO; 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 @@ -51,69 +52,66 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess 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); } } @@ -1144,7 +1142,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess * @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(); @@ -1153,10 +1150,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType(); final BigDecimal interestAmount = loanTransactionDTO.getInterest(); final BigDecimal feesAmount = loanTransactionDTO.getFees(); final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); - final boolean isReversed = loanTransactionDTO.isReversed(); + final boolean isReversed = transactionType.isAccrualAdjustment() != loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); // create journal entries for recognizing interest (or reversal) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 35fb7e5e4..9298daed6 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 class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService } @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 3318a4d04..379366f92 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 jakarta.ws.rs.core.UriInfo; 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 @@ public class LoanChargesApiResource { 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 @@ public class LoanChargesApiResource { 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 abe81fc1f..cb4254bc9 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 class LoanAccountDomainServiceJpa implements LoanAccountDomainService { 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 class LoanAccountDomainServiceJpa implements LoanAccountDomainService { 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 208f33532..72dc168a0 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 24e0befbf..38371fb53 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 class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche } 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 bfe83b9e1..34996041e 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 @@ public final class LoanApplicationValidator { "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 final class LoanApplicationValidator { "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 9a7308751..d2d40343c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -18,68 +18,66 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrualAdjustment; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.loan.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,64 +86,35 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessingService { - private static final String ACCRUAL_ON_CHARGE_DUE_DATE = "due-date"; + private static final Predicate ACCRUAL_PREDICATE = t -> t.isNotReversed() + && (t.isAccrual() || t.isAccrualAdjustment()); + 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); - } - - /** - * method adds accrual for Loan COB business step - */ - @Override - @Transactional - public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate, - loan); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); - } - - 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); - } - } - + public void addPeriodicAccruals(@NotNull LocalDate tillDate) throws JobExecutionException { + List loans = loanRepositoryWrapper.findLoansForPeriodicAccrual(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); + addPeriodicAccruals(tillDate, loan); } 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,72 +123,35 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } + /** + * method adds accrual for Loan COB business step + */ + @Override + @Transactional + public void addPeriodicAccruals(@NotNull LocalDate tillDate, @NotNull Loan loan) { + addAccruals(loan, tillDate, true, false, true); + } + /** * method adds accrual for batch job "Add Accrual Transactions" */ @Override @Transactional - public void addAccrualAccounting(final Long loanId, final 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 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()); - } - - if (DateUtils.isAfter(accrualData.getDueDateAsLocaldate(), tillDate)) { - if (accruedTill == null || firstTime) { - accruedTill = accrualData.getAccruedTill(); - firstTime = false; - } - if (accruedTill == null || DateUtils.isBefore(accruedTill, tillDate)) { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tillDate); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(tillDate, accrualData); - } - } else { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); - accruedTill = accrualData.getDueDateAsLocaldate(); + public void addAccruals(@NotNull LocalDate tillDate) throws JobExecutionException { + List loans = loanRepositoryWrapper.findLoansForAddAccrual(AccountingRuleType.ACCRUAL_PERIODIC.getValue(), tillDate, + !isChargeOnDueDate()); + List errors = new ArrayList<>(); + for (Loan loan : loans) { + try { + setSetHelpers(loan); + addAccruals(loan, tillDate, false, false, true); + } catch (Exception e) { + log.error("Failed to add accrual for loan {}", loan.getId(), e); + errors.add(e); } } - } - - @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 (!errors.isEmpty()) { + throw new JobExecutionException(errors); } } @@ -228,13 +160,16 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing * reschedule */ @Override - public void reprocessExistingAccruals(Loan loan) { - Collection accruals = retrieveListOfAccrualTransactions(loan); - if (!accruals.isEmpty()) { + public void reprocessExistingAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; + } + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + if (!accrualTransactions.isEmpty()) { if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - reprocessPeriodicAccruals(loan, accruals); + reprocessPeriodicAccruals(loan, accrualTransactions); } else if (loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - reprocessNonPeriodicAccruals(loan, accruals); + reprocessNonPeriodicAccruals(loan, accrualTransactions); } } } @@ -244,78 +179,123 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing */ @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()) { + public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) { + if (isProgressiveAccrual(loan)) { 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); - } + 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); } - } /** * 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()); + public void processIncomePostingAndAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; } + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails == null || !recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + LocalDate lastCompoundingDate = loan.getDisbursementDate(); + List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); + List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { + if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { + break; + } + 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); + reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate()); + reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate()); + } + + @Transactional + @Override + public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException { + if (loanId == null) { + return; + } + Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + if (isProgressiveAccrual(loan)) { + return; + } + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + processIncomePostingAndAccruals(loan); + this.loanRepositoryWrapper.saveAndFlush(loan); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); } /** * method calculates accruals for loan on loan closure */ @Override - public void processAccrualsForLoanClosure(Loan loan) { + public void processAccrualsOnLoanClosure(@NotNull Loan loan) { // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income - processAccrualTransactionsOnLoanClosure(loan); - + addAccruals(loan, loan.getLastLoanRepaymentScheduleInstallment().getDueDate(), false, true, false); + if (isProgressiveAccrual(loan)) { + return; + } // check and process accruals for loan WITH interest recalculation details and compounding posted as income processIncomeAndAccrualTransactionOnLoanClosure(loan); } + @Override + public void processAccrualsOnLoanReopen(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + List accruals = retrieveListOfAccrualTransactions(loan); + LocalDate accruedTill = loan.getAccruedTill(); + // if accruedTill is null, the all the accrual transactions will be reversed here + if (reverseTransactionsAfter(accruals, accruedTill)) { + return; + } + LoanTransaction closureAccrual = accruals.stream() + .filter(t -> !t.isReversed() && t.isAccrual() && DateUtils.isEqual(t.getTransactionDate(), accruedTill)) + .sorted(LoanTransactionComparator.INSTANCE).reduce((first, second) -> second).orElse(null); + if (closureAccrual == null) { + return; + } + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment( + i -> isInPeriod(closureAccrual.getDateOf(), i, i.getInstallmentNumber().equals(firstInstallmentNumber))); + if (MathUtil.isGreaterThan(closureAccrual.getInterestPortion(), installment.getInterestCharged()) + || MathUtil.isGreaterThan(closureAccrual.getFeeChargesPortion(), installment.getFeeChargesCharged()) + || MathUtil.isGreaterThan(closureAccrual.getPenaltyChargesPortion(), installment.getPenaltyCharges())) { + reverseAccrual(closureAccrual); + } + } + } + /** * method calculates accruals for loan on loan fore closure */ @Override - public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions) { + 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); + reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate); HashMap incomeDetails = new HashMap<>(); @@ -333,558 +313,421 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private void calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(final LocalDate tillDate, - final LoanScheduleAccrualData accrualData) { + // PeriodicAccruals - 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; + 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; } - interestPortion = interestPortion.subtract(totalAccInterest); - amount = amount.add(interestPortion); - totalAccInterest = totalAccInterest.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + List existingAccruals = retrieveListOfAccrualTransactions(loan); + reverseTransactionsAfter(existingAccruals, loan.getLastLoanRepaymentScheduleInstallment().getDueDate()); + ensureAccrualTransactionMappings(loan); + + boolean progressiveAccrual = isProgressiveAccrual(loan); + LocalDate accruedTill = loan.getAccruedTill(); + if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill) + && existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), tillDate))) { + return; } - // fee - if (feePortion != null) { - if (totalAccFee == null) { - totalAccFee = BigDecimal.ZERO; + AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic); + + LocalDate accrualDate = isFinal ? (progressiveAccrual ? DateUtils.getBusinessLocalDate() : getFinalAccrualTransactionDate(loan)) + : tillDate; + boolean mergeTransactions = isFinal || progressiveAccrual; + MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); + List accrualTransactions = new ArrayList<>(); + Money totalInterestPortion = null; + 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 (totalCreditedFee == null) { - totalCreditedFee = BigDecimal.ZERO; + if (mergeTransactions) { + totalInterestPortion = MathUtil.plus(totalInterestPortion, interestPortion); + if (progressiveAccrual) { + Money feeAdjustmentPortion = MathUtil.negate(feePortion); + Money penaltyAdjustmentPortion = MathUtil.negate(penaltyPortion); + mergeAdjustTransaction = createOrMergeAccrualTransaction(loan, mergeAdjustTransaction, accrualDate, period, + accrualTransactions, null, feeAdjustmentPortion, penaltyAdjustmentPortion, true); + } + mergeAccrualTransaction = createOrMergeAccrualTransaction(loan, mergeAccrualTransaction, accrualDate, period, + accrualTransactions, null, feePortion, penaltyPortion, false); + } else { + LocalDate dueDate = period.getDueDate(); + if (!isFinal && DateUtils.isAfter(dueDate, tillDate) && DateUtils.isBefore(tillDate, accruedTill)) { + continue; + } + LocalDate periodAccrualDate = DateUtils.isBefore(dueDate, accrualDate) ? dueDate : accrualDate; + LoanTransaction accrualTransaction = addAccrualTransaction(loan, periodAccrualDate, period, interestPortion, feePortion, + penaltyPortion, false); + if (accrualTransaction != null) { + accrualTransactions.add(accrualTransaction); + } } - feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee); - amount = amount.add(feePortion); - totalAccFee = totalAccFee.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(period.getInstallmentNumber()); + installment.updateAccrualPortion(interestAccruable, feeAccruable, penaltyAccruable); + } + if (mergeTransactions && !MathUtil.isEmpty(totalInterestPortion)) { + if (progressiveAccrual) { + Money interestAdjustmentPortion = MathUtil.negate(totalInterestPortion); + createOrMergeAccrualTransaction(loan, mergeAdjustTransaction, accrualDate, null, accrualTransactions, + interestAdjustmentPortion, null, null, true); } + createOrMergeAccrualTransaction(loan, mergeAccrualTransaction, accrualDate, null, accrualTransactions, totalInterestPortion, + null, null, false); + } + if (accrualTransactions.isEmpty()) { + return; } - // 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 (!isFinal) { + loan.setAccruedTill(tillDate); + } + 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 (amount.compareTo(BigDecimal.ZERO) > 0) { - addAccrualAccounting(accrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion, - totalAccPenalty, tillDate); + if (addJournal) { + Map accountingBridgeData = deriveAccountingBridgeData(loan, newTransactionMapping); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } } - 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(); - } + 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; + } - 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; - } + @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)); + } + + private void addInterestAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, LoanScheduleGenerator scheduleGenerator, + @NotNull LoanRepaymentScheduleInstallment installment, @NotNull AccrualPeriodsData accrualPeriods) { + if (installment.isAdditional() || installment.isReAged()) { + return; } - int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, tillDate); - double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays; - - if (daysToBeAccrued >= totalNumberOfDays) { - interestPortion = accrualData.getAccruableIncome(); + AccrualPeriodData period = accrualPeriods.getPeriodByInstallmentNumber(installment.getInstallmentNumber()); + MonetaryCurrency currency = accrualPeriods.getCurrency(); + Money interest = null; + boolean isFullPeriod = isFullPeriod(tillDate, installment); + boolean isInPeriod = isInPeriod(tillDate, installment, false); + if (isFullPeriod) { + interest = installment.getInterestCharged(currency); } else { - interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + if (isInPeriod) { // first period first day is not accrued + interest = scheduleGenerator.getPeriodInterestTillDate(installment, tillDate); + } } - interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode()); - return interestPortion; + 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 transactionAccrued = MathUtil.toMoney(calcInterestAccruedAmount(installment, accrualPeriods, tillDate), currency); + period.setTransactionAccrued(transactionAccrued); + Money accrued = MathUtil.minusToZero(transactionAccrued, transactionWaived); + period.setInterestAccrued(accrued); } - private void calculateFinalAccrualsForScheduleAndAddAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { + @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); + } - BigDecimal amount = BigDecimal.ZERO; - BigDecimal interestPortion = null; - BigDecimal totalAccInterest = null; + @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); + } - // interest - if (scheduleAccrualData.getAccruableIncome() != null) { - interestPortion = scheduleAccrualData.getAccruableIncome(); - totalAccInterest = interestPortion; - if (scheduleAccrualData.getAccruedInterestIncome() != null) { - interestPortion = interestPortion.subtract(scheduleAccrualData.getAccruedInterestIncome()); - } - amount = amount.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; - } - } - - // fee - BigDecimal feePortion = null; - BigDecimal totalAccFee = null; - if (scheduleAccrualData.getDueDateFeeIncome() != null) { - feePortion = scheduleAccrualData.getDueDateFeeIncome(); - totalAccFee = feePortion; - if (scheduleAccrualData.getAccruedFeeIncome() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome()); - } - if (scheduleAccrualData.getCreditedFee() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee()); - } - amount = amount.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; - } - } - - // penalty - BigDecimal penaltyPortion = null; - BigDecimal totalAccPenalty = null; - if (scheduleAccrualData.getDueDatePenaltyIncome() != null) { - penaltyPortion = scheduleAccrualData.getDueDatePenaltyIncome(); - totalAccPenalty = penaltyPortion; - if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); - } - if (scheduleAccrualData.getCreditedPenalty() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty()); - } - amount = amount.add(penaltyPortion); - if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { - penaltyPortion = null; - } - } - - if (amount.compareTo(BigDecimal.ZERO) > 0) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate()); - } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate()); - } + @NotNull + private BigDecimal calcInterestAccruedAmount(@NotNull LoanRepaymentScheduleInstallment installment, + @NotNull AccrualPeriodsData accrualPeriods, @NotNull LocalDate tillDate) { + Loan loan = installment.getLoan(); + if (isProgressiveAccrual(loan)) { + BigDecimal totalAccrued = loan.getLoanTransactions().stream().filter(ACCRUAL_PREDICATE) + .map(t -> t.isAccrual() ? t.getInterestPortion() : MathUtil.negate(t.getInterestPortion())) + .reduce(BigDecimal.ZERO, MathUtil::add); + BigDecimal prevAccrued = accrualPeriods.getPeriods().stream() + .filter(p -> p.getInstallmentNumber() < installment.getInstallmentNumber()) + .map(p -> MathUtil.toBigDecimal(p.getTransactionAccrued())).reduce(BigDecimal.ZERO, MathUtil::add); + BigDecimal accrued = MathUtil.subtractToZero(totalAccrued, prevAccrued); + // if this is the current-last period, all the remaining accrued amount is added + return isInPeriod(tillDate, installment, false) ? accrued : MathUtil.min(installment.getInterestCharged(), accrued, false); + } else { + return isFullPeriod(tillDate, installment) ? installment.getInterestAccrued() + : loan.getLoanTransactions().stream() + .filter(t -> !t.isReversed() && t.isAccrual() && isInPeriod(t.getTransactionDate(), installment, false)) + .map(LoanTransaction::getInterestPortion).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 { - - AppUser user = context.authenticatedUser(); - Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId()); - Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId()); - MonetaryCurrency currency = loan.getCurrency(); - - // create accrual Transaction - LoanTransaction loanTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, - externalIdFactory.create()); - - // update charges paid by - Map applicableCharges = scheduleAccrualData.getApplicableCharges(); - - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - // - LoanCharge loanCharge = loanChargeRepository.getReferenceById(chargeData.getId()); - loanTransaction.getLoanChargesPaid() - .add(new LoanChargePaidBy(loanTransaction, loanCharge, entry.getValue(), scheduleAccrualData.getInstallmentNumber())); - + 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); } + } - loanTransactionRepository.saveAndFlush(loanTransaction); - loan.addLoanTransaction(loanTransaction); + 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); - Map transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion, - scheduleAccrualData, accruedTill); + 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)); + + Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency); + Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived); + chargeData.setChargeAccrued(MathUtil.minusToZero(MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency), transactionWaived)); + + duePeriod.addCharge(chargeData); + } + + @NotNull + private BigDecimal calcChargeWaivedAmount(@NotNull Collection loanChargePaidBy, @NotNull LocalDate tillDate) { + return loanChargePaidBy.stream().filter(pb -> { + LoanTransaction t = pb.getLoanTransaction(); + return !t.isReversed() && t.isWaiveCharge() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + }).map(LoanChargePaidBy::getAmount).reduce(BigDecimal.ZERO, MathUtil::add); + } + + @NotNull + private BigDecimal 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); + } + + @NotNull + private BigDecimal calcChargeAccruedAmount(@NotNull Collection loanChargePaidBy) { + return loanChargePaidBy.stream().filter(pb -> ACCRUAL_PREDICATE.test(pb.getLoanTransaction())) + .map(pb -> pb.getLoanTransaction().isAccrual() ? pb.getAmount() : MathUtil.negate(pb.getAmount())) + .reduce(BigDecimal.ZERO, MathUtil::add); + } + + private boolean isChargeDue(@NotNull LoanCharge loanCharge, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) { + LocalDate fromDate = installment.getFromDate(); + LocalDate dueDate = installment.getDueDate(); + LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; + return chargeOnDueDate ? loanCharge.isDueInPeriod(fromDate, toDate, isFirstPeriod) + : isInPeriod(loanCharge.getSubmittedOnDate(), fromDate, toDate, isFirstPeriod); + } + + private LoanTransaction createOrMergeAccrualTransaction(@NotNull Loan loan, LoanTransaction transaction, LocalDate transactionDate, + AccrualPeriodData accrualPeriod, List accrualTransactions, Money interest, Money fee, Money penalty, + boolean adjustment) { + if (transaction == null) { + transaction = addAccrualTransaction(loan, transactionDate, accrualPeriod, interest, fee, penalty, adjustment); + if (transaction != null) { + accrualTransactions.add(transaction); + } + } else { + mergeAccrualTransaction(transaction, accrualPeriod, interest, fee, penalty, adjustment); + } + return transaction; + } + + private LoanTransaction addAccrualTransaction(@NotNull Loan loan, @NotNull LocalDate transactionDate, AccrualPeriodData accrualPeriod, + Money interestPortion, Money feePortion, Money penaltyPortion, boolean adjustment) { + interestPortion = MathUtil.negativeToZero(interestPortion); + BigDecimal interest = MathUtil.toBigDecimal(interestPortion); + feePortion = MathUtil.negativeToZero(feePortion); + BigDecimal fee = MathUtil.toBigDecimal(feePortion); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + BigDecimal penalty = MathUtil.toBigDecimal(penaltyPortion); + BigDecimal amount = MathUtil.add(interest, fee, penalty); + if (!MathUtil.isGreaterThanZero(amount)) { + return null; + } + LoanTransaction transaction = adjustment + ? accrualAdjustment(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()) + : accrueTransaction(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()); + loan.addLoanTransaction(transaction); // update repayment schedule portions - - 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, accrualPeriod, adjustment); + return transaction; } - private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, - final Map transactionMap) { + private void mergeAccrualTransaction(@NotNull LoanTransaction transaction, AccrualPeriodData accrualPeriod, 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; + } - 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); - - accountingBridgeData.put("newLoanTransactions", newLoanTransactions); - return accountingBridgeData; + transaction.updateComponentsAndTotal(null, interestPortion, feePortion, penaltyPortion); + // update repayment schedule portions + addTransactionMappings(transaction, accrualPeriod, 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<>(); + private void addTransactionMappings(@NotNull LoanTransaction transaction, AccrualPeriodData accrualPeriod, boolean adjustment) { + if (accrualPeriod == null) { + return; + } + Loan loan = transaction.getLoan(); + Integer installmentNumber = accrualPeriod.getInstallmentNumber(); + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(installmentNumber); - final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL); + // add charges paid by mappings + addPaidByMappings(transaction, installment, accrualPeriod, adjustment); + } - 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 accrualPeriod, boolean adjustment) { + Loan loan = installment.getLoan(); + MonetaryCurrency currency = loan.getCurrency(); + for (AccrualChargeData accrualCharge : accrualPeriod.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; } - thisTransactionData.put("loanChargesPaid", loanChargesPaidData); - } - - return thisTransactionData; - } - - private void updateCharges(final Collection chargesData, final LoanScheduleAccrualData accrualData, - final LocalDate startDate, final LocalDate endDate) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { - updateChargeForDueDate(chargesData, accrualData, startDate, endDate); - } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate); - } - - } - - private void updateChargeForSubmittedOnDate(Collection chargesData, LoanScheduleAccrualData accrualData, - LocalDate startDate, LocalDate endDate) { - final Map applicableCharges = new HashMap<>(); - BigDecimal submittedDateFeeIncome = BigDecimal.ZERO; - BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO; - LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate(); - for (LoanChargeData loanCharge : chargesData) { - BigDecimal chargeAmount = BigDecimal.ZERO; - if (isChargeSubmittedDateAndDueDateInRange(accrualData, startDate, endDate, scheduleEndDate, loanCharge)) { - chargeAmount = loanCharge.getAmount(); - chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); - } - if (loanCharge.isPenalty()) { - submittedDatePenaltyIncome = submittedDatePenaltyIncome.add(chargeAmount); - } else { - submittedDateFeeIncome = submittedDateFeeIncome.add(chargeAmount); - } - - } - - if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDateFeeIncome = null; - } - - if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - submittedDatePenaltyIncome = null; - } - - accrualData.updateChargeDetails(applicableCharges, submittedDateFeeIncome, submittedDatePenaltyIncome); - } - - private boolean isChargeSubmittedDateAndDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, - LocalDate scheduleEndDate, LoanChargeData loanCharge) { - return ((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(startDate, loanCharge.getSubmittedOnDate()) - && DateUtils.isEqual(startDate, loanCharge.getDueDate())) || DateUtils.isBefore(startDate, loanCharge.getDueDate())) - && !DateUtils.isBefore(endDate, loanCharge.getSubmittedOnDate()) - && !DateUtils.isBefore(scheduleEndDate, loanCharge.getDueDate()); - } - - private void updateChargeForDueDate(Collection chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, - LocalDate endDate) { - final Map applicableCharges = new HashMap<>(); - BigDecimal dueDateFeeIncome = BigDecimal.ZERO; - BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; - for (LoanChargeData loanCharge : chargesData) { - BigDecimal chargeAmount = BigDecimal.ZERO; - if (loanCharge.getDueDate() == null) { - if (loanCharge.isInstallmentFee() && DateUtils.isEqual(endDate, accrualData.getDueDateAsLocaldate())) { - chargeAmount = calculateInstallmentFeeCharges(accrualData, applicableCharges, loanCharge, chargeAmount); - } - } else if (isChargeDueDateInRange(accrualData, startDate, endDate, loanCharge)) { - chargeAmount = loanCharge.getAmount(); - chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); - } - - if (loanCharge.isPenalty()) { - dueDatePenaltyIncome = dueDatePenaltyIncome.add(chargeAmount); - } else { - dueDateFeeIncome = dueDateFeeIncome.add(chargeAmount); - } - } - - if (dueDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDateFeeIncome = null; - } - - if (dueDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { - dueDatePenaltyIncome = null; - } - - accrualData.updateChargeDetails(applicableCharges, dueDateFeeIncome, dueDatePenaltyIncome); - } - - private boolean isChargeDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, - LoanChargeData loanCharge) { - return ((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(loanCharge.getDueDate(), startDate)) - || DateUtils.isAfter(loanCharge.getDueDate(), startDate)) && !DateUtils.isAfter(loanCharge.getDueDate(), endDate); - } - - private BigDecimal calculateDueDateCharges(Map applicableCharges, LoanChargeData loanCharge, - BigDecimal chargeAmount) { - BigDecimal dueDateChargeAmount = chargeAmount; - if (loanCharge.getAmountUnrecognized() != null) { - dueDateChargeAmount = dueDateChargeAmount.subtract(loanCharge.getAmountUnrecognized()); - } - boolean canAddCharge = dueDateChargeAmount.compareTo(BigDecimal.ZERO) > 0; - if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) { - BigDecimal amountForAccrual = dueDateChargeAmount; - if (loanCharge.getAmountAccrued() != null) { - amountForAccrual = dueDateChargeAmount.subtract(loanCharge.getAmountAccrued()); - } - applicableCharges.put(loanCharge, amountForAccrual); - } - return dueDateChargeAmount; - } - - private BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrualData, - Map applicableCharges, LoanChargeData loanCharge, BigDecimal chargeAmount) { - BigDecimal installmentFeeChargeAmount = chargeAmount; - Collection installmentData = loanCharge.getInstallmentChargeData(); - for (LoanInstallmentChargeData installmentChargeData : installmentData) { - - if (installmentChargeData.getInstallmentNumber().equals(accrualData.getInstallmentNumber())) { - BigDecimal accruableForInstallment = installmentChargeData.getAmount(); - if (installmentChargeData.getAmountUnrecognized() != null) { - accruableForInstallment = accruableForInstallment.subtract(installmentChargeData.getAmountUnrecognized()); - } - installmentFeeChargeAmount = accruableForInstallment; - boolean canAddCharge = installmentFeeChargeAmount.compareTo(BigDecimal.ZERO) > 0; - if (canAddCharge && (installmentChargeData.getAmountAccrued() == null - || installmentFeeChargeAmount.compareTo(installmentChargeData.getAmountAccrued()) != 0)) { - BigDecimal amountForAccrual = installmentFeeChargeAmount; - if (installmentChargeData.getAmountAccrued() != null) { - amountForAccrual = installmentFeeChargeAmount.subtract(installmentChargeData.getAmountAccrued()); - } - applicableCharges.put(loanCharge, amountForAccrual); - BigDecimal amountAccrued = installmentFeeChargeAmount; - if (loanCharge.getAmountAccrued() != null) { - amountAccrued = amountAccrued.add(loanCharge.getAmountAccrued()); - } - loanCharge.updateAmountAccrued(amountAccrued); - } - break; - } - } - return installmentFeeChargeAmount; - } - - private void updateInterestIncome(final LoanScheduleAccrualData accrualData, - final Collection loanWaiverTransactions, - final Collection loanSchedulePeriodDataList, final LocalDate tillDate) { - - BigDecimal interestIncome = BigDecimal.ZERO; - if (accrualData.getInterestIncome() != null) { - interestIncome = accrualData.getInterestIncome(); - } - if (accrualData.getWaivedInterestIncome() != null) { - Collection loanTransactionDatas = new ArrayList<>(); - - getLoanWaiverTransactionsInRange(accrualData, loanWaiverTransactions, tillDate, loanTransactionDatas); - - BigDecimal recognized = getWaivedInterestIncome(accrualData, loanSchedulePeriodDataList, loanTransactionDatas); - - BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); - if (interestWaived.compareTo(recognized) > 0) { - interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); - } - } - - accrualData.updateAccruableIncome(interestIncome); - } - - private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, - Collection loanSchedulePeriodDataList, Collection loanTransactionDatas) { - BigDecimal recognized = BigDecimal.ZERO; - BigDecimal unrecognized = BigDecimal.ZERO; - BigDecimal remainingAmt = BigDecimal.ZERO; - - Iterator iterator = loanTransactionDatas.iterator(); - for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) { - if (MathUtil.isLessThanOrEqualZero(recognized) && MathUtil.isLessThanOrEqualZero(unrecognized) && iterator.hasNext()) { - LoanTransactionData loanTransactionData = iterator.next(); - recognized = recognized.add(loanTransactionData.getInterestPortion()); - unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion()); - } - if (DateUtils.isBefore(loanSchedulePeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) { - remainingAmt = remainingAmt.add(loanSchedulePeriodData.getInterestWaived()); - if (recognized.compareTo(remainingAmt) > 0) { - recognized = recognized.subtract(remainingAmt); - remainingAmt = BigDecimal.ZERO; - } else { - remainingAmt = remainingAmt.subtract(recognized); - recognized = BigDecimal.ZERO; - if (unrecognized.compareTo(remainingAmt) >= 0) { - unrecognized = unrecognized.subtract(remainingAmt); - remainingAmt = BigDecimal.ZERO; - } else if (iterator.hasNext()) { - remainingAmt = remainingAmt.subtract(unrecognized); - unrecognized = BigDecimal.ZERO; - } - } - - } - } - return recognized; - } - - private void getLoanWaiverTransactionsInRange(LoanScheduleAccrualData accrualData, - Collection loanWaiverTransactions, LocalDate tillDate, - Collection loanTransactionDatas) { - for (LoanTransactionData loanTransactionData : loanWaiverTransactions) { - LocalDate transactionDate = loanTransactionData.getDate(); - if (!DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate()) - || (DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate()) - && !DateUtils.isAfter(transactionDate, accrualData.getDueDateAsLocaldate()) - && !DateUtils.isAfter(transactionDate, tillDate))) { - loanTransactionDatas.add(loanTransactionData); + 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); } } } - private void postJournalEntries(final Loan loan, final List existingTransactionIds, - final List existingReversedTransactionIds) { - final MonetaryCurrency currency = loan.getCurrency(); - boolean isAccountTransfer = false; - final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, - existingReversedTransactionIds, isAccountTransfer); - journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + private boolean isFullPeriod(@NotNull LocalDate tillDate, @NotNull LoanRepaymentScheduleInstallment installment) { + return isAfterPeriod(tillDate, installment) || DateUtils.isEqual(tillDate, installment.getDueDate()); } - 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()); + // ReprocessAccruals + + private void reprocessPeriodicAccruals(Loan loan, final List accrualTransactions) { + if (loan.isChargedOff()) { + return; } - } - - private void checkAndUpdateAccrualsForInstallment(Loan loan, Collection accruals, - 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(); - } - - } + ensureAccrualTransactionMappings(loan); + List installments = loan.getRepaymentScheduleInstallments(); + boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate(); + for (LoanRepaymentScheduleInstallment installment : installments) { + checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment); } - installment.updateAccrualPortion(interest, fee, penalty); + // reverse accruals after last installment + LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); + reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate()); } - 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 - return installment.getFeeChargesCharged(loan.getCurrency()).isLessThan(fee) - || installment.getInterestCharged(loan.getCurrency()).isLessThan(interest) - || installment.getPenaltyChargesCharged(loan.getCurrency()).isLessThan(penalty) - || (loan.isInterestBearing() && DateUtils.isEqual(loan.getAccruedTill(), loanTransaction.getTransactionDate()) - && !DateUtils.isEqual(loan.getAccruedTill(), installment.getDueDate())); - } - - private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, boolean isChargeAccrualBasedOnSubmittedOnDate) { - // check config for charge accrual date and return date - return isChargeAccrualBasedOnSubmittedOnDate && !loanTransaction.getLoanChargesPaid().isEmpty() - ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getEffectiveDueDate() - : 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,142 +757,50 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private LocalDate createLoanScheduleAccrualDataList(Loan loan, LocalDate accruedTill, - 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(); + private void checkAndUpdateAccrualsForInstallment(Loan loan, List accrualTransactions, + List installments, boolean isBasedOnSubmittedOnDate, + LoanRepaymentScheduleInstallment installment) { 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()); - } + Money zero = Money.zero(currency); + Money interest = zero; + Money fee = zero; + Money penalty = zero; + for (LoanTransaction accrualTransaction : accrualTransactions) { + LocalDate transactionDateForRange = getDateForRangeCalculation(accrualTransaction, isBasedOnSubmittedOnDate); + boolean isInPeriod = 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(); } } - 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); - } + installment.updateAccrualPortion(interest, fee, penalty); } - private void createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, - Collection 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(), - interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), accrualExternalId); - LocalDate fromDate = loan.getDisbursementDate(); - if (loan.getAccruedTill() != null) { - fromDate = loan.getAccruedTill(); - } - newAccrualTransactions.add(accrualTransaction); - loan.addLoanTransaction(accrualTransaction); - Set accrualCharges = accrualTransaction.getLoanChargesPaid(); - for (LoanCharge loanCharge : loan.getActiveCharges()) { - boolean isDue = loanCharge.isDueInPeriod(fromDate, foreClosureDate, DateUtils.isEqual(fromDate, loan.getDisbursementDate())); - if (loanCharge.isActive() && !loanCharge.isPaid() && (isDue || loanCharge.isInstalmentFee())) { - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, - loanCharge.getAmountOutstanding(currency).getAmount(), null); - accrualCharges.add(loanChargePaidBy); - loanCharge.getLoanChargePaidBySet().add(loanChargePaidBy); - } - } + 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 + return installment.getFeeChargesCharged(loan.getCurrency()).isLessThan(fee) + || installment.getInterestCharged(loan.getCurrency()).isLessThan(interest) + || installment.getPenaltyChargesCharged(loan.getCurrency()).isLessThan(penalty) + || (loan.isInterestBearing() && DateUtils.isEqual(loan.getAccruedTill(), loanTransaction.getTransactionDate()) + && !DateUtils.isEqual(loan.getAccruedTill(), installment.getDueDate())); } - private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate tillDate, Map incomeDetails) { - MonetaryCurrency currency = loan.getCurrency(); - Money receivableInterest = Money.zero(currency); - Money receivableFee = Money.zero(currency); - Money receivablePenalty = Money.zero(currency); - for (final LoanTransaction transaction : loan.getLoanTransactions()) { - if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() - && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { - if (transaction.isAccrual()) { - receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) { - receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); - } - } - if (receivableInterest.isLessThanZero()) { - receivableInterest = receivableInterest.zero(); - } - if (receivableFee.isLessThanZero()) { - receivableFee = receivableFee.zero(); - } - if (receivablePenalty.isLessThanZero()) { - receivablePenalty = receivablePenalty.zero(); - } - } - - incomeDetails.put(Loan.INTEREST, receivableInterest); - incomeDetails.put(Loan.FEE, receivableFee); - incomeDetails.put(Loan.PENALTIES, receivablePenalty); + private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, boolean isChargeAccrualBasedOnSubmittedOnDate) { + // check config for charge accrual date and return date + return isChargeAccrualBasedOnSubmittedOnDate && !loanTransaction.getLoanChargesPaid().isEmpty() + ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getEffectiveDueDate() + : loanTransaction.getTransactionDate(); } - private List retrieveListOfAccrualTransactions(Loan loan) { - return loan.getLoanTransactions().stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) - .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); - } + // IncomePosting private List extractInterestRecalculationAdditionalDetails(Loan loan) { List retDetails = new ArrayList<>(); @@ -1065,21 +816,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing return retDetails; } - private List retrieveListOfIncomePostingTransactions(Loan loan) { - return loan.getLoanTransactions().stream() // - .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // - .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); - } - - private LoanTransaction getTransactionForDate(List transactions, LocalDate effectiveDate) { - for (LoanTransaction loanTransaction : transactions) { - if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) { - return loanTransaction; - } - } - return null; - } - private void addUpdateIncomeAndAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, LocalDate lastCompoundingDate, LoanTransaction existingIncomeTransaction, LoanTransaction existingAccrualTransaction) { BigDecimal interest = BigDecimal.ZERO; @@ -1114,29 +850,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing loan.updateLoanOutstandingBalances(); } - private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, - LoanTransaction existingAccrualTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, - HashMap feeDetails, ExternalId externalId) { - if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - - 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(); - LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties, externalId); - updateLoanChargesPaidBy(loan, accrual, feeDetails, null); - loan.addLoanTransaction(accrual); - } - } - } - private void createUpdateIncomePostingTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, LoanTransaction existingIncomeTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, ExternalId externalId) { if (existingIncomeTransaction == null) { @@ -1151,6 +864,248 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } + private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, + LoanTransaction existingAccrualTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, + HashMap feeDetails, ExternalId externalId) { + if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + 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); + loan.addLoanTransaction(accrual); + } + } + } + + // LoanClosure + + private void processIncomeAndAccrualTransactionOnLoanClosure(Loan loan) { + // TODO analyze progressive accrual case + if (loan.getLoanInterestRecalculationDetails() != null + && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction() + && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { + + LocalDate closedDate = loan.getClosedOnDate(); + reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate); + reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate); + + HashMap cumulativeIncomeFromInstallments = new HashMap<>(); + determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments); + HashMap cumulativeIncomeFromIncomePosting = new HashMap<>(); + determineCumulativeIncomeDetails(loan, retrieveListOfIncomePostingTransactions(loan), cumulativeIncomeFromIncomePosting); + + BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(Loan.INTEREST) + .subtract(cumulativeIncomeFromIncomePosting.get(Loan.INTEREST)); + BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(Loan.FEE).subtract(cumulativeIncomeFromIncomePosting.get(Loan.FEE)); + BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(Loan.PENALTY) + .subtract(cumulativeIncomeFromIncomePosting.get(Loan.PENALTY)); + BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); + + createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost); + } + loan.updateLoanOutstandingBalances(); + } + + private void determineCumulativeIncomeFromInstallments(Loan loan, HashMap cumulativeIncomeFromInstallments) { + BigDecimal interest = BigDecimal.ZERO; + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalty = BigDecimal.ZERO; + List installments = loan.getRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + interest = interest.add(installment.getInterestCharged(loan.getCurrency()).getAmount()); + fee = fee.add(installment.getFeeChargesCharged(loan.getCurrency()).getAmount()); + penalty = penalty.add(installment.getPenaltyChargesCharged(loan.getCurrency()).getAmount()); + } + cumulativeIncomeFromInstallments.put(Loan.INTEREST, interest); + cumulativeIncomeFromInstallments.put(Loan.FEE, fee); + cumulativeIncomeFromInstallments.put(Loan.PENALTY, penalty); + } + + private void determineCumulativeIncomeDetails(Loan loan, List transactions, + HashMap incomeDetailsMap) { + BigDecimal interest = BigDecimal.ZERO; + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalty = BigDecimal.ZERO; + for (LoanTransaction transaction : transactions) { + interest = interest.add(transaction.getInterestPortion(loan.getCurrency()).getAmount()); + fee = fee.add(transaction.getFeeChargesPortion(loan.getCurrency()).getAmount()); + penalty = penalty.add(transaction.getPenaltyChargesPortion(loan.getCurrency()).getAmount()); + } + incomeDetailsMap.put(Loan.INTEREST, interest); + incomeDetailsMap.put(Loan.FEE, fee); + incomeDetailsMap.put(Loan.PENALTY, penalty); + } + + private void createIncomePostingAndAccrualTransactionOnLoanClosure(Loan loan, LocalDate closedDate, BigDecimal interestToPost, + BigDecimal feeToPost, BigDecimal penaltyToPost, BigDecimal amountToPost) { + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); + + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(loan, loan.getOffice(), closedDate, amountToPost, + interestToPost, feeToPost, penaltyToPost, externalId); + loan.addLoanTransaction(finalIncomeTransaction); + + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + List updatedAccrualTransactions = retrieveListOfAccrualTransactions(loan); + LocalDate lastAccruedDate = loan.getDisbursementDate(); + if (!updatedAccrualTransactions.isEmpty()) { + lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); + } + HashMap feeDetails = new HashMap<>(); + determineFeeDetails(loan, lastAccruedDate, closedDate, feeDetails); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), closedDate, amountToPost, + interestToPost, feeToPost, penaltyToPost, externalId); + updateLoanChargesPaidBy(loan, finalAccrual, feeDetails, null); + loan.addLoanTransaction(finalAccrual); + } + } + + // LoanForClosure + + private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate tillDate, Map incomeDetails) { + MonetaryCurrency currency = loan.getCurrency(); + Money receivableInterest = Money.zero(currency); + Money receivableFee = Money.zero(currency); + Money receivablePenalty = Money.zero(currency); + for (final LoanTransaction transaction : loan.getLoanTransactions()) { + if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() + && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { + if (transaction.isAccrual()) { + receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); + receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); + receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); + } 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)); + } + } + if (receivableInterest.isLessThanZero()) { + receivableInterest = receivableInterest.zero(); + } + if (receivableFee.isLessThanZero()) { + receivableFee = receivableFee.zero(); + } + if (receivablePenalty.isLessThanZero()) { + receivablePenalty = receivablePenalty.zero(); + } + } + + incomeDetails.put(Loan.INTEREST, receivableInterest); + incomeDetails.put(Loan.FEE, receivableFee); + incomeDetails.put(Loan.PENALTIES, receivablePenalty); + } + + private void createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, + 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(), + interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), accrualExternalId); + LocalDate fromDate = loan.getDisbursementDate(); + if (loan.getAccruedTill() != null) { + fromDate = loan.getAccruedTill(); + } + newAccrualTransactions.add(accrualTransaction); + loan.addLoanTransaction(accrualTransaction); + Set accrualCharges = accrualTransaction.getLoanChargesPaid(); + for (LoanCharge loanCharge : loan.getActiveCharges()) { + boolean isDue = loanCharge.isDueInPeriod(fromDate, foreClosureDate, DateUtils.isEqual(fromDate, loan.getDisbursementDate())); + if (loanCharge.isActive() && !loanCharge.isPaid() && (isDue || loanCharge.isInstalmentFee())) { + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, + loanCharge.getAmountOutstanding(currency).getAmount(), null); + accrualCharges.add(loanChargePaidBy); + loanCharge.getLoanChargePaidBySet().add(loanChargePaidBy); + } + } + } + + // Utility + + 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, + final List existingReversedTransactionIds) { + final MonetaryCurrency currency = loan.getCurrency(); + boolean isAccountTransfer = false; + final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, + existingReversedTransactionIds, isAccountTransfer); + journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } + + private void ensureAccrualTransactionMappings(Loan loan) { + boolean chargeOnDueDate = isChargeOnDueDate(); + List transactions = retrieveListOfAccrualTransactions(loan); + for (LoanTransaction transaction : transactions) { + if (!MathUtil.isEmpty(transaction.getFeeChargesPortion()) || !MathUtil.isEmpty(transaction.getPenaltyChargesPortion())) { + 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(); + LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment( + i -> isInPeriod(chargeDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber))); + paidBy.setInstallmentNumber(installment.getInstallmentNumber()); + } + } + } + } + } + + private List retrieveListOfAccrualTransactions(Loan loan) { + return loan.getLoanTransactions().stream().filter(ACCRUAL_PREDICATE).sorted(LoanTransactionComparator.INSTANCE) + .collect(Collectors.toList()); + } + + private List retrieveListOfIncomePostingTransactions(Loan loan) { + return loan.getLoanTransactions().stream() // + .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); + } + + private LoanTransaction getTransactionForDate(List transactions, LocalDate effectiveDate) { + for (LoanTransaction loanTransaction : transactions) { + if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) { + return loanTransaction; + } + } + return null; + } + + private boolean isChargeOnDueDate() { + final String chargeAccrualDateType = configurationDomainService.getAccrualDateConfigForCharge(); + return !ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE.equalsIgnoreCase(chargeAccrualDateType); + } + private void determineFeeDetails(Loan loan, LocalDate fromDate, LocalDate toDate, Map feeDetails) { BigDecimal fee = BigDecimal.ZERO; BigDecimal penalties = BigDecimal.ZERO; @@ -1217,253 +1172,32 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private void reverseTransactionsPostEffectiveDate(Collection transactions, LocalDate effectiveDate) { + private boolean reverseTransactionsAfter(List transactions, LocalDate effectiveDate) { + boolean reversed = false; for (LoanTransaction loanTransaction : transactions) { if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { - loanTransaction.reverse(); + reverseAccrual(loanTransaction); + reversed = true; } } + return reversed; } - 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) { - if (loan.getLoanInterestRecalculationDetails() != null - && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction() - && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { - - LocalDate closedDate = loan.getClosedOnDate(); - reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate); - reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate); - - HashMap cumulativeIncomeFromInstallments = new HashMap<>(); - determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments); - HashMap cumulativeIncomeFromIncomePosting = new HashMap<>(); - determineCumulativeIncomeDetails(loan, retrieveListOfIncomePostingTransactions(loan), cumulativeIncomeFromIncomePosting); - - BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(Loan.INTEREST) - .subtract(cumulativeIncomeFromIncomePosting.get(Loan.INTEREST)); - BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(Loan.FEE).subtract(cumulativeIncomeFromIncomePosting.get(Loan.FEE)); - BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(Loan.PENALTY) - .subtract(cumulativeIncomeFromIncomePosting.get(Loan.PENALTY)); - BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); - - createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost); - } - loan.updateLoanOutstandingBalances(); - } - - private void createIncomePostingAndAccrualTransactionOnLoanClosure(Loan loan, LocalDate closedDate, BigDecimal interestToPost, - BigDecimal feeToPost, BigDecimal penaltyToPost, BigDecimal amountToPost) { - ExternalId externalId = ExternalId.empty(); - boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); - - if (isExternalIdAutoGenerationEnabled) { - externalId = ExternalId.generate(); - } - LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(loan, loan.getOffice(), closedDate, amountToPost, - interestToPost, feeToPost, penaltyToPost, externalId); - loan.addLoanTransaction(finalIncomeTransaction); - - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - List updatedAccrualTransactions = retrieveListOfAccrualTransactions(loan); - LocalDate lastAccruedDate = loan.getDisbursementDate(); - if (!updatedAccrualTransactions.isEmpty()) { - lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); - } - HashMap feeDetails = new HashMap<>(); - determineFeeDetails(loan, lastAccruedDate, closedDate, feeDetails); - if (isExternalIdAutoGenerationEnabled) { - externalId = ExternalId.generate(); - } - LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), closedDate, amountToPost, - interestToPost, feeToPost, penaltyToPost, externalId); - updateLoanChargesPaidBy(loan, finalAccrual, feeDetails, null); - loan.addLoanTransaction(finalAccrual); - } - } - - private void reverseTransactionsOnOrAfter(List transactions, LocalDate date) { + private boolean reverseTransactionsOnOrAfter(List transactions, LocalDate date) { + boolean reversed = false; for (LoanTransaction loanTransaction : transactions) { if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) { - loanTransaction.reverse(); + reverseAccrual(loanTransaction); + reversed = true; } } + return reversed; } - private void determineCumulativeIncomeFromInstallments(Loan loan, HashMap cumulativeIncomeFromInstallments) { - BigDecimal interest = BigDecimal.ZERO; - BigDecimal fee = BigDecimal.ZERO; - BigDecimal penalty = BigDecimal.ZERO; - List installments = loan.getRepaymentScheduleInstallments(); - for (LoanRepaymentScheduleInstallment installment : installments) { - interest = interest.add(installment.getInterestCharged(loan.getCurrency()).getAmount()); - fee = fee.add(installment.getFeeChargesCharged(loan.getCurrency()).getAmount()); - penalty = penalty.add(installment.getPenaltyChargesCharged(loan.getCurrency()).getAmount()); - } - cumulativeIncomeFromInstallments.put(Loan.INTEREST, interest); - cumulativeIncomeFromInstallments.put(Loan.FEE, fee); - cumulativeIncomeFromInstallments.put(Loan.PENALTY, penalty); - } - - private void determineCumulativeIncomeDetails(Loan loan, Collection transactions, - HashMap incomeDetailsMap) { - BigDecimal interest = BigDecimal.ZERO; - BigDecimal fee = BigDecimal.ZERO; - BigDecimal penalty = BigDecimal.ZERO; - for (LoanTransaction transaction : transactions) { - interest = interest.add(transaction.getInterestPortion(loan.getCurrency()).getAmount()); - fee = fee.add(transaction.getFeeChargesPortion(loan.getCurrency()).getAmount()); - penalty = penalty.add(transaction.getPenaltyChargesPortion(loan.getCurrency()).getAmount()); - } - incomeDetailsMap.put(Loan.INTEREST, interest); - incomeDetailsMap.put(Loan.FEE, fee); - incomeDetailsMap.put(Loan.PENALTY, penalty); + private void reverseAccrual(LoanTransaction transaction) { + transaction.reverse(); + LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); } private LocalDate getFinalAccrualTransactionDate(Loan loan) { @@ -1474,4 +1208,11 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing }; } + public boolean isProgressiveAccrual(@NotNull Loan loan) { + return loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE; + } + + 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 34cf975ba..214bb16d3 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.portfolio.rate.service.RateAssembler; 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 class LoanAssembler { 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 class LoanAssembler { // 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 @@ public class LoanAssembler { } } + @Override public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { CodeValue codeValue = null; if (codeValueId != null) { @@ -379,6 +384,7 @@ public class LoanAssembler { return codeValue; } + @Override public Fund findFundByIdIfProvided(final Long fundId) { Fund fund = null; if (fundId != null) { @@ -387,6 +393,7 @@ public class LoanAssembler { return fund; } + @Override public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) { Staff staff = null; if (loanOfficerId != null) { @@ -419,6 +426,7 @@ public class LoanAssembler { } } + @Override public Map updateFrom(JsonCommand command, Loan loan) { final Map changes = new HashMap<>(); LoanProduct loanProduct; @@ -844,6 +852,7 @@ public class LoanAssembler { return changes; } + @Override public Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser) { final Map actualChanges = new LinkedHashMap<>(); @@ -868,6 +877,7 @@ public class LoanAssembler { 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 2ebaaf1f8..efe690299 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.sql.SQLException; 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 class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform } @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 class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform 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 ed0203718..d69d78c3c 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 @@ -838,14 +838,11 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation); - defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory .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 8f39c262e..e98348858 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.DisbursementData; 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 @@ public interface LoanReadPlatformService { 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 @@ public interface LoanReadPlatformService { 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 fce39ef51..6d951c047 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.ArrayList; 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.charge.service.ChargeReadPlatformService; 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.LoanApprovalData; 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; @@ -1742,258 +1737,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa 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); @@ -2146,155 +1889,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa } } - @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 ba6284515..06f402c41 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.BusinessEventListener; 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 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { final Loan loan = event.get(); log.debug("Loan Status change for loan {}", loan.getId()); - if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { - log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId()); - loanAccrualsProcessingService.processAccrualsForLoanClosure(loan); - } - if (loan.isOpen()) { + LoanStatus oldStatus = event.getOldStatus(); + LoanStatus newStatus = loan.getStatus(); + if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) { + log.debug("Loan Status {} for loan {}", newStatus.getCode(), loan.getId()); + loan.updateLoanSummaryDerivedFields(); + loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan); + } else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) { loan.handleMaturityDateActivate(); + loanAccrualsProcessingService.processAccrualsOnLoanReopen(loan); } } } @@ -64,10 +68,11 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { final Loan loan = event.get(); if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { - if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { + LoanStatus oldStatus = event.getOldStatus(); + LoanStatus newStatus = loan.getStatus(); + if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) { loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan); - } - if (loan.isOpen()) { + } else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) { loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index b8ed5b39c..8fd879d5f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -597,15 +597,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.getLoanProduct().isMultiDisburseLoan()) { final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); if (!allNonContraTransactionsPostDisbursement.isEmpty()) { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory - .determineProcessor(loan.getTransactionProcessingStrategyCode()); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), - allNonContraTransactionsPostDisbursement, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), - loan.getActiveCharges()); - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - mapEntry.getValue().updateLoan(loan); - } - loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values()); + changedTransactionDetail = loan.reprocessTransactions(); } loan.updateLoanSummaryDerivedFields(); } @@ -1023,7 +1015,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf // 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 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final boolean isTransactionChronologicallyLatest = loan .isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction); - if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) { + if (newInterestPaymentWaiverTransaction.isNotZero()) { loan.addLoanTransaction(newInterestPaymentWaiverTransaction); } @@ -1133,19 +1125,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf 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 @@ -1503,7 +1483,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf 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 8b9490f07..77509f464 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 java.util.stream.Stream; 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 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS 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 class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS } 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 5fccb83f9..5f2728e77 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.math.BigDecimal; 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 class LoanReAgingServiceImpl { } reverseReAgeTransaction(reAgeTransaction, command); loanTransactionRepository.saveAndFlush(reAgeTransaction); - - reProcessLoanTransactions(reAgeTransaction.getLoan()); + loan.reprocessTransactions(); loan.updateLoanScheduleDependentDerivedFields(); persistNote(loan, command, changes); @@ -179,15 +177,6 @@ public class LoanReAgingServiceImpl { 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 1546f6c93..10a73d3dd 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.math.BigDecimal; 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 @@ public class LoanReAmortizationServiceImpl { 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 269452c3e..0f7ce982f 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 @@ package org.apache.fineract.portfolio.loanaccount.starter; 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 class LoanAccountAutoStarter { @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 d045c0917..bd5e5d17d 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.LoanApplicationWritePla 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 class LoanAccountConfiguration { 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/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 47e57821f..d1e501c58 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 class ExternalEventConfigurationValidationServiceTest { "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 class ExternalEventConfigurationValidationServiceTest { "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 a62e1f2dc..6b3d6054c 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 class LoanTest { 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 8f8b7a6ff..59f48876f 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 class ClientLoanIntegrationTest extends BaseLoanIntegrationTest { 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/LoanChargebackWithCreditAllocationsIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java index 696fafe63..70b42eecb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java @@ -300,7 +300,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa transaction(312.0, "Repayment", "20 February 2023", 626.0, 312.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(312.0, "Repayment", "20 March 2023", 314.0, 312.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(384.0, "Repayment", "20 April 2023", 0.0, 314.0, 0.0, 50.0, 20.0, 0.0, 0.0), // - transaction(70.0, "Accrual", "20 April 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(100.0, "Chargeback", "02 May 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) // ); @@ -585,7 +584,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), // - transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(100.0, "Chargeback", "02 May 2023", 50.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0) // ); @@ -651,7 +649,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), // - transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(100.0, "Chargeback", "20 January 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) // ); @@ -718,7 +715,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa verifyTransactions(loanId, // transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), // - transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), // transaction(100.0, "Chargeback", "20 January 2023", 0.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) // ); 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 b9ece7fb9..3af858d15 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 @@ -317,7 +317,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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")); }); } @@ -437,7 +437,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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")); }); } @@ -634,7 +635,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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", () -> { @@ -654,7 +656,9 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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") // + ); // }); } @@ -692,7 +696,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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") // ); }); } @@ -992,7 +997,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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); @@ -1109,6 +1115,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // transaction(85.63, "Repayment", "10 January 2021"), // + transaction(5.7, "Accrual", "22 January 2021"), // transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), // transaction(5.42, "Interest Refund", "22 January 2021") // ); @@ -1183,7 +1190,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { 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(); @@ -1194,7 +1202,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { reversedTransaction(85.63, "Repayment", "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/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 89ec953e5..a531b6374 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -569,7 +569,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati verifyTransactions(loanId.get(), transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // transaction(1070.0, "Repayment", repaymentDate1, 0.0, 1000.0, 0.0, 40.0, 30, 0.0, 0.0, true), // - transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false)); // }); @@ -665,7 +664,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(500.0)).locale("en")); verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), // - transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); // @@ -675,8 +673,8 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get())); verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), // - transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), // + transaction(70.0, "Accrual", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); // @@ -728,7 +726,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(500.0)).locale("en")); verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), // - transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), // transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); // @@ -793,6 +790,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati final String disbursementDay2 = "02 February 2023"; final String repaymentDate1 = "15 January 2023"; final String repaymentPeriod1DueDate = "01 February 2023"; + final String repaymentPeriod2DueDate = "01 March 2023"; final String repaymentPeriod1CloseDate = "02 February 2023"; final String repaymentPeriod2CloseDate = "02 March 2023"; final String creationBusinessDay = "15 January 2023"; @@ -830,7 +828,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false), transaction(570, "Repayment", repaymentDate1, 0.0, 500.0, 0, 40.0, 30.0, 0, 0, false), - transaction(70.0, "Accrual", repaymentDate1, 0, 0, 0.0, 40.0, 30.0, 0, 0, false), transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false), transaction(500.0, "Disbursement", disbursementDay2, 500, 0, 0, 0, 0, 0, 0, false)); }); @@ -839,10 +836,10 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get())); verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false), - transaction(70.0, "Accrual", repaymentDate1, 0, 0, 0.0, 40.0, 30.0, 0, 0, false), transaction(570.0, "Repayment", repaymentDate1, 0.0, 500.0, 0, 40.0, 30.0, 0, 0, false), transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false), - transaction(500.0, "Disbursement", disbursementDay2, 500.0, 0, 0, 0, 0, 0, 0, false)); + transaction(500.0, "Disbursement", disbursementDay2, 500.0, 0, 0, 0, 0, 0, 0, false), + transaction(70.0, "Accrual", repaymentPeriod2DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false)); }); } 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 9c35d61e4..908f1b70f 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 class ExternalEventConfigurationHelper { 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 class ExternalEventConfigurationHelper { defaults.add(loanTransactionInterestRefundPreBusinessEvent); return defaults; - } public static String getExternalEventConfigurationsForUpdateJSON() {