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()) //
|
.principal(result.getOutstandingBalance()) //
|
||||||
.interest(result.getPayableInterest());
|
.interest(result.getPayableInterest());
|
||||||
|
|
||||||
|
installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
|
||||||
|
.forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));
|
||||||
|
|
||||||
installments.forEach(installment -> amounts //
|
installments.forEach(installment -> amounts //
|
||||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(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
|
@Test
|
||||||
public void testLoanScheduleWithInterestRecalculation_WITH_REST_SAME_AS_REPAYMENT_INTEREST_COMPOUND_NONE_STRATEGY_REDUCE_EMI() {
|
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);
|
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,
|
private Integer applyForLoanApplicationWithExternalId(RequestSpecification requestSpecification,
|
||||||
ResponseSpecification responseSpecification, final Integer clientID, final Integer loanProductID, String principal,
|
ResponseSpecification responseSpecification, final Integer clientID, final Integer loanProductID, String principal,
|
||||||
final String externalId) {
|
final String externalId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user