FINERACT-1981: Pay-off does not consider the overdue installments
This commit is contained in:
committed by
Adam Saghy
parent
94360c9f8a
commit
f92290b355
+3
@@ -274,6 +274,9 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
.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)));
|
||||
|
||||
+146
@@ -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);
|
||||
}
|
||||
}
|
||||
+101
@@ -3118,6 +3118,94 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
|
||||
|
||||
}
|
||||
|
||||
@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() {
|
||||
|
||||
@@ -7217,6 +7305,19 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user