Skip to content

Commit

Permalink
FINERACT-1981: Pay-off does not consider the overdue installments
Browse files Browse the repository at this point in the history
  • Loading branch information
kulminsky authored and adamsaghy committed Oct 29, 2024
1 parent 94360c9 commit f92290b
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency
.principal(result.getOutstandingBalance()) //
.interest(result.getPayableInterest());

installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
.forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));

installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* 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.loanschedule.domain;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
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.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class PrepaymentCalculationTest {

private static final MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
.fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$"));

@Mock
private LoanProductRelatedDetail loanProductRelatedDetail;

@Mock
private LoanApplicationTerms loanApplicationTerms;

@Mock
private AdvancedPaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;

@Mock
private EMICalculator emiCalculator;

@InjectMocks
private ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator;

private Loan loan;

@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);

moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);

loan = Mockito.mock(Loan.class);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(loan.getDisbursementDate()).thenReturn(LocalDate.of(2022, 9, 7));
when(loan.getRepaymentScheduleInstallments()).thenReturn(createRepaymentScheduleInstallments());

when(loanApplicationTerms.getPreClosureInterestCalculationStrategy())
.thenReturn(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE);

when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0));

List<RepaymentPeriod> repaymentPeriods = createMockRepaymentPeriods();
ProgressiveLoanInterestScheduleModel scheduleModel = new ProgressiveLoanInterestScheduleModel(repaymentPeriods,
loanProductRelatedDetail, 100, mc);

when(loanRepaymentScheduleTransactionProcessor.reprocessProgressiveLoanTransactions(Mockito.any(), Mockito.anyList(), Mockito.any(),
Mockito.anyList(), Mockito.anySet())).thenReturn(org.apache.commons.lang3.tuple.Pair.of(null, scheduleModel));

PayableDetails payableDetails = new PayableDetails(Money.of(monetaryCurrency, BigDecimal.valueOf(200)),
Money.of(monetaryCurrency, BigDecimal.valueOf(500)), Money.of(monetaryCurrency, BigDecimal.valueOf(0)),
Money.of(monetaryCurrency, BigDecimal.valueOf(1000)));

when(emiCalculator.getPayableDetails(Mockito.any(ProgressiveLoanInterestScheduleModel.class), Mockito.any(LocalDate.class),
Mockito.any(LocalDate.class))).thenReturn(payableDetails);
}

@AfterAll
public static void tearDown() {
moneyHelper.close();
}

@Test
public void testCalculatePrepaymentAmount() {
LocalDate prepaymentDate = LocalDate.of(2023, 6, 1);

OutstandingAmountsDTO result = progressiveLoanScheduleGenerator.calculatePrepaymentAmount(monetaryCurrency, prepaymentDate,
loanApplicationTerms, mc, loan, null, loanRepaymentScheduleTransactionProcessor);

assertEquals("1000.00", result.principal().getAmount().toString());
assertEquals("15.00", result.interest().getAmount().toString());
}

private List<LoanRepaymentScheduleInstallment> createRepaymentScheduleInstallments() {
LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2022, 10, 1),
LocalDate.of(2022, 11, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(10), BigDecimal.ZERO, BigDecimal.ZERO, false, null);

LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 11, 2),
LocalDate.of(2022, 12, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ZERO, false, null);

return List.of(installment1, installment2);
}

private List<RepaymentPeriod> createMockRepaymentPeriods() {
RepaymentPeriod period1 = Mockito.mock(RepaymentPeriod.class);
when(period1.getFromDate()).thenReturn(LocalDate.of(2022, 10, 1));
when(period1.getDueDate()).thenReturn(LocalDate.of(2022, 11, 1));

RepaymentPeriod period2 = Mockito.mock(RepaymentPeriod.class);
when(period2.getFromDate()).thenReturn(LocalDate.of(2022, 11, 2));
when(period2.getDueDate()).thenReturn(LocalDate.of(2022, 12, 1));

return List.of(period1, period2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3118,6 +3118,94 @@ public void testRBIPaymentStrategy() {

}

@Test
public void testLoanPrePaymentWithMultiplePayments() {
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);

// Create a loan product
Integer loanProductId = createLoanProduct(false, NONE);
Integer collateralId = CollateralManagementHelper.createCollateralProduct(REQUEST_SPEC, RESPONSE_SPEC);
Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(REQUEST_SPEC, RESPONSE_SPEC,
String.valueOf(clientID), collateralId);
List<HashMap> collaterals = List.of(collaterals(clientCollateralId, BigDecimal.ONE));

// Apply for a loan
final String disbursementDate = "1 May 2023";
final String approvalDate = "1 April 2023";
final String submissionDate = "1 March 2023";
final String interestRate = "7";
final Integer loanID = applyForLoanApplication(clientID, loanProductId, disbursementDate, submissionDate, interestRate, null, null,
"1000", collaterals);
Assertions.assertNotNull(loanID);

// Check loan status
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);

// Approve the loan
LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = LOAN_TRANSACTION_HELPER.approveLoan(approvalDate, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);

// Disburse the loan
LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------");
String loanDetails = LOAN_TRANSACTION_HELPER.getLoanDetails(REQUEST_SPEC, RESPONSE_SPEC, loanID);
loanStatusHashMap = LOAN_TRANSACTION_HELPER.disburseLoanWithNetDisbursalAmount(disbursementDate, loanID,
JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);

// Make the first partial repayment
LOG.info("------------------------MAKE FIRST PARTIAL REPAYMENT-----------------------------------");
Float firstRepaymentAmount = 500.0f; // First partial repayment
String firstRepaymentDate = "1 June 2023";
LOAN_TRANSACTION_HELPER.makeRepayment(firstRepaymentDate, firstRepaymentAmount, loanID);

// Verify the prepayment amount after the first partial repayment
LOG.info("------------------------GET PREPAYMENT AMOUNT AFTER FIRST PAYMENT-----------------------");
HashMap<String, Object> prepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID);
Assertions.assertNotNull(prepayAmount);

// Extract the principal and interest portions
Float totalPrepayAmount = (Float) prepayAmount.get("amount");
Float principalAmount = (Float) prepayAmount.get("principalPortion");
Float interestAmount = (Float) prepayAmount.get("interestPortion");

// Expected values after the first partial repayment
Float expectedTotalPrepayAmount = 606.18f;
Float expectedPrincipal = 570.0f;
Float expectedInterest = 36.18f;

// Validate calculations
validateNumberForEqual(String.valueOf(expectedTotalPrepayAmount), String.valueOf(totalPrepayAmount));
validateNumberForEqual(String.valueOf(expectedPrincipal), String.valueOf(principalAmount));
validateNumberForEqual(String.valueOf(expectedInterest), String.valueOf(interestAmount));

// Make the second partial repayment
LOG.info("------------------------MAKE SECOND PARTIAL REPAYMENT----------------------------------");
Float secondRepaymentAmount = 606.18f;
String secondRepaymentDate = "1 July 2023";
LOAN_TRANSACTION_HELPER.makeRepayment(secondRepaymentDate, secondRepaymentAmount, loanID);

// Recheck the prepayment amount
LOG.info("------------------------RECHECK PREPAYMENT AMOUNT AFTER FULL REPAYMENT------------------");
HashMap<String, Object> postPrepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID);
Assertions.assertNotNull(postPrepayAmount);

// Verify that the principal and interest portions are zero
Float postPrincipalAmount = (Float) postPrepayAmount.get("principalPortion");
Float postInterestAmount = (Float) postPrepayAmount.get("interestPortion");

validateNumberForEqual("0.0", String.valueOf(postPrincipalAmount));
validateNumberForEqual("0.0", String.valueOf(postInterestAmount));

// Check the loan status after repayment
LOG.info("------------------------CHECK LOAN STATUS---------------------------------------------");
loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
}

@Test
public void testLoanScheduleWithInterestRecalculation_WITH_REST_SAME_AS_REPAYMENT_INTEREST_COMPOUND_NONE_STRATEGY_REDUCE_EMI() {

Expand Down Expand Up @@ -7217,6 +7305,19 @@ private Integer applyForLoanApplication(final Integer clientID, final Integer lo
return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON);
}

private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String disbursementDate,
String submissionDate, String interestRate, List<HashMap> charges, final String savingsId, String principal,
List<HashMap> collaterals) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal).withLoanTermFrequency("2")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("2").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate).withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
.withExpectedDisbursementDate(disbursementDate).withSubmittedOnDate(submissionDate).withCollaterals(collaterals)
.withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId);
return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON);
}

private Integer applyForLoanApplicationWithExternalId(RequestSpecification requestSpecification,
ResponseSpecification responseSpecification, final Integer clientID, final Integer loanProductID, String principal,
final String externalId) {
Expand Down

0 comments on commit f92290b

Please sign in to comment.