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 f7c135710..131951e9c 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 @@ -1349,13 +1349,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money feeChargesPortion = Money.zero(transactionCtx.getCurrency()); Money penaltychargesPortion = Money.zero(transactionCtx.getCurrency()); for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) { - if (currentInstallment.isNotFullyPaidOff()) { - principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(transactionCtx.getCurrency())); - interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(transactionCtx.getCurrency())); - feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(transactionCtx.getCurrency())); - penaltychargesPortion = penaltychargesPortion - .plus(currentInstallment.getPenaltyChargesOutstanding(transactionCtx.getCurrency())); - } + principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(transactionCtx.getCurrency())); + interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(transactionCtx.getCurrency())); + feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(transactionCtx.getCurrency())); + penaltychargesPortion = penaltychargesPortion + .plus(currentInstallment.getPenaltyChargesOutstanding(transactionCtx.getCurrency())); } loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); @@ -1489,9 +1487,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep final Money amountToEditLastInstallment = loanTransaction.getLoan().getPrincipal().minus(installments.stream() .filter(i -> !i.isAdditional()).map(LoanRepaymentScheduleInstallment::getPrincipal).reduce(ZERO, BigDecimal::add)); - if (amountToEditLastInstallment.getAmount().signum() != 0) { - installments.stream().filter(i -> !i.isAdditional() && !i.isObligationsMet()).reduce((first, second) -> second) - .ifPresent(last -> last.updatePrincipal(last.getPrincipal().add(amountToEditLastInstallment.getAmount()))); + BigDecimal principalBalance = amountToEditLastInstallment.getAmount(); + for (int i = installments.size() - 1; i > 0 && BigDecimal.ZERO.compareTo(principalBalance) != 0; i--) { + final LoanRepaymentScheduleInstallment installment = installments.get(i); + if (!installment.isAdditional() && !installment.isObligationsMet()) { + final BigDecimal installmentPrincipal = MathUtil.nullToZero(installment.getPrincipal()); + + installment.updatePrincipal(MathUtil.negativeToZero(installmentPrincipal.add(principalBalance))); + if (MathUtil.isLessThanOrEqualTo(MathUtil.abs(principalBalance), installmentPrincipal)) { + principalBalance = BigDecimal.ZERO; + } else { + principalBalance = principalBalance.signum() < 0 ? principalBalance.add(installmentPrincipal) + : principalBalance.subtract(installmentPrincipal); + } + } } } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java index 1f7c02ab8..802c3522f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java @@ -28,6 +28,7 @@ import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AllowAttributeOverrides; import org.apache.fineract.client.models.ChargeData; @@ -46,6 +48,7 @@ import org.apache.fineract.client.models.GetLoanTransactionRelation; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.JournalEntryTransactionItem; +import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; @@ -541,6 +544,58 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest extends BaseL } + @Test + public void testProgressiveChargeOffWithEarlyRepaymentZeroInterest() { + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + AtomicReference loanIdRef = new AtomicReference<>(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().chargeOffBehaviour("ZERO_INTEREST")); + + runAt("01 January 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "01 January 2024", + 1000.0, 7.0, 6, null); + + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(1000), "01 January 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 164.26, 0, 0, 5.83); + validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 165.21, 0, 0, 4.88); + validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 166.18, 0, 0, 3.91); + validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 167.15, 0, 0, 2.94); + validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 168.12, 0, 0, 1.97); + validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 169.08, 0, 0, 0.99); + + verifyTransactions(loanId, transaction(1000.0d, "Disbursement", "01 January 2024")); + executeInlineCOB(loanId); + }); + runAt("10 February 2024", () -> { + Long loanId = loanIdRef.get(); + + loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("15 January 2024").locale("en").transactionAmount(170.09)); + + loanTransactionHelper.chargeOffLoan(loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate("31 January 2024").locale("en").dateFormat("dd MMMM yyyy")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 164.95, 167.46, -2.51, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.14, + 2.63, 2.51, 170.09, 0.0); + validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 170.09, 0, 0, 0.0); + validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 170.09, 0, 0, 0.0); + validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 170.09, 0, 0, 0.0); + validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 170.09, 0, 0, 0.0); + validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 154.69, 0, 0, 0.0); + + verifyTransactions(loanId, // + transaction(1000.0d, "Disbursement", "01 January 2024", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // + transaction(170.09d, "Repayment", "15 January 2024", 832.54, 167.46, 2.63, 0.0, 0.0, 0.0, 0.0, false), // + transaction(835.05d, "Charge-off", "31 January 2024", 0.0, 832.54, 2.51, 0.0, 0.0, 0.0, 0.0, false) // + ); + }); + } + private void verifyJournalEntry(JournalEntryTransactionItem journalEntryTransactionItem, Double amount, LocalDate entryDate, Account account, String type) { assertEquals(amount, journalEntryTransactionItem.getAmount()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java index 7ede4180a..7db24009b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java @@ -33,9 +33,8 @@ import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.integrationtests.common.ClientHelper; -import org.junit.Ignore; -import org.junit.Test; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; @Slf4j public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { @@ -474,7 +473,7 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { // Accruals around installment due dates are as expected validateTransactionsExist(loanDetails, // - transaction(0.16, "Accrual", "20 January 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // + transaction(0.17, "Accrual", "20 January 2025", 0.0, 0.0, 0.17, 0.0, 0.0, 0.0, 0.0), // transaction(0.16, "Accrual", "21 January 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // transaction(0.16, "Accrual", "20 February 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // transaction(0.18, "Accrual", "21 February 2025", 0.0, 0.0, 0.18, 0.0, 0.0, 0.0, 0.0), // @@ -623,7 +622,6 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { }); } - @Ignore // TODO: enable when implementation is complete @Test public void testProgressiveChargeBackInterestRecalculation() { AtomicReference loanIdRef = new AtomicReference<>();