From aac6f7cd21a06c0cdc6384a1d2e84228535e3817 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 19 Mar 2024 18:17:52 +0100 Subject: [PATCH] FINERACT-1971: Fix loan balance for additional (N+1) installment --- .../service/LoanReadPlatformServiceImpl.java | 18 +-- .../BaseLoanIntegrationTest.java | 26 +++- .../reaging/LoanReAgingIntegrationTest.java | 144 ++++++++++++++++++ 3 files changed, 170 insertions(+), 18 deletions(-) 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 bfa5b96b5..ef43af882 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 @@ -1225,16 +1225,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final boolean complete = rs.getBoolean("complete"); final boolean isAdditional = rs.getBoolean("isAdditional"); BigDecimal disbursedAmount = BigDecimal.ZERO; - if (!isAdditional) { - disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds, - disbursementChargeAmount, waivedChargeAmount, periods); - } + disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds, + disbursementChargeAmount, waivedChargeAmount, periods); + // Add the Charge back or Credits to the initial amount to avoid negative balance final BigDecimal credits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalCredits"); - if (!isAdditional) { - this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(credits); - } + + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(credits); totalPrincipalDisbursed = totalPrincipalDisbursed.add(disbursedAmount); @@ -1313,16 +1311,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa } BigDecimal outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.subtract(principalDue); - if (isAdditional) { - outstandingPrincipalBalanceOfLoan = this.outstandingLoanPrincipalBalance.add(principalDue); - } // update based on current period values this.lastDueDate = dueDate; this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(principalDue); - if (isAdditional) { - this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalDue); - } final boolean isDownPayment = rs.getBoolean("isDownPayment"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index fe7b07331..95156bc93 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -547,6 +547,13 @@ public abstract class BaseLoanIntegrationTest { outstandingTotalExpected, outstandingTotal)); } + Double loanBalanceExpected = installments[i].loanBalance; + Double loanBalance = period.getPrincipalLoanBalanceOutstanding(); + if (loanBalanceExpected != null) { + Assertions.assertEquals(loanBalanceExpected, loanBalance, + "%d. installment's loan balance is different, expected: %.2f, actual: %.2f".formatted(i, loanBalanceExpected, + loanBalance)); + } installmentNumber++; Assertions.assertEquals(installmentNumber, period.getPeriod()); } @@ -716,27 +723,35 @@ public abstract class BaseLoanIntegrationTest { } protected Installment installment(double principalAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, null, null, null, null, completed, dueDate, null); + return new Installment(principalAmount, null, null, null, null, completed, dueDate, null, null); } protected Installment installment(double principalAmount, double interestAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null); + return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null, null); } protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null); + return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null, null); } protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate, null); + return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstandingAmount, completed, dueDate, null, + null); } protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount, OutstandingAmounts outstandingAmounts, Boolean completed, String dueDate) { - return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, null, completed, dueDate, outstandingAmounts); + return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, null, completed, dueDate, outstandingAmounts, + null); + } + + protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double penaltyAmount, + double totalOutstanding, Boolean completed, String dueDate, double loanBalance) { + return new Installment(principalAmount, interestAmount, feeAmount, penaltyAmount, totalOutstanding, completed, dueDate, null, + loanBalance); } protected OutstandingAmounts outstanding(double principal, double fee, double penalty, double total) { @@ -921,6 +936,7 @@ public abstract class BaseLoanIntegrationTest { Boolean completed; String dueDate; OutstandingAmounts outstandingAmounts; + Double loanBalance; } @AllArgsConstructor diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java index e3bebe228..2db311f86 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java @@ -28,6 +28,7 @@ import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; @@ -262,4 +263,147 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest { }); } + @Test + public void test_LoanReAgeTransaction_WithChargeback_Works() { + AtomicLong createdLoanId = new AtomicLong(); + + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 3; + int repaymentEvery = 1; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .enableDownPayment(true) // + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) // + .enableAutoRepaymentForDownPayment(true) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); // + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1250.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(312.5, "Down Payment", "01 January 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(1250, null, "01 January 2023"), // + installment(312.5, true, "01 January 2023"), // + installment(312.5, false, "01 February 2023"), // + installment(312.5, false, "01 March 2023"), // + installment(312.5, false, "01 April 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1)); + createdLoanId.set(loanId); + }); + + String repaymentExternalId = UUID.randomUUID().toString(); + runAt("01 February 2023", () -> { + long loanId = createdLoanId.get(); + + loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("01 February 2023").locale("en").transactionAmount(100.0).externalId(repaymentExternalId)); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(312.5, "Down Payment", "01 January 2023"), // + transaction(100.0, "Repayment", "01 February 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(1250, null, "01 January 2023"), // + installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023"), // + installment(312.5, 0, 0, 0, 212.5, false, "01 February 2023"), // + installment(312.5, 0, 0, 0, 312.5, false, "01 March 2023"), // + installment(312.5, 0, 0, 0, 312.5, false, "01 April 2023") // + ); + }); + + runAt("10 April 2023", () -> { + long loanId = createdLoanId.get(); + + // disburse Loan + loanTransactionHelper.chargebackLoanTransaction(loanId, repaymentExternalId, + new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(100.0)); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(312.5, "Down Payment", "01 January 2023"), // + transaction(100.0, "Repayment", "01 February 2023"), // + transaction(100.0, "Chargeback", "10 April 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(1250, null, "01 January 2023"), // + installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023", 937.5), // + installment(312.5, 0, 0, 0, 212.5, false, "01 February 2023", 625.0), // + installment(312.5, 0, 0, 0, 312.5, false, "01 March 2023", 312.5), // + installment(312.5, 0, 0, 0, 312.5, false, "01 April 2023", 0.0), // + installment(100.0, 0.0, 0.0, 0.0, 100.0, false, "10 April 2023", 0.0) // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 4, 1), LocalDate.of(2023, 4, 1)); + }); + + runAt("12 April 2023", () -> { + long loanId = createdLoanId.get(); + + // create re-age transaction + reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(312.5, "Down Payment", "01 January 2023"), // + transaction(100.0, "Repayment", "01 February 2023"), // + transaction(100.0, "Chargeback", "10 April 2023"), // + transaction(937.5, "Re-age", "12 April 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(1250, null, "01 January 2023"), // + installment(312.5, 0, 0, 0, 0.0, true, "01 January 2023", 937.5), // + installment(100.0, 0, 0, 0, 0.0, true, "01 February 2023", 837.5), // + installment(0.0, 0, 0, 0, 0.0, true, "01 March 2023", 837.5), // + installment(0.0, 0, 0, 0, 0.0, true, "01 April 2023", 837.5), // + installment(0.0, 0.0, 0.0, 0.0, 0.0, true, "10 April 2023", 937.5), // + installment(234.38, 0.0, 0.0, 0.0, 234.38, false, "12 April 2023", 703.12), // + installment(234.38, 0.0, 0.0, 0.0, 234.38, false, "12 May 2023", 468.74), // + installment(234.38, 0.0, 0.0, 0.0, 234.38, false, "12 June 2023", 234.36), // + installment(234.36, 0.0, 0.0, 0.0, 234.36, false, "12 July 2023", 0.0) // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12)); + }); + } }