FINERACT-2114: Interest rate modification - Advanced payment allocation processor support

This commit is contained in:
Janos Meszaros
2024-10-29 12:24:23 +01:00
committed by Adam Saghy
parent 3e64cc1536
commit 94360c9f8a
16 changed files with 633 additions and 363 deletions
@@ -79,6 +79,11 @@ public final class DateUtils {
return truncate == null ? now : now.truncatedTo(truncate);
}
@NotNull
public static OffsetDateTime getOffsetDateTimeOfTenantFromLocalDate(@NotNull final LocalDate date) {
return OffsetDateTime.of(date.atStartOfDay(), getOffsetDateTimeOfTenant().getOffset());
}
public static LocalDateTime getLocalDateTimeOfSystem() {
return getLocalDateTimeOfSystem(null);
}
@@ -26,9 +26,11 @@ public enum LoanTermVariationType {
PRINCIPAL_AMOUNT(3, "loanTermType.principalAmount"), //
DUE_DATE(4, "loanTermType.dueDate"), //
INSERT_INSTALLMENT(5, "loanTermType.insertInstallment"), //
DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), GRACE_ON_PRINCIPAL(8,
"loanTermType.graceOnPrincipal"), EXTEND_REPAYMENT_PERIOD(9,
"loanTermType.extendRepaymentPeriod"), INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment");
DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), //
GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), //
GRACE_ON_PRINCIPAL(8, "loanTermType.graceOnPrincipal"), //
EXTEND_REPAYMENT_PERIOD(9, "loanTermType.extendRepaymentPeriod"), //
INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); //
private final Integer value;
private final String code;
@@ -478,7 +478,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
&& disbursementDate.equals(loanRepaymentScheduleInstallment.getObligationsMetOnDate());
}
private boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) {
protected boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) {
return !loanRepaymentScheduleInstallment.isObligationsMet() && loanRepaymentScheduleInstallment.getObligationsMetOnDate() == null;
}
@@ -63,6 +63,8 @@ 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;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -71,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
@@ -169,8 +172,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
currentInstallment.updateObligationsMet(currency, disbursementDate);
}
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
final Loan loan = loanTransactions.get(0).getLoan();
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
@@ -180,17 +181,26 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
LoanTermVariationsDataWrapper loanTermVariations = Optional
.ofNullable(loan.getActiveLoanTermVariations()).map(loanTermVariationsSet -> loanTermVariationsSet.stream()
.map(LoanTermVariations::toData).collect(Collectors.toCollection(ArrayList::new)))
.map(LoanTermVariationsDataWrapper::new).orElse(null);
List<ChangeOperation> changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges);
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
if (chargeOrTransaction.isTransaction()) {
LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get();
for (final ChangeOperation changeOperation : changeOperations) {
if (changeOperation.isInterestRateChange()) {
final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get();
processInterestRateChange(installments, interestRateChange, scheduleModel);
} else if (changeOperation.isTransaction()) {
LoanTransaction transaction = changeOperation.getLoanTransaction().get();
processSingleTransaction(transaction, ctx);
transaction = getProcessedTransaction(changedTransactionDetail, transaction);
if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT
overpaidTransactions.add(transaction);
}
} else {
LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get();
LoanCharge loanCharge = changeOperation.getLoanCharge().get();
processSingleCharge(loanCharge, currency, installments, disbursementDate);
if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) {
overpaidTransactions = processOverpaidTransactions(overpaidTransactions, currency, installments, charges,
@@ -205,13 +215,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createNewTransaction(oldTransaction, newTransaction, ctx);
}
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
List<LoanTransaction> txs = chargeOrTransactions.stream() //
.filter(ChargeOrTransaction::isTransaction) //
List<LoanTransaction> txs = changeOperations.stream() //
.filter(ChangeOperation::isTransaction) //
.map(e -> e.getLoanTransaction().get()).toList();
reprocessInstallments(disbursementDate, txs, installments, currency);
return Pair.of(changedTransactionDetail, scheduleModel);
}
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate);
processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments);
}
private void processInterestRateChangeOnInstallments(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate interestRateChangeSubmittedOnDate, final List<LoanRepaymentScheduleInstallment> installments) {
installments.stream() //
.filter(installment -> isNotObligationsMet(installment)
&& !interestRateChangeSubmittedOnDate.isAfter(installment.getDueDate()))
.forEach(installment -> updateInstallmentIfInterestPeriodPresent(scheduleModel, installment)); //
}
private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanRepaymentScheduleInstallment installment) {
emiCalculator.findRepaymentPeriod(scheduleModel, installment.getDueDate()).ifPresent(interestRepaymentPeriod -> {
installment.updateInterestCharged(interestRepaymentPeriod.getDueInterest().getAmount());
installment.updatePrincipal(interestRepaymentPeriod.getDuePrincipal().getAmount());
});
}
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
@@ -736,17 +770,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
@NotNull
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
Set<LoanCharge> charges) {
List<ChargeOrTransaction> chargeOrTransactions = new ArrayList<>();
private List<ChangeOperation> createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations,
final List<LoanTransaction> loanTransactions, final Set<LoanCharge> charges) {
List<ChangeOperation> changeOperations = new ArrayList<>();
if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) {
changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList());
}
if (charges != null) {
chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList());
changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList());
}
if (loanTransactions != null) {
chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList());
changeOperations.addAll(loanTransactions.stream().map(ChangeOperation::new).toList());
}
Collections.sort(chargeOrTransactions);
return chargeOrTransactions;
Collections.sort(changeOperations);
return changeOperations;
}
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
@@ -24,26 +24,40 @@ import java.time.OffsetDateTime;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.jetbrains.annotations.NotNull;
@Getter
public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
public class ChangeOperation implements Comparable<ChangeOperation> {
private final Optional<LoanTermVariationsData> interestRateChange;
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChargeOrTransaction(LoanCharge loanCharge) {
public ChangeOperation(LoanCharge loanCharge) {
this.interestRateChange = Optional.empty();
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChargeOrTransaction(LoanTransaction loanTransaction) {
public ChangeOperation(LoanTransaction loanTransaction) {
this.interestRateChange = Optional.empty();
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
public ChangeOperation(LoanTermVariationsData interestRateChange) {
this.interestRateChange = Optional.of(interestRateChange);
this.loanTransaction = Optional.empty();
this.loanCharge = Optional.empty();
}
public boolean isInterestRateChange() {
return interestRateChange.isPresent();
}
public boolean isTransaction() {
return loanTransaction.isPresent();
}
@@ -52,8 +66,18 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
return loanCharge.isPresent();
}
private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
}
private LocalDate getEffectiveDate() {
if (loanCharge.isPresent()) {
if (interestRateChange.isPresent()) {
return getSubmittedOnDate();
} else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
return loanCharge.get().getDueDate();
} else {
@@ -66,16 +90,10 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
}
}
private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
}
private LocalDate getSubmittedOnDate() {
if (loanCharge.isPresent()) {
if (interestRateChange.isPresent()) {
return interestRateChange.get().getTermVariationApplicableFrom();
} else if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getSubmittedOnDate();
@@ -85,7 +103,9 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
}
private OffsetDateTime getCreatedDateTime() {
if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
if (interestRateChange.isPresent()) {
return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate());
} else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getCreatedDateTime();
@@ -96,16 +116,16 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
@Override
@SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals")
public int compareTo(@NotNull ChargeOrTransaction o) {
public int compareTo(@NotNull ChangeOperation o) {
int datePortion = DateUtils.compare(this.getEffectiveDate(), o.getEffectiveDate());
if (datePortion == 0) {
boolean isAccrual = isAccrualActivity();
final boolean isAccrual = isAccrualActivity();
if (isAccrual != o.isAccrualActivity()) {
return isAccrual ? 1 : -1;
}
int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate());
if (submittedDate == 0) {
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime());
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime(), null);
}
return submittedDate;
}
@@ -22,9 +22,9 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import org.jetbrains.annotations.NotNull;
public record InterestRate(LocalDate effectiveFrom, //
LocalDate validFrom, //
BigDecimal interestRate //
public record InterestRate(//
LocalDate effectiveFrom, //
BigDecimal interestRate//
) implements Comparable<InterestRate> {
@Override
@@ -23,8 +23,10 @@ import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;
import java.util.function.Consumer;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -36,7 +38,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
public class ProgressiveLoanInterestScheduleModel {
private final List<RepaymentPeriod> repaymentPeriods;
private final List<InterestRate> interestRates;
private final TreeSet<InterestRate> interestRates;
private final LoanProductRelatedDetail loanProductRelatedDetail;
private final Integer installmentAmountInMultiplesOf;
private MathContext mc;
@@ -44,17 +46,17 @@ public class ProgressiveLoanInterestScheduleModel {
public ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, MathContext mc) {
this.repaymentPeriods = repaymentPeriods;
this.interestRates = new ArrayList<>();
this.interestRates = new TreeSet<>(Collections.reverseOrder());
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.mc = mc;
}
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final List<InterestRate> interestRates,
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
this.mc = mc;
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods);
this.interestRates = new ArrayList<>(interestRates);
this.interestRates = new TreeSet<>(interestRates);
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
}
@@ -80,8 +82,15 @@ public class ProgressiveLoanInterestScheduleModel {
}
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)).map(InterestRate::interestRate).findFirst()
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate());
return interestRates.stream() //
.filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) //
.map(InterestRate::interestRate) //
.findFirst() //
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); //
}
public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigDecimal newInterestRate) {
interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate));
}
public Optional<RepaymentPeriod> findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) {
@@ -128,11 +137,12 @@ public class ProgressiveLoanInterestScheduleModel {
}
return repaymentPeriods.stream()//
.filter(repaymentPeriod -> {
if (repaymentPeriod.getPrevious().isPresent()) {
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
if (isFirstPeriod) {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
} else {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
}
})//
@@ -183,7 +193,7 @@ public class ProgressiveLoanInterestScheduleModel {
repaymentPeriod.getInterestPeriods().add(interestPeriod);
}
private Money getZero() {
public Money getZero() {
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
}
}
@@ -127,8 +127,12 @@ public class RepaymentPeriod {
return getEmi().minus(getCalculatedDueInterest(), mc);
}
public Money getTotalPaidAmount() {
return getPaidPrincipal().plus(getPaidInterest());
}
public boolean isFullyPaid() {
return getEmi().isEqualTo(getPaidPrincipal().plus(getPaidInterest()));
return getEmi().isEqualTo(getTotalPaidAmount());
}
public Money getDueInterest() {
@@ -35,7 +35,6 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -61,27 +60,19 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator;
private final EMICalculator emiCalculator;
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
}
@Override
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
// generate list of proposed schedule due dates
LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
if (loanApplicationTerms.getLoanTermVariations() != null) {
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
.fetchLoanTermDueDateVariationsData(loanEndDate);
if (lastDueDateVariation != null) {
loanEndDate = lastDueDateVariation.getDateValue();
}
}
// determine the total charges due at time of disbursement
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
// setup variables for tracking important facts required for loan
// schedule generation.
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
? loanApplicationTerms.getExpectedDisbursementDate()
@@ -94,6 +85,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
// set and handled separately after all installments generated
final Set<LoanCharge> nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges);
// generate list of proposed schedule due dates
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, holidayDetailDTO);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
@@ -102,29 +94,17 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
final ArrayList<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
final List<DisbursementData> disbursementDataList = getSortedDisbursementList(loanApplicationTerms);
for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) {
scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate());
scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate());
// in same repayment period the logic firstly applies interest rate changes and just after the disbursements
applyInterestRateChangesOnPeriod(loanApplicationTerms, repaymentPeriod, interestScheduleModel);
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement, false, mc);
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
if (loanApplicationTerms.getLoanTermVariations() != null) {
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate);
}
}
}
emiCalculator.findRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate()).ifPresent(interestRepaymentPeriod -> {
final Money principalDue = interestRepaymentPeriod.getDuePrincipal();
final Money interestDue = interestRepaymentPeriod.getDueInterest();
@@ -166,10 +146,24 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
}
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
private List<DisbursementData> getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) {
final List<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
return disbursementDataList;
}
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
private void applyInterestRateChangesOnPeriod(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleModelRepaymentPeriod repaymentPeriod, final ProgressiveLoanInterestScheduleModel interestScheduleModel) {
if (loanApplicationTerms.getLoanTermVariations() != null) {
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate);
}
}
}
}
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
@@ -181,10 +175,10 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
}
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms,
final ArrayList<DisbursementData> disbursementDataList, final LoanScheduleParams scheduleParams,
final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List<LoanScheduleModelPeriod> periods,
final BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final List<DisbursementData> disbursementDataList,
final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel,
final List<LoanScheduleModelPeriod> periods, final BigDecimal chargesDueAtTimeOfDisbursement,
final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
for (DisbursementData disbursementData : disbursementDataList) {
final LocalDate disbursementDate = disbursementData.disbursementDate();
@@ -81,16 +81,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency(), scheduleModel.mc()))
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, scheduleModel.getZero())
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, disbursementDueDate), scheduleModel));
}
private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel,
final RepaymentPeriod changedRepaymentPeriod, final LocalDate disbursementDueDate) {
final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(disbursementDueDate);
final RepaymentPeriod changedRepaymentPeriod, final LocalDate operationDueDate) {
final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(operationDueDate);
if (isRelatedToNextRepaymentPeriod) {
final Optional<RepaymentPeriod> nextRepaymentPeriod = scheduleModel.repaymentPeriods().stream()
.filter(repaymentPeriod -> changedRepaymentPeriod.equals(repaymentPeriod.getPrevious().orElse(null))).findFirst();
@@ -106,14 +104,20 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
final BigDecimal newInterestRate) {
// TODO: impl
final LocalDate interestRateChangeEffectiveDate = newInterestSubmittedOnDate.minusDays(1);
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, newInterestRate);
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.getZero(),
scheduleModel.getZero())
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel));
}
@Override
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount) {
final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency());
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount)
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.getZero(), balanceCorrectionAmount)
.ifPresent(repaymentPeriod -> {
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
@@ -186,10 +190,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
@@ -229,7 +231,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
if (shouldBeAdjusted) {
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc);
long uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount()))
.count();
Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc);
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
originalEmi.plus(adjustment, mc));
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
@@ -238,7 +242,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate();
final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy(mc);
newScheduleModel.repaymentPeriods().forEach(period -> {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)
&& !adjustedEqualMonthlyInstallmentValue.isLessThan(period.getTotalPaidAmount())) {
period.setEmi(adjustedEqualMonthlyInstallmentValue);
}
});
@@ -393,7 +398,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc), mc);
final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment));
repaymentPeriods.forEach(period -> {
if (!finalEqualMonthlyInstallment.isLessThan(period.getTotalPaidAmount())) {
period.setEmi(finalEqualMonthlyInstallment);
}
});
}
Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
@@ -571,7 +580,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());//
}
/**
@@ -585,7 +594,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());//
}
/**
@@ -0,0 +1,159 @@
/**
* 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.domain.transactionprocessor.impl;
import com.google.common.collect.Collections2;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class ChangeOperationTest {
// DateUtils.getOffsetDateTimeOfTenantFromLocalDate
private static final MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
@BeforeAll
public static void init() {
threadLocalContextUtil.when(ThreadLocalContextUtil::getTenant)
.thenReturn(new FineractPlatformTenant(null, null, null, "Europe/Budapest", null));
}
@AfterAll
public static void tearDown() {
threadLocalContextUtil.close();
}
@Test
public void testCompareToEqual() {
ChangeOperation interestChange = createInterestRateChange("2023-10-17");
ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(interestChange.compareTo(transaction) < 0);
Assertions.assertTrue(interestChange.compareTo(charge) < 0);
Assertions.assertTrue(charge.compareTo(transaction) == 0);
Assertions.assertTrue(transaction.compareTo(charge) == 0);
}
@Test
public void testCompareToEqualBackdatedCharge() {
ChangeOperation interestChange = createInterestRateChange("2023-10-17");
ChangeOperation charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(interestChange.compareTo(transaction) > 0);
Assertions.assertTrue(interestChange.compareTo(charge) > 0);
Assertions.assertTrue(charge.compareTo(transaction) == 0);
Assertions.assertTrue(transaction.compareTo(charge) == 0);
}
@Test
public void testCompareToCreatedDateTime() {
ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00");
ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testCompareToSubmittedOnDate() {
ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testComparatorEffectiveDate() {
ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testComparatorOnDifferentSubmittedDay() {
ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00");
ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00");
Collection<List<ChangeOperation>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChangeOperation> expected = List.of(cot1, cot3, cot2);
for (List<ChangeOperation> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
@Test
public void testComparatorOnSameDayBackdatedCharge() {
ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00");
ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00");
ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00");
Collection<List<ChangeOperation>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChangeOperation> expected = List.of(cot1, cot3, cot2);
for (List<ChangeOperation> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
@Test
public void testComparatorOnSameDay() {
ChangeOperation cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00");
ChangeOperation cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00");
ChangeOperation cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00");
Collection<List<ChangeOperation>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChangeOperation> expected = List.of(cot1, cot3, cot2);
for (List<ChangeOperation> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
private ChangeOperation createInterestRateChange(String submittedDate) {
LoanTermVariationsData interestRateChange = Mockito.mock(LoanTermVariationsData.class);
Mockito.when(interestRateChange.getTermVariationApplicableFrom()).thenReturn(LocalDate.parse(submittedDate));
return new ChangeOperation(interestRateChange);
}
private ChangeOperation createCharge(String effectiveDate, String submittedDate, String creationDateTime) {
LoanCharge charge = Mockito.mock(LoanCharge.class);
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime)));
return new ChangeOperation(charge);
}
private ChangeOperation createTransaction(String transactionDate, String submittedDate, String creationDateTime) {
LoanTransaction transaction = Mockito.mock(LoanTransaction.class);
Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate));
Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime));
return new ChangeOperation(transaction);
}
}
@@ -1,127 +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.domain.transactionprocessor.impl;
import com.google.common.collect.Collections2;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class ChargeOrTransactionTest {
@Test
public void testCompareToEqual() {
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) == 0);
Assertions.assertTrue(transaction.compareTo(charge) == 0);
}
@Test
public void testCompareToEqualBackdatedCharge() {
ChargeOrTransaction charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) == 0);
Assertions.assertTrue(transaction.compareTo(charge) == 0);
}
@Test
public void testCompareToCreatedDateTime() {
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00");
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testCompareToSubmittedOnDate() {
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testComparatorEffectiveDate() {
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
Assertions.assertTrue(charge.compareTo(transaction) > 0);
Assertions.assertTrue(transaction.compareTo(charge) < 0);
}
@Test
public void testComparatorOnDifferentSubmittedDay() {
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00");
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00");
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
for (List<ChargeOrTransaction> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
@Test
public void testComparatorOnSameDayBackdatedCharge() {
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00");
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00");
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00");
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
for (List<ChargeOrTransaction> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
@Test
public void testComparatorOnSameDay() {
ChargeOrTransaction cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00");
ChargeOrTransaction cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00");
ChargeOrTransaction cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00");
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
for (List<ChargeOrTransaction> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) {
LoanCharge charge = Mockito.mock(LoanCharge.class);
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime)));
return new ChargeOrTransaction(charge);
}
private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) {
LoanTransaction transaction = Mockito.mock(LoanTransaction.class);
Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate));
Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime));
return new ChargeOrTransaction(transaction);
}
}
@@ -44,7 +44,6 @@ import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -95,13 +94,12 @@ class ProgressiveEMICalculatorTest {
final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()));
final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate()));
final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate()));
final BigDecimal rateFactor = emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear,
daysInPeriod, daysInPeriod, mc);
return rateFactor.setScale(12, MoneyHelper.getRoundingMode());
return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod,
daysInPeriod, mc);
}
@Test
public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() {
public void test_rateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() {
// Given
final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
@@ -116,7 +114,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() {
public void test_rateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() {
// Given
final DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
@@ -132,7 +130,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() {
public void test_fnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() {
// Given
final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
@@ -159,7 +157,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculator_generateInterestScheduleModel() {
public void test_generateInterestScheduleModel() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
final Integer installmentAmountInMultiplesOf = null;
@@ -184,7 +182,7 @@ class ProgressiveEMICalculatorTest {
@Test
@Timeout(1) // seconds
public void testEMICalculation_performance() {
public void test_emi_calculator_performance() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -223,19 +221,19 @@ class ProgressiveEMICalculatorTest {
List<RepaymentPeriod> repaymentPeriods = interestSchedule.repaymentPeriods();
for (int i = 0; i < repaymentPeriods.size(); i++) {
final RepaymentPeriod repaymentPeriod = repaymentPeriods.get(i);
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal().getAmount()));
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest().getAmount()));
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal()));
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest()));
if (i == repaymentPeriods.size() - 1) {
Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance()));
} else {
Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi().getAmount()));
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi()));
Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance()));
}
}
}
@Test
public void testEMICalculation_CheckEmiButNewEmiNotBetterThanOriginal() {
public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -272,7 +270,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -283,7 +281,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -296,7 +294,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66);
@@ -309,7 +307,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() {
public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -356,7 +354,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule() {
public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -367,7 +365,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -380,7 +378,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66);
@@ -392,9 +390,8 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
}
@Disabled("till interest rate change got implemented")
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0201_4per() {
public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -405,7 +402,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("7");
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -420,10 +417,10 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
final BigDecimal interestRateNewValue = new BigDecimal("4");
final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0);
final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2);
emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue);
@@ -436,9 +433,8 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 5, 0, 16.89, 0.003333333333, 0.06, 16.83, 0.0);
}
@Disabled("till interest rate change got implemented")
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0215_4per() {
public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -449,7 +445,54 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("7");
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
emiCalculator.payInterest(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1), toMoney(16.90));
final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0);
final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2);
emiCalculator.changeInterestRate(interestModel, interestChangeDate, interestRateNewValue);
checkPeriod(interestModel, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 66.67);
checkPeriod(interestModel, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 66.67);
checkPeriod(interestModel, 1, 0, 17.01, 0.003333333333, 0.22, 0.0, 17.01, 66.56);
checkPeriod(interestModel, 2, 0, 16.85, 0.003333333333, 0.22, 0.44, 16.41, 50.15);
checkPeriod(interestModel, 3, 0, 16.85, 0.003333333333, 0.17, 16.68, 33.47);
checkPeriod(interestModel, 4, 0, 16.85, 0.003333333333, 0.11, 16.74, 16.73);
checkPeriod(interestModel, 5, 0, 16.79, 0.003333333333, 0.06, 16.73, 0.0);
}
@Test
public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -464,14 +507,15 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
final BigDecimal interestRateNewValue = new BigDecimal("4");
final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0);
final LocalDate interestChangeDate = LocalDate.of(2024, 2, 15);
emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 16.90, 0.002614942529, 0.22, 0.37, 16.53, 67.04);
checkPeriod(interestSchedule, 1, 1, 16.90, 0.001839080460, 0.15, 0.37, 16.53, 67.04);
checkPeriod(interestSchedule, 2, 0, 16.90, 0.003333333333, 0.22, 16.68, 50.36);
@@ -484,7 +528,7 @@ class ProgressiveEMICalculatorTest {
* This test case tests a period early and late repayment with balance correction
*/
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_correction_on0215() {
public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -495,7 +539,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("7");
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -510,37 +554,36 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
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().getAmount()));
Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest().getAmount()));
Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance()));
Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest()));
// 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().getAmount()));
Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest().getAmount()));
Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest()));
// pay off a period with balance correction
final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1);
final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15);
final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.77));
final Money op1stCorrectionAmount = toMoney(16.77);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
op1stCorrectionDate);
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount()));
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount);
emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate,
Money.of(monetaryCurrency, BigDecimal.valueOf(0.24)));
emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24));
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 66.80);
@@ -553,18 +596,17 @@ class ProgressiveEMICalculatorTest {
// totally pay off another period with balance correction
final LocalDate op2ndCorrectionPeriodDueDate = LocalDate.of(2024, 4, 1);
final LocalDate op2ndCorrectionDate = LocalDate.of(2024, 3, 1);
final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.42));
final Money op2ndCorrectionAmount = toMoney(16.42);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate,
op2ndCorrectionDate);
Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance().getAmount()));
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest().getAmount()));
Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance()));
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal()));
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest()));
emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount);
emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate,
Money.of(monetaryCurrency, BigDecimal.valueOf(0.49)));
emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49));
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 50.38);
@@ -578,17 +620,17 @@ class ProgressiveEMICalculatorTest {
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().getAmount()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest().getAmount()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest()));
// 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().getAmount()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest().getAmount()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance()));
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal()));
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest()));
// 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));
@@ -601,7 +643,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0215() {
public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -612,7 +654,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("7");
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -627,51 +669,43 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
// partially pay off a period with balance correction
final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1);
final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15);
final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(15.0));
final Money op1stCorrectionAmount = toMoney(15.0);
// get remaining balance and dues for a date
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
op1stCorrectionDate);
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount()));
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
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().getAmount()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount()));
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
// 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().getAmount()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount()));
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(1.43)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(0.58)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(16.77)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(0.24)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(17.01)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(17.01)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(17.01)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15),
Money.of(monetaryCurrency, BigDecimal.valueOf(15.77)));
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));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(16.77));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(0.24));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), toMoney(17.01));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), toMoney(17.01));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), toMoney(17.01));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), toMoney(15.77));
// check periods in model
@@ -685,7 +719,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0115() {
public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -696,7 +730,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("7");
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -717,13 +751,13 @@ class ProgressiveEMICalculatorTest {
// 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().getAmount()));
Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest().getAmount()));
Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest()));
// 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().getAmount()));
Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest().getAmount()));
Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest()));
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));
@@ -738,8 +772,8 @@ class ProgressiveEMICalculatorTest {
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().getAmount()));
Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest().getAmount()));
Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal()));
Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest()));
// check periods in model
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21);
@@ -765,7 +799,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -776,7 +810,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -789,7 +823,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
Money disbursedAmount = toMoney(100);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66);
@@ -800,7 +834,7 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(200));
disbursedAmount = toMoney(200.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 51.33, 0.0, 0.0, 2.02, 49.31, 250.69);
@@ -814,7 +848,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -825,7 +859,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -838,7 +872,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66);
@@ -849,7 +883,7 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 15), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66);
@@ -863,7 +897,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() {
public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
@@ -874,7 +908,7 @@ class ProgressiveEMICalculatorTest {
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -887,14 +921,14 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount);
disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(50));
disbursedAmount = toMoney(50.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);
// add disbursement on same date
disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(25));
disbursedAmount = toMoney(25.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21);
@@ -908,7 +942,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() {
public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)),
@@ -918,7 +952,7 @@ class ProgressiveEMICalculatorTest {
repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 1)),
repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 1)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -931,7 +965,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.80, 16.33, 83.67);
@@ -944,7 +978,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() {
public void test_disbursedAmt1000_NoInterest_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
@@ -952,7 +986,7 @@ class ProgressiveEMICalculatorTest {
repayment(4, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
final BigDecimal interestRate = new BigDecimal("0");
final BigDecimal interestRate = BigDecimal.ZERO;
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -965,7 +999,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
final Money disbursedAmount = toMoney(1000.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 250.0, 0.0, 0.0, 250.0, 750.0);
@@ -975,7 +1009,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() {
public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)),
@@ -985,7 +1019,7 @@ class ProgressiveEMICalculatorTest {
repayment(5, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 5)),
repayment(6, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -998,7 +1032,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 16.77, 0.0, 0.0, 0.18, 16.59, 83.41);
@@ -1011,14 +1045,14 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() {
public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)),
repayment(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)),
repayment(3, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -1031,7 +1065,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 33.57, 0.0, 0.0, 0.36, 33.21, 66.79);
@@ -1041,7 +1075,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)),
@@ -1051,7 +1085,7 @@ class ProgressiveEMICalculatorTest {
repayment(5, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)),
repayment(6, LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31)));
final BigDecimal interestRate = new BigDecimal("9.4822");
final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
@@ -1064,7 +1098,7 @@ class ProgressiveEMICalculatorTest {
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 16.90, 0.0, 0.0, 0.40, 16.50, 83.50);
@@ -1105,16 +1139,20 @@ class ProgressiveEMICalculatorTest {
final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx);
final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx);
Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi().getAmount()));
Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi()));
Assertions.assertEquals(rateFactor, toDouble(applyMathContext(interestPeriod.getRateFactor())));
Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest().getAmount()));
Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest().getAmount()));
Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal().getAmount()));
Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest()));
Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest()));
Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal()));
Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance()));
}
private static double toDouble(final Money value) {
return value == null ? 0.0 : toDouble(value.getAmount());
}
private static double toDouble(final BigDecimal value) {
return value == null ? 0 : value.doubleValue();
return value == null ? 0.0 : value.doubleValue();
}
private static BigDecimal applyMathContext(final BigDecimal value) {
@@ -35,7 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChargeOrTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.springframework.stereotype.Component;
@@ -145,6 +145,6 @@ public class LoanReAgingValidator {
}
private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) {
return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0;
return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0;
}
}
@@ -34,7 +34,7 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConsta
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChargeOrTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.springframework.stereotype.Component;
@@ -130,6 +130,6 @@ public class LoanReAmortizationValidator {
}
private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) {
return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0;
return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0;
}
}
@@ -83,6 +83,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -5213,6 +5214,124 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC14-: Advanced payment allocation with Interest, Interest Recalculation Enabled
// Interest Calculation Period Type: daily
// Use original EMI and apply disbursement and change Interest Rate
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Payment. Alloc. and with 7% Interest, 360/30, 1 repayment per month
// 2. Submit Loan and approve
// 3. Disburse
// 4. Validate Repayment Schedule
// 5. Reschedule Interest Rate to 4%
// 6. Validate Repayment Schedule
@Test
public void uc147d() {
final String operationDate = "1 January 2024";
runAt(operationDate, () -> {
BigDecimal interestRatePerPeriod = BigDecimal.valueOf(7.0);
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestRateFrequencyType(YEARS)//
.daysInMonthType(DaysInMonthType.DAYS_30)//
.daysInYearType(DaysInYearType.DAYS_360)//
.numberOfRepayments(6)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
.repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())//
.enableDownPayment(false)//
.allowPartialPeriodInterestCalcualtion(null)//
.enableAutoRepaymentForDownPayment(null)//
.isInterestRecalculationEnabled(true)//
.interestRecalculationCompoundingMethod(0)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.multiDisburseLoan(true)//
.disallowExpectedDisbursements(true)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType("flat")//
.overAppliedNumber(10000)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0)//
.installmentAmountInMultiplesOf(null)//
.rescheduleStrategyMethod(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue())//
.recalculationRestFrequencyType(1);//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6);
applicationRequest = applicationRequest.numberOfRepayments(6)//
.loanTermFrequency(6)//
.loanTermFrequencyType(2)//
.interestRatePerPeriod(interestRatePerPeriod)//
.interestCalculationPeriodType(DAYS)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.repaymentEvery(1)//
.repaymentFrequencyType(2)//
.maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))//
;//
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()//
.approvedLoanAmount(BigDecimal.valueOf(100))//
.approvedOnDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));//
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()//
.transactionAmount(BigDecimal.valueOf(100.0))//
.actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));//
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 102.05, 0.0, 100.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 0.0, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.0,
0.58, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.52, 0.0, 16.52, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.49, 0.0,
0.49, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.62, 0.0, 16.62, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.39, 0.0,
0.39, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.72, 0.0, 16.72, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.29, 0.0,
0.29, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.81, 0.0, 16.81, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20, 0.0,
0.20, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.90, 0.0, 16.90, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.10, 0.0,
0.10, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
assertEquals(loanDetails.getNumberOfRepayments(), 6);
updateBusinessDate("1 February 2024");
loanTransactionHelper.makeRepayment("1 February 2024", 17.01f, loanResponse.getLoanId().intValue());
updateBusinessDate("14 February 2024");
PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper//
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()//
.loanId(loanDetails.getId())//
.rescheduleReasonId(1L)//
.rescheduleFromDate("15 February 2024").dateFormat(DATETIME_PATTERN).locale("en")//
.submittedOnDate("14 February 2024")//
.newInterestRate(BigDecimal.valueOf(4.0)));//
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), //
new PostUpdateRescheduleLoansRequest()//
.approvedOnDate("14 February 2024").locale("en").dateFormat(DATETIME_PATTERN));//
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 84.5, 17.01, 83.57, 16.43, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.58,
0.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.53, 0.0, 16.53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.37, 0.0,
0.37, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.68, 0.0, 16.68, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22, 0.0,
0.22, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.73, 0.0, 16.73, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17, 0.0,
0.17, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.79, 0.0, 16.79, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11, 0.0,
0.11, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.84, 0.0, 16.84, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06, 0.0,
0.06, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
});
}
// uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid