FINERACT-1981: Prepayment calculation

This commit is contained in:
Adam Saghy
2024-11-06 15:04:17 +01:00
parent 474623b6e3
commit 492ec44cef
16 changed files with 391 additions and 255 deletions
@@ -59,7 +59,6 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
@@ -82,7 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -149,7 +148,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// only for progressive loans
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
LocalDate disbursementDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
LocalDate disbursementDate, LocalDate currentDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
if (loanTransactions.isEmpty()) {
@@ -213,7 +212,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
createNewTransaction(oldTransaction, newTransaction, ctx);
}
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
recalculateInterestForDate(currentDate, ctx);
List<LoanTransaction> txs = changeOperations.stream() //
.filter(ChangeOperation::isTransaction) //
.map(e -> e.getLoanTransaction().get()).toList();
@@ -248,7 +247,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
LocalDate currentDate = DateUtils.getBusinessLocalDate();
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
.getLeft();
}
@NotNull
@@ -1590,11 +1591,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
PayableDetails payableDetails = emiCalculator.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate);
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate);
switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getPayableInterest().getAmount());
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getPayablePrincipal().getAmount());
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount());
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getDuePrincipal().getAmount());
default -> {
}
}
@@ -0,0 +1,29 @@
/**
* 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.data;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@Data
public class OutstandingDetails {
private final Money outstandingPrincipal;
private final Money outstandingInterest;
}
@@ -22,10 +22,9 @@ import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@Data
public class PayableDetails {
public class PeriodDueDetails {
private final Money emi;
private final Money payablePrincipal;
private final Money payableInterest;
private final Money outstandingBalance;
private final Money duePrincipal;
private final Money dueInterest;
}
@@ -197,4 +197,21 @@ public class ProgressiveLoanInterestScheduleModel {
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
}
public Money getTotalDueInterest() {
return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest))
.reduce(getZero(), Money::plus);
}
public Money getTotalDuePrincipal() {
return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
.reduce(getZero(), Money::plus);
}
public Money getTotalPaidInterest() {
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(getZero(), Money::plus);
}
public Money getTotalPaidPrincipal() {
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus);
}
}
@@ -46,7 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -195,7 +195,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
continue;
}
Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate);
Money outstandingBalance = emiCalculator.getOutstandingLoanBalanceOfPeriod(interestScheduleModel, periodDueDate,
disbursementDate);
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal(), mc);
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
@@ -263,17 +264,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate,
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
PayableDetails result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate);
OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
// TODO: We should add all the past due outstanding amounts as well
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
.principal(result.getOutstandingBalance()) //
.interest(result.getPayableInterest());
installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
.forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));
.principal(result.getOutstandingPrincipal()) //
.interest(result.getOutstandingInterest());//
installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
@@ -25,7 +25,8 @@ import java.util.List;
import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
@@ -55,8 +56,10 @@ public interface EMICalculator {
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money principalAmount);
PayableDetails getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate payDate);
PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate);
Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate);
OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate);
}
@@ -35,11 +35,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
@Component
@@ -156,8 +158,63 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@Override
public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) {
public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate,
final LocalDate targetDate) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
repaymentPeriodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.count() > 1L;
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
}
return new PeriodDueDetails(repaymentPeriod.getEmi(), //
repaymentPeriod.getDuePrincipal(), //
repaymentPeriod.getDueInterest()); //
}
@Override
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
repaymentPeriodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
return repaymentPeriod.getOutstandingLoanBalance();
}
@Override
public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) {
MathContext mc = scheduleModel.mc();
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
scheduleModelCopy.repaymentPeriods().stream()//
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
.flatMap(rp -> rp.getInterestPeriods().stream()//
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
.reduce((one, two) -> two))
.ifPresent(ip -> ip.setDueDate(targetDate)); //
calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy);
scheduleModelCopy.repaymentPeriods()
.forEach(rp -> rp.getInterestPeriods().stream().filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> {
ip.setRateFactor(BigDecimal.ZERO);
ip.setRateFactorTillPeriodDueDate(BigDecimal.ZERO);
}));
calculateOutstandingBalance(scheduleModelCopy);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
Money totalOutstandingPrincipal = MathUtil
.negativeToZero(scheduleModelCopy.getTotalDuePrincipal().minus(scheduleModelCopy.getTotalPaidPrincipal()));
Money totalOutstandingInterest = MathUtil
.negativeToZero(scheduleModelCopy.getTotalDueInterest().minus(scheduleModelCopy.getTotalPaidInterest()));
return new OutstandingDetails(totalOutstandingPrincipal, totalOutstandingInterest);
}
@NotNull
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate repaymentPeriodDueDate, LocalDate targetDate) {
MathContext mc = scheduleModel.mc();
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream()
@@ -175,11 +232,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
}
scheduleModelCopy.repaymentPeriods().stream()
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()
.ifPresent(rp -> rp.getInterestPeriods().stream()
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate()))
.reduce((one, two) -> two).ifPresent(ip -> ip.setDueDate(targetDate)));
scheduleModelCopy.repaymentPeriods().stream()//
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
.flatMap(rp -> rp.getInterestPeriods().stream()//
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
.reduce((one, two) -> two))
.ifPresent(ip -> ip.setDueDate(targetDate)); //
interestPeriod.setDueDate(adjustedTargetDate);
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
@@ -188,19 +246,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateOutstandingBalance(scheduleModelCopy);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
boolean multiplePeriodIsUnpaid = scheduleModelCopy.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()).count() > 1L;
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
}
return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(),
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc));
}
@Override
public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate) {
return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance();
return scheduleModelCopy;
}
/**
@@ -1,146 +0,0 @@
/**
* 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);
}
}
@@ -35,7 +35,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
@@ -558,17 +558,14 @@ class ProgressiveEMICalculatorTest {
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
// schedule 1st period 1st day
PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 1, 1));
Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance()));
Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest()));
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1));
Assertions.assertEquals(17.01, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.0, toDouble(dueAmounts.getDueInterest()));
// schedule 2nd period last day
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest()));
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(16.52, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.49, toDouble(dueAmounts.getDueInterest()));
// pay off a period with balance correction
final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1);
@@ -576,11 +573,10 @@ class ProgressiveEMICalculatorTest {
final Money op1stCorrectionAmount = toMoney(16.77);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate,
op1stCorrectionDate);
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount);
emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24));
@@ -599,11 +595,10 @@ class ProgressiveEMICalculatorTest {
final Money op2ndCorrectionAmount = toMoney(16.42);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate,
final PeriodDueDetails repaymentDetails2st = emiCalculator.getDueAmounts(interestSchedule, op2ndCorrectionPeriodDueDate,
op2ndCorrectionDate);
Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance()));
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal()));
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest()));
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getDuePrincipal()));
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getDueInterest()));
emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount);
emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49));
@@ -619,18 +614,16 @@ class ProgressiveEMICalculatorTest {
// check numbers on last period due date
LocalDate periodDueDate = LocalDate.of(2024, 7, 1);
LocalDate payDate = LocalDate.of(2024, 7, 1);
final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate);
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest()));
final PeriodDueDetails repaymentDetails3rd = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate);
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getDuePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getDueInterest()));
// check numbers after the last period due date
periodDueDate = LocalDate.of(2024, 7, 1);
payDate = LocalDate.of(2024, 7, 15);
final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate);
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest()));
final PeriodDueDetails repaymentDetails4th = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate);
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getDuePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getDueInterest()));
// balance update on the last period, check the right interest interval split
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO));
@@ -678,25 +671,22 @@ class ProgressiveEMICalculatorTest {
final Money op1stCorrectionAmount = toMoney(15.0);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate,
op1stCorrectionDate);
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
PayableDetails details = null;
// check getPayableDetails forcast
details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
PeriodDueDetails details = null;
// check getdueAmounts forcast
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
// apply balance change and check again
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount);
details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(1.43));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(0.58));
@@ -749,21 +739,20 @@ class ProgressiveEMICalculatorTest {
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
// get remaining balance and dues on due date
PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 2, 1));
Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest()));
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1));
Assertions.assertEquals(16.43, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.58, toDouble(dueAmounts.getDueInterest()));
// check numbers on payoff date
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15));
Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest()));
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15));
Assertions.assertEquals(16.75, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.26, toDouble(dueAmounts.getDueInterest()));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26));
// check again numbers are zero
// payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
// dueAmounts = emiCalculator.getdueAmounts(interestSchedule, LocalDate.of(2024, 2, 1),
// LocalDate.of(2024, 2, 1));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
@@ -771,9 +760,9 @@ class ProgressiveEMICalculatorTest {
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1));
Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest()));
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1));
Assertions.assertEquals(15.21, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.5, toDouble(dueAmounts.getDueInterest()));
// check periods in model
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21);
@@ -1261,9 +1250,9 @@ class ProgressiveEMICalculatorTest {
private static void checkDailyInterest(final ProgressiveLoanInterestScheduleModel interestModel, final LocalDate repaymentPeriodDueDate,
final LocalDate interestStartDay, final int dayOffset, final double dailyInterest, final double interest) {
Money previousInterest = emiCalculator
.getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getPayableInterest();
Money currentInterest = emiCalculator.getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
.getPayableInterest();
.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getDueInterest();
Money currentInterest = emiCalculator.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
.getDueInterest();
Assertions.assertEquals(dailyInterest, toDouble(currentInterest.minus(previousInterest)));
Assertions.assertEquals(interest, toDouble(currentInterest));
@@ -100,15 +100,15 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), transactionsToReprocess, loan.getCurrency(),
installmentsToReprocess, loan.getActiveCharges());
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
payableInterest = installmentsToReprocess.stream() //
.map(installment -> emiCalculator //
.getPayableDetails(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
.getPayableInterest() //
.getDueAmounts(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
.getDueInterest() //
.getAmount()) //
.reduce(BigDecimal.ZERO, BigDecimal::add); //
}
@@ -5518,6 +5518,197 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC151: Validate Prepayment calculation till Pay-off date
// 1. Create a Loan product with Adv. Pment. Alloc. and with Declining Balance, 7% interest rate
// 2. Submit Loan, approve and Disburse
// 3. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
// 4. Pay 1st installment fully on the due date of the 1st period
// 5. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
// 6. Pay 2nd installment partially in the middle of 2nd period (15/02/2024)
// 7. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
// 8. Prepay the loan on 15/02/2024
@Test
public void uc151() {
String operationDate = "01 January 2024";
AtomicLong createdLoanId = new AtomicLong();
runAt(operationDate, () -> {
Long clientId = client.getClientId();
PostLoanProductsRequest product = create4IProgressive().interestRateFrequencyType(YEARS).numberOfRepayments(6)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//
.allowPartialPeriodInterestCalcualtion(false)//
.multiDisburseLoan(true)//
.maxTrancheCount(10)//
.disallowExpectedDisbursements(true)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
operationDate, 1000.0, 7.0, 6, null);
applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
.approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
// Before 1st disbursement date
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2023, 12, 31));
assertEquals(100.0f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// On 1st day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 1));
assertEquals(100.0f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// On 2nd day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 2));
assertEquals(100.02f, prepayAmounts.get("amount"));
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// On due date of 1st period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 1));
assertEquals(100.58f, prepayAmounts.get("amount"));
assertEquals(0.58f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// On the 1st day of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 2));
assertEquals(100.60f, prepayAmounts.get("amount"));
assertEquals(0.60f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// In the middle of 2nd period (15 Feb)
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 15));
assertEquals(100.86f, prepayAmounts.get("amount"));
assertEquals(0.86f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
// On the due date of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 3, 1));
assertEquals(101.16f, prepayAmounts.get("amount"));
assertEquals(1.16f, prepayAmounts.get("interestPortion"));
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
});
String repaymentDate = "01 February 2024";
runAt(repaymentDate, () -> {
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate(repaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(17.01));
// Before 1st disbursement date
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2023, 12, 31));
assertEquals(83.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// On 1st day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 1));
assertEquals(83.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// On 2nd day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 2));
assertEquals(83.57f, prepayAmounts.get("amount"));
assertEquals(0.00f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// On due date of 1st period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 1));
assertEquals(83.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// On the 1st day of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 2));
assertEquals(83.59f, prepayAmounts.get("amount"));
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// In the middle of 2nd period (15 Feb)
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 15));
assertEquals(83.81f, prepayAmounts.get("amount"));
assertEquals(0.24f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
// On the due date of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 3, 1));
assertEquals(84.06f, prepayAmounts.get("amount"));
assertEquals(0.49f, prepayAmounts.get("interestPortion"));
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
});
String secondRepaymentDate = "15 February 2024";
runAt(secondRepaymentDate, () -> {
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(5.0));
// Before 1st disbursement date
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2023, 12, 31));
assertEquals(78.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// On 1st day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 1));
assertEquals(78.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// On 2nd day
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 1, 2));
assertEquals(78.57f, prepayAmounts.get("amount"));
assertEquals(0.00f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// On due date of 1st period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 1));
assertEquals(78.57f, prepayAmounts.get("amount"));
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// On the 1st day of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 2));
assertEquals(78.59f, prepayAmounts.get("amount"));
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// In the middle of 2nd period (15 Feb)
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 2, 15));
assertEquals(78.81f, prepayAmounts.get("amount"));
assertEquals(0.24f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
// On the due date of 2nd period
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
LocalDate.of(2024, 3, 1));
assertEquals(79.05f, prepayAmounts.get("amount"));
assertEquals(0.48f, prepayAmounts.get("interestPortion"));
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(78.81));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
@@ -867,8 +867,8 @@ public abstract class BaseLoanIntegrationTest {
return postLoansRequest;
}
protected PostLoansRequest applyPin4ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate,
Double amount, Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
protected PostLoansRequest applyLP2ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId)
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).productId(loanProductId)
@@ -912,7 +912,7 @@ public abstract class BaseLoanIntegrationTest {
protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyPin4ProgressiveLoanRequest(clientId, loanProductId,
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId,
loanDisbursementDate, amount, interestRate, numberOfRepayments, customizer));
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
@@ -44,7 +44,7 @@ public class LoanPayOffTest extends BaseLoanIntegrationTest {
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
loanId = postLoansResponse.getLoanId();
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024");
@@ -318,7 +318,7 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(
create4IProgressive().supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem(refundType));
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
Long loanId = postLoansResponse.getLoanId();
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 June 2024");
@@ -44,7 +44,7 @@ public class LoanTransactionBackdatedProgressiveTest extends BaseLoanIntegration
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
loanId = postLoansResponse.getLoanId();
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024");
@@ -1488,6 +1488,15 @@ public class LoanTransactionHelper extends IntegrationTest {
return response;
}
public HashMap getPrepayAmount(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanID,
final LocalDate transactionDate) {
final String URL = "/fineract-provider/api/v1/loans/" + loanID
+ "/transactions/template?command=prepayLoan&locale=en&dateFormat=yyyy-MM-dd&transactionDate=" + transactionDate + "&"
+ Utils.TENANT_IDENTIFIER;
final HashMap response = Utils.performServerGet(requestSpec, responseSpec, URL, "");
return response;
}
private String createLoanRefundTransferURL() {
return "/fineract-provider/api/v1/accounttransfers/refundByTransfer?tenantIdentifier=default";
}