FINERACT-1971: Fix loan balance for additional (N+1) installment
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 bfa5b96..ef43af8 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 @@
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 @@
}
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 fe7b073..95156bc 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 @@
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 @@
}
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 @@
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 e3bebe2..2db311f 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.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 @@
});
}
+ @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));
+ });
+ }
}