FINERACT-2114: Interest rate modification - Advanced payment allocation processor support
This commit is contained in:
committed by
Adam Saghy
parent
3e64cc1536
commit
94360c9f8a
+5
@@ -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);
|
||||
}
|
||||
|
||||
+5
-3
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+52
-15
@@ -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) {
|
||||
|
||||
+37
-17
@@ -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;
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
|
||||
+20
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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() {
|
||||
|
||||
+30
-36
@@ -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();
|
||||
|
||||
+24
-15
@@ -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());//
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+159
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
-127
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+169
-131
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+119
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user