FINERACT-1981: Remove hardcoded MoneyHelper dependency during progressive loan

This commit is contained in:
Andrii Kulminskyi
2024-10-22 18:45:28 +02:00
committed by Adam Saghy
parent 806ee69798
commit a9ecdf97a4
31 changed files with 536 additions and 355 deletions
@@ -359,6 +359,11 @@ public final class MathUtil {
return value == null || isGreaterThanZero(value) ? value : Money.zero(value.getCurrency());
}
/** @return parameter value or ZERO if it is negative */
public static Money negativeToZero(Money value, MathContext mc) {
return value == null || isGreaterThanZero(value, mc) ? value : Money.zero(value.getCurrency(), mc);
}
public static boolean isEmpty(Money value) {
return value == null || value.isZero();
}
@@ -367,6 +372,10 @@ public final class MathUtil {
return value != null && value.isGreaterThanZero();
}
public static boolean isGreaterThanZero(Money value, MathContext mc) {
return value != null && value.isGreaterThanZero(mc);
}
public static boolean isLessThanZero(Money value) {
return value != null && value.isLessThanZero();
}
@@ -387,10 +396,18 @@ public final class MathUtil {
return first == null ? second : second == null ? first : first.plus(second);
}
public static Money plus(Money first, Money second, MathContext mc) {
return first == null ? second : second == null ? first : first.plus(second, mc);
}
public static Money plus(Money first, Money second, Money third) {
return plus(plus(first, second), third);
}
public static Money plus(Money first, Money second, Money third, MathContext mc) {
return plus(plus(first, second), third, mc);
}
public static Money plus(Money first, Money second, Money third, Money fourth) {
return plus(plus(plus(first, second), third), fourth);
}
@@ -21,11 +21,14 @@ package org.apache.fineract.organisation.monetary.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Iterator;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@Embeddable
@Getter
public class Money implements Comparable<Money> {
@Column(name = "currency_code", length = 3)
@@ -40,6 +43,16 @@ public class Money implements Comparable<Money> {
@Column(name = "amount", scale = 6, precision = 19)
private BigDecimal amount;
private final transient MathContext mc;
protected Money() {
this.currencyCode = null;
this.currencyDigitsAfterDecimal = 0;
this.inMultiplesOf = 0;
this.amount = null;
this.mc = getMc();
}
public static Money total(final Money... monies) {
if (monies.length == 0) {
throw new IllegalArgumentException("Money array must not be empty");
@@ -53,7 +66,7 @@ public class Money implements Comparable<Money> {
public static Money total(final Iterable<? extends Money> monies) {
final Iterator<? extends Money> it = monies.iterator();
if (it.hasNext() == false) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Money iterator must not be empty");
}
Money total = it.next();
@@ -63,34 +76,47 @@ public class Money implements Comparable<Money> {
return total;
}
public static Money of(final MonetaryCurrency currency, final BigDecimal newAmount, final MathContext mc) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), defaultToZeroIfNull(newAmount),
currency.getCurrencyInMultiplesOf(), mc);
}
public static Money of(final MonetaryCurrency currency, final BigDecimal newAmount) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), defaultToZeroIfNull(newAmount),
currency.getCurrencyInMultiplesOf());
currency.getCurrencyInMultiplesOf(), null);
}
public static Money of(final CurrencyData currency, final BigDecimal newAmount) {
return new Money(currency.getCode(), currency.getDecimalPlaces(), defaultToZeroIfNull(newAmount), currency.getInMultiplesOf());
return new Money(currency.getCode(), currency.getDecimalPlaces(), defaultToZeroIfNull(newAmount), currency.getInMultiplesOf(),
null);
}
public static Money of(final CurrencyData currency, final BigDecimal newAmount, final MathContext mc) {
return new Money(currency.getCode(), currency.getDecimalPlaces(), defaultToZeroIfNull(newAmount), currency.getInMultiplesOf(), mc);
}
public static Money zero(final MonetaryCurrency currency) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO, currency.getCurrencyInMultiplesOf());
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO, currency.getCurrencyInMultiplesOf(), null);
}
public static Money zero(final MonetaryCurrency currency, MathContext mc) {
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO, currency.getCurrencyInMultiplesOf(), mc);
}
public static Money zero(final CurrencyData currency, MathContext mc) {
return new Money(currency.getCode(), currency.getDecimalPlaces(), BigDecimal.ZERO, currency.getInMultiplesOf(), mc);
}
public static Money zero(final CurrencyData currency) {
return new Money(currency.getCode(), currency.getDecimalPlaces(), BigDecimal.ZERO, currency.getInMultiplesOf());
return new Money(currency.getCode(), currency.getDecimalPlaces(), BigDecimal.ZERO, currency.getInMultiplesOf(), null);
}
protected Money() {
this.currencyCode = null;
this.currencyDigitsAfterDecimal = 0;
this.inMultiplesOf = 0;
this.amount = null;
}
private Money(final String currencyCode, final int digitsAfterDecimal, final BigDecimal amount, final Integer inMultiplesOf) {
private Money(final String currencyCode, final int digitsAfterDecimal, final BigDecimal amount, final Integer inMultiplesOf,
final MathContext mc) {
this.currencyCode = currencyCode;
this.currencyDigitsAfterDecimal = digitsAfterDecimal;
this.inMultiplesOf = inMultiplesOf;
this.mc = mc;
final BigDecimal amountZeroed = defaultToZeroIfNull(amount);
final BigDecimal amountStripped = amountZeroed.stripTrailingZeros();
@@ -101,7 +127,7 @@ public class Money implements Comparable<Money> {
final double existingVal = amountScaled.doubleValue();
amountScaled = BigDecimal.valueOf(roundToMultiplesOf(existingVal, inMultiplesOf));
}
this.amount = amountScaled.setScale(this.currencyDigitsAfterDecimal, MoneyHelper.getRoundingMode());
this.amount = amountScaled.setScale(this.currencyDigitsAfterDecimal, getMc().getRoundingMode());
}
public static double roundToMultiplesOf(final double existingVal, final Integer inMultiplesOf) {
@@ -130,10 +156,14 @@ public class Money implements Comparable<Money> {
}
public static Money roundToMultiplesOf(final Money existingVal, final Integer inMultiplesOf) {
return roundToMultiplesOf(existingVal, inMultiplesOf, MoneyHelper.getMathContext());
}
public static Money roundToMultiplesOf(final Money existingVal, final Integer inMultiplesOf, final MathContext mc) {
BigDecimal amountScaled = existingVal.getAmount();
BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf.intValue());
if (inMultiplesOfValue.compareTo(BigDecimal.ZERO) > 0) {
amountScaled = amountScaled.divide(inMultiplesOfValue, 0, MoneyHelper.getRoundingMode()).multiply(inMultiplesOfValue);
amountScaled = amountScaled.divide(inMultiplesOfValue, 0, mc.getRoundingMode()).multiply(inMultiplesOfValue);
}
return Money.of(existingVal.getCurrency(), amountScaled);
}
@@ -171,7 +201,7 @@ public class Money implements Comparable<Money> {
}
public Money copy() {
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros(), this.inMultiplesOf);
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros(), this.inMultiplesOf, this.mc);
}
public Money plus(final Iterable<? extends Money> moniesToAdd) {
@@ -188,6 +218,11 @@ public class Money implements Comparable<Money> {
return this.plus(toAdd.getAmount());
}
public Money plus(final Money moneyToAdd, final MathContext mc) {
final Money toAdd = checkCurrencyEqual(moneyToAdd);
return this.plus(toAdd.getAmount(), mc);
}
public Money plus(final BigDecimal amountToAdd) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
return this;
@@ -196,6 +231,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money plus(final BigDecimal amountToAdd, MathContext mc) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.add(amountToAdd);
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money plus(final double amountToAdd) {
if (amountToAdd == 0) {
return this;
@@ -209,11 +252,21 @@ public class Money implements Comparable<Money> {
return this.minus(toSubtract.getAmount());
}
public Money minus(final Money moneyToSubtract, final MathContext mc) {
final Money toSubtract = checkCurrencyEqual(moneyToSubtract);
return this.minus(toSubtract.getAmount(), mc);
}
public Money add(final Money moneyToAdd) {
final Money toAdd = checkCurrencyEqual(moneyToAdd);
return this.add(toAdd.getAmount());
}
public Money add(final Money moneyToAdd, final MathContext mc) {
final Money toAdd = checkCurrencyEqual(moneyToAdd);
return this.add(toAdd.getAmount(), mc);
}
public Money add(final BigDecimal amountToAdd) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
return this;
@@ -222,6 +275,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money add(final BigDecimal amountToAdd, final MathContext mc) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.add(amountToAdd);
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money minus(final BigDecimal amountToSubtract) {
if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) {
return this;
@@ -230,8 +291,16 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money minus(final BigDecimal amountToSubtract, final MathContext mc) {
if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.subtract(amountToSubtract);
return Money.of(monetaryCurrency(), newAmount, mc);
}
private Money checkCurrencyEqual(final Money money) {
if (isSameCurrency(money) == false) {
if (!isSameCurrency(money)) {
throw new UnsupportedOperationException("currencies are different.");
}
return money;
@@ -249,6 +318,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money dividedBy(final BigDecimal valueToDivideBy, final RoundingMode roundingMode, final MathContext mc) {
if (valueToDivideBy.compareTo(BigDecimal.ONE) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.divide(valueToDivideBy, roundingMode);
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money dividedBy(final double valueToDivideBy, final RoundingMode roundingMode) {
if (valueToDivideBy == 1) {
return this;
@@ -265,6 +342,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money dividedBy(final long valueToDivideBy, final RoundingMode roundingMode, final MathContext mc) {
if (valueToDivideBy == 1) {
return this;
}
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode);
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money multipliedBy(final BigDecimal valueToMultiplyBy) {
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
return this;
@@ -273,6 +358,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money multipliedBy(final BigDecimal valueToMultiplyBy, final MathContext mc) {
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
return this;
}
final BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money multipliedBy(final double valueToMultiplyBy) {
if (valueToMultiplyBy == 1) {
return this;
@@ -289,6 +382,14 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), newAmount);
}
public Money multipliedBy(final long valueToMultiplyBy, final MathContext mc) {
if (valueToMultiplyBy == 1) {
return this;
}
final BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy));
return Money.of(monetaryCurrency(), newAmount, mc);
}
public Money multiplyRetainScale(final BigDecimal valueToMultiplyBy, final RoundingMode roundingMode) {
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
return this;
@@ -309,17 +410,20 @@ public class Money implements Comparable<Money> {
@Override
public int compareTo(final Money other) {
final Money otherMoney = other;
if (this.currencyCode.equals(otherMoney.currencyCode) == false) {
if (!this.currencyCode.equals(other.currencyCode)) {
throw new UnsupportedOperationException("currencies arent different");
}
return this.amount.compareTo(otherMoney.amount);
return this.amount.compareTo(other.amount);
}
public boolean isZero() {
return isEqualTo(Money.zero(getCurrency()));
}
public boolean isZero(final MathContext mc) {
return isEqualTo(Money.zero(getCurrency(), mc));
}
public boolean isEqualTo(final Money other) {
return compareTo(other) == 0;
}
@@ -340,6 +444,10 @@ public class Money implements Comparable<Money> {
return isGreaterThan(Money.zero(getCurrency()));
}
public boolean isGreaterThanZero(MathContext mc) {
return isGreaterThan(Money.zero(getCurrency(), mc));
}
public boolean isLessThan(final Money other) {
return compareTo(other) < 0;
}
@@ -348,22 +456,14 @@ public class Money implements Comparable<Money> {
return isLessThan(Money.zero(getCurrency()));
}
public String getCurrencyCode() {
return this.currencyCode;
}
public int getCurrencyDigitsAfterDecimal() {
return this.currencyDigitsAfterDecimal;
public boolean isLessThanZero(final MathContext mc) {
return isLessThan(Money.zero(getCurrency(), mc));
}
public Integer getCurrencyInMultiplesOf() {
return this.inMultiplesOf;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getAmountDefaultedToNullIfZero() {
return defaultToNullIfZero(this.amount);
}
@@ -388,10 +488,21 @@ public class Money implements Comparable<Money> {
return Money.of(monetaryCurrency(), this.amount.negate());
}
public Money negated(final MathContext mc) {
if (isZero(mc)) {
return this;
}
return Money.of(monetaryCurrency(), this.amount.negate(), mc);
}
public Money abs() {
return isLessThanZero() ? negated() : this;
}
public Money abs(MathContext mc) {
return isLessThanZero(mc) ? negated(mc) : this;
}
public MonetaryCurrency getCurrency() {
return monetaryCurrency();
}
@@ -403,4 +514,12 @@ public class Money implements Comparable<Money> {
public Money zero() {
return Money.zero(getCurrency());
}
public Money zero(MathContext mc) {
return Money.zero(getCurrency(), mc);
}
public MathContext getMc() {
return mc != null ? mc : MoneyHelper.getMathContext();
}
}
@@ -65,6 +65,7 @@ public class MoneyHelper {
public static void fetchRoundingModeFromGlobalConfig() {
roundingMode = RoundingMode.valueOf(staticConfigurationDomainService.getRoundingMode());
log.info("Fetch Rounding Mode from Global Config {}", roundingMode.name());
mathContext = null;
}
}
+5
View File
@@ -59,6 +59,11 @@ dependencies {
'org.apache.commons:commons-collections4'
)
implementation ('org.mnode.ical4j:ical4j') {
exclude group: 'commons-logging'
exclude group: 'javax.activation'
exclude group: 'com.sun.mail', module: 'javax.mail'
}
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor'
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
@@ -98,6 +99,7 @@ public final class LoanScheduleParams {
private int loanTermInDays;
private final MonetaryCurrency currency;
private final boolean applyInterestRecalculation;
private final MathContext mc;
private LoanScheduleParams(final int periodNumber, final int instalmentNumber, int loanTermInDays, LocalDate periodStartDate,
final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest,
@@ -108,7 +110,7 @@ public final class LoanScheduleParams {
final Money outstandingBalanceAsPerRest, final List<LoanRepaymentScheduleInstallment> installments,
final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean partialUpdate, final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
final boolean partialUpdate, final MonetaryCurrency currency, final boolean applyInterestRecalculation, final MathContext mc) {
this.periodNumber = periodNumber;
this.instalmentNumber = instalmentNumber;
this.loanTermInDays = loanTermInDays;
@@ -136,8 +138,9 @@ public final class LoanScheduleParams {
this.partialUpdate = partialUpdate;
this.currency = currency;
this.applyInterestRecalculation = applyInterestRecalculation;
this.mc = mc;
if (this.currency != null) {
this.compoundedInLastInstallment = Money.zero(this.currency);
this.compoundedInLastInstallment = Money.zero(this.currency, mc);
}
}
@@ -150,19 +153,19 @@ public final class LoanScheduleParams {
final Money principalToBeScheduled, final Money outstandingBalance, final Money outstandingBalanceAsPerRest,
final List<LoanRepaymentScheduleInstallment> installments, final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
final MonetaryCurrency currency, final boolean applyInterestRecalculation, final MathContext mc) {
final boolean partialUpdate = true;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation, mc);
}
public static LoanScheduleParams createLoanScheduleParamsForCompleteUpdate(final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean applyInterestRecalculation) {
final boolean applyInterestRecalculation, MathContext mc) {
final int periodNumber = 1;
final int instalmentNumber = 1;
final LocalDate periodStartDate = null;
@@ -191,22 +194,22 @@ public final class LoanScheduleParams {
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation, mc);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
final LocalDate periodStartDate, final Money principalToBeScheduled) {
final LocalDate periodStartDate, final Money principalToBeScheduled, final MathContext mc) {
final int loanTermInDays = 0;
final int periodNumber = 1;
final int instalmentNumber = 1;
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
final Money totalCumulativePrincipal = Money.zero(currency, mc);
final Money totalCumulativeInterest = Money.zero(currency, mc);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency, mc);
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
final Money totalPenaltyChargesCharged = Money.zero(currency, mc);
final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement;
final Money reducePrincipal = Money.zero(currency);
final Money reducePrincipal = Money.zero(currency, mc);
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
final Map<LocalDate, Money> latePaymentMap = new HashMap<>();
final Map<LocalDate, Money> compoundingMap = new TreeMap<>();
@@ -218,27 +221,28 @@ public final class LoanScheduleParams {
final Collection<RecalculationDetail> recalculationDetails = null;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = null;
final LocalDate scheduleTillDate = null;
final Money unCompoundedAmount = Money.zero(currency);
final Money unCompoundedAmount = Money.zero(currency, mc);
final boolean applyInterestRecalculation = false;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation, mc);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
final LocalDate periodStartDate, final Money principalToBeScheduled, final LoanScheduleParams loanScheduleParams) {
final LocalDate periodStartDate, final Money principalToBeScheduled, final LoanScheduleParams loanScheduleParams,
MathContext mc) {
final int loanTermInDays = 0;
final int periodNumber = 1;
final int instalmentNumber = 1;
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
final Money totalCumulativePrincipal = Money.zero(currency, mc);
final Money totalCumulativeInterest = Money.zero(currency, mc);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency, mc);
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
final Money totalPenaltyChargesCharged = Money.zero(currency, mc);
final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement;
final Money reducePrincipal = Money.zero(currency);
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
@@ -253,13 +257,13 @@ public final class LoanScheduleParams {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanScheduleParams.loanRepaymentScheduleTransactionProcessor;
final LocalDate scheduleTillDate = loanScheduleParams.scheduleTillDate;
final boolean applyInterestRecalculation = loanScheduleParams.applyInterestRecalculation;
final Money unCompoundedAmount = Money.zero(currency);
final Money unCompoundedAmount = Money.zero(currency, mc);
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation, mc);
}
public int getPeriodNumber() {
@@ -288,7 +292,7 @@ public final class LoanScheduleParams {
public void addTotalCumulativePrincipal(final Money totalCumulativePrincipal) {
if (this.totalCumulativePrincipal != null) {
this.totalCumulativePrincipal = this.totalCumulativePrincipal.plus(totalCumulativePrincipal);
this.totalCumulativePrincipal = this.totalCumulativePrincipal.plus(totalCumulativePrincipal, mc);
} else {
this.totalCumulativePrincipal = totalCumulativePrincipal;
}
@@ -300,7 +304,7 @@ public final class LoanScheduleParams {
public void addTotalCumulativeInterest(final Money totalCumulativeInterest) {
if (this.totalCumulativeInterest != null) {
this.totalCumulativeInterest = this.totalCumulativeInterest.plus(totalCumulativeInterest);
this.totalCumulativeInterest = this.totalCumulativeInterest.plus(totalCumulativeInterest, mc);
} else {
this.totalCumulativeInterest = totalCumulativeInterest;
}
@@ -312,7 +316,7 @@ public final class LoanScheduleParams {
public void addTotalFeeChargesCharged(final Money totalFeeChargesCharged) {
if (this.totalFeeChargesCharged != null) {
this.totalFeeChargesCharged = this.totalFeeChargesCharged.plus(totalFeeChargesCharged);
this.totalFeeChargesCharged = this.totalFeeChargesCharged.plus(totalFeeChargesCharged, mc);
} else {
this.totalFeeChargesCharged = totalFeeChargesCharged;
}
@@ -324,7 +328,7 @@ public final class LoanScheduleParams {
public void addTotalPenaltyChargesCharged(final Money totalPenaltyChargesCharged) {
if (this.totalPenaltyChargesCharged != null) {
this.totalPenaltyChargesCharged = this.totalPenaltyChargesCharged.plus(totalPenaltyChargesCharged);
this.totalPenaltyChargesCharged = this.totalPenaltyChargesCharged.plus(totalPenaltyChargesCharged, mc);
} else {
this.totalPenaltyChargesCharged = totalPenaltyChargesCharged;
}
@@ -336,7 +340,7 @@ public final class LoanScheduleParams {
public void addTotalRepaymentExpected(final Money totalRepaymentExpected) {
if (this.totalRepaymentExpected != null) {
this.totalRepaymentExpected = this.totalRepaymentExpected.plus(totalRepaymentExpected);
this.totalRepaymentExpected = this.totalRepaymentExpected.plus(totalRepaymentExpected, mc);
} else {
this.totalRepaymentExpected = totalRepaymentExpected;
}
@@ -428,7 +432,7 @@ public final class LoanScheduleParams {
public void addReducePrincipal(Money reducePrincipal) {
if (this.reducePrincipal != null) {
this.reducePrincipal = this.reducePrincipal.plus(reducePrincipal);
this.reducePrincipal = this.reducePrincipal.plus(reducePrincipal, mc);
} else {
this.reducePrincipal = reducePrincipal;
}
@@ -436,13 +440,13 @@ public final class LoanScheduleParams {
public void reduceReducePrincipal(Money reducePrincipal) {
if (this.reducePrincipal != null) {
this.reducePrincipal = this.reducePrincipal.minus(reducePrincipal);
this.reducePrincipal = this.reducePrincipal.minus(reducePrincipal, mc);
}
}
public void addPrincipalToBeScheduled(Money principalToBeScheduled) {
if (this.principalToBeScheduled != null) {
this.principalToBeScheduled = this.principalToBeScheduled.plus(principalToBeScheduled);
this.principalToBeScheduled = this.principalToBeScheduled.plus(principalToBeScheduled, mc);
} else {
this.principalToBeScheduled = principalToBeScheduled;
}
@@ -450,7 +454,7 @@ public final class LoanScheduleParams {
public void addOutstandingBalance(Money outstandingBalance) {
if (this.outstandingBalance != null) {
this.outstandingBalance = this.outstandingBalance.plus(outstandingBalance);
this.outstandingBalance = this.outstandingBalance.plus(outstandingBalance, mc);
} else {
this.outstandingBalance = outstandingBalance;
}
@@ -458,13 +462,13 @@ public final class LoanScheduleParams {
public void reduceOutstandingBalance(Money outstandingBalance) {
if (this.outstandingBalance != null) {
this.outstandingBalance = this.outstandingBalance.minus(outstandingBalance);
this.outstandingBalance = this.outstandingBalance.minus(outstandingBalance, mc);
}
}
public void addOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) {
if (this.outstandingBalanceAsPerRest != null) {
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.plus(outstandingBalanceAsPerRest);
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.plus(outstandingBalanceAsPerRest, mc);
} else {
this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest;
}
@@ -472,7 +476,7 @@ public final class LoanScheduleParams {
public void reduceOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) {
if (this.outstandingBalanceAsPerRest != null) {
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.minus(outstandingBalanceAsPerRest);
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.minus(outstandingBalanceAsPerRest, mc);
}
}
@@ -522,7 +526,7 @@ public final class LoanScheduleParams {
public void minusUnCompoundedAmount(Money unCompoundedAmount) {
if (this.unCompoundedAmount != null) {
this.unCompoundedAmount = this.unCompoundedAmount.minus(unCompoundedAmount);
this.unCompoundedAmount = this.unCompoundedAmount.minus(unCompoundedAmount, mc);
}
}
@@ -94,10 +94,10 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
: loanApplicationTerms.getSubmittedOnDate();
if (loanScheduleParams == null) {
scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement),
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms));
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms), mc);
} else if (!loanScheduleParams.isPartialUpdate()) {
scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, Money.of(currency, chargesDueAtTimeOfDisbursement),
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms), loanScheduleParams);
periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms), loanScheduleParams, mc);
} else {
scheduleParams = loanScheduleParams;
}
@@ -345,7 +345,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
scheduleParams.getPeriodStartDate(), scheduledDueDate, currentPeriodParams.getPrincipalForThisPeriod(),
scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(),
currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(),
totalInstallmentDue, !isCompletePeriod);
totalInstallmentDue, !isCompletePeriod, mc);
if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) {
installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount());
}
@@ -651,7 +651,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
scheduleParams.getPeriodStartDate(), transactionDate, currentPeriodParams.getPrincipalForThisPeriod(),
scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(),
currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(),
currentPeriodParams.fetchTotalAmountForPeriod(), false);
currentPeriodParams.fetchTotalAmountForPeriod(), false, mc);
scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(interestTillDate.interestPaymentDueToGrace());
}
}
@@ -789,7 +789,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(),
scheduleParams.getPeriodStartDate(), transactionDate, principalForThisPeriod,
scheduleParams.getOutstandingBalance(), interestForCurrentInstallment, feeChargesForInstallment,
penaltyChargesForInstallment, totalInstallmentDue, true);
penaltyChargesForInstallment, totalInstallmentDue, true, mc);
periods.add(installment);
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
updateCompoundingMap(loanApplicationTerms, holidayDetailDTO, scheduleParams, lastRestDate, scheduledDueDate);
@@ -1416,7 +1416,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(params.getInstalmentNumber(),
startDate, transactionDate, totalInterest.zero(), totalInterest.zero(), totalInterest, totalInterest.zero(),
totalInterest.zero(), totalInterest, true);
totalInterest.zero(), totalInterest, true, mc);
params.incrementInstalmentNumber();
periods.add(installment);
totalCumulativeInterest = totalCumulativeInterest.plus(totalInterest);
@@ -1489,7 +1489,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
if (totalInterest.isGreaterThanZero()) {
LoanScheduleModelRepaymentPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(params.getInstalmentNumber(),
startDate, params.getActualRepaymentDate(), totalInterest.zero(), totalInterest.zero(), totalInterest,
totalInterest.zero(), totalInterest.zero(), totalInterest, true);
totalInterest.zero(), totalInterest.zero(), totalInterest, true, mc);
params.incrementInstalmentNumber();
periods.add(installment);
params.getCompoundingDateVariations().put(startDate, new TreeMap<>(params.getCompoundingMap()));
@@ -2189,7 +2189,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
// for complete schedule generation
LoanScheduleParams loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation);
loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation, mc);
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
final List<LoanRepaymentScheduleInstallment> retainedInstallments = new ArrayList<>();
@@ -2402,7 +2402,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
installment.resetDerivedComponents();
newRepaymentScheduleInstallments.add(installment);
outstandingBalance = outstandingBalance.minus(installment.getPrincipal(currency));
final LoanScheduleModelPeriod loanScheduleModelPeriod = createLoanScheduleModelPeriod(installment, outstandingBalance);
final LoanScheduleModelPeriod loanScheduleModelPeriod = createLoanScheduleModelPeriod(installment, outstandingBalance, mc);
periods.add(loanScheduleModelPeriod);
totalCumulativePrincipal = totalCumulativePrincipal.plus(installment.getPrincipal(currency));
totalCumulativeInterest = totalCumulativeInterest.plus(installment.getInterestCharged(currency));
@@ -2493,7 +2493,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap,
uncompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest,
newRepaymentScheduleInstallments, recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate,
currency, applyInterestRecalculation);
currency, applyInterestRecalculation, mc);
retainedInstallments.addAll(newRepaymentScheduleInstallments);
loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations);
loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getSummary().getTotalInterestCharged()));
@@ -2729,13 +2729,13 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
}
private LoanScheduleModelPeriod createLoanScheduleModelPeriod(final LoanRepaymentScheduleInstallment installment,
final Money outstandingPrincipal) {
final Money outstandingPrincipal, final MathContext mc) {
final MonetaryCurrency currency = outstandingPrincipal.getCurrency();
return LoanScheduleModelRepaymentPeriod.repayment(installment.getInstallmentNumber(), installment.getFromDate(),
installment.getDueDate(), installment.getPrincipal(currency), outstandingPrincipal,
installment.getInterestCharged(currency), installment.getFeeChargesCharged(currency),
installment.getPenaltyChargesCharged(currency), installment.getDue(currency),
installment.isRecalculatedInterestComponent());
installment.getPenaltyChargesCharged(currency), installment.getDue(currency), installment.isRecalculatedInterestComponent(),
mc);
}
private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms,
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -45,9 +46,9 @@ import org.springframework.stereotype.Component;
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
@Override
public List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(final LocalDate scheduleStartDate,
public List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(final MathContext mc, final LocalDate scheduleStartDate,
final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
final Money zeroAmount = Money.zero(loanApplicationTerms.getCurrency());
final Money zeroAmount = Money.zero(loanApplicationTerms.getCurrency(), mc);
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
final ArrayList<LoanScheduleModelRepaymentPeriod> repaymentPeriods = new ArrayList<>(numberOfRepayments);
LocalDate lastRepaymentDate = scheduleStartDate;
@@ -65,7 +66,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
}
nextRepaymentDate = applyLoanTermVariations(loanApplicationTerms, nextRepaymentDate);
repaymentPeriods.add(LoanScheduleModelRepaymentPeriod.repayment(repaymentPeriodNumber, lastRepaymentDate, nextRepaymentDate,
zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, false));
zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, false, mc));
lastRepaymentDate = nextRepaymentDate;
isFirstRepayment = false;
}
@@ -75,11 +76,13 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
private LocalDate applyLoanTermVariations(final LoanApplicationTerms loanApplicationTerms, final LocalDate scheduledDueDate) {
LocalDate modifiedScheduledDueDate = scheduledDueDate;
// due date changes should be applied only for that dueDate
if (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(scheduledDueDate)) {
LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation();
if (DateUtils.isEqual(modifiedScheduledDueDate, loanTermVariationsData.getTermVariationApplicableFrom())) {
modifiedScheduledDueDate = loanTermVariationsData.getDateValue();
loanApplicationTerms.updateVariationDays(DateUtils.getDifferenceInDays(scheduledDueDate, modifiedScheduledDueDate));
if (loanApplicationTerms.getLoanTermVariations() != null) {
if (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(scheduledDueDate)) {
LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation();
if (DateUtils.isEqual(modifiedScheduledDueDate, loanTermVariationsData.getTermVariationApplicableFrom())) {
modifiedScheduledDueDate = loanTermVariationsData.getDateValue();
loanApplicationTerms.updateVariationDays(DateUtils.getDifferenceInDays(scheduledDueDate, modifiedScheduledDueDate));
}
}
}
return modifiedScheduledDueDate;
@@ -197,28 +200,30 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
private AdjustedDateDetailsDTO recursivelyCheckNonWorkingDaysAndHolidaysAndWorkingDaysExemptionToGenerateNextRepaymentPeriodDate(
final AdjustedDateDetailsDTO adjustedDateDetailsDTO, final LoanApplicationTerms loanApplicationTerms,
final HolidayDetailDTO holidayDetailDTO, final boolean isFirstRepayment) {
final Recur recur = CalendarUtils.getICalRecur(holidayDetailDTO.getWorkingDays().getRecurrence());
final boolean isSevenDaysWeek = (recur.getDayList().size() == 7); // 7 Seven days in the week
// If Workings days are not seven day week
if (!isSevenDaysWeek) {
checkAndUpdateWorkingDayIfRepaymentDateIsNonWorkingDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
if (holidayDetailDTO != null) {
final Recur recur = CalendarUtils.getICalRecur(holidayDetailDTO.getWorkingDays().getRecurrence());
final boolean isSevenDaysWeek = (recur.getDayList().size() == 7); // 7 Seven days in the week
// If Workings days are not seven day week
if (!isSevenDaysWeek) {
checkAndUpdateWorkingDayIfRepaymentDateIsNonWorkingDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
}
// Check Holidays If applied
checkAndUpdateWorkingDayIfRepaymentDateIsHolidayDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
}
// Check Holidays If applied
checkAndUpdateWorkingDayIfRepaymentDateIsHolidayDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
/**
* Check Changed Schedule Date is holiday or is not a working day Then re-call this method to get the non
* holiday and working day
*/
if ((holidayDetailDTO.isHolidayEnabled() && HolidayUtil.getApplicableHoliday(adjustedDateDetailsDTO.getChangedScheduleDate(),
holidayDetailDTO.getHolidays()) != null)
|| WorkingDaysUtil.isNonWorkingDay(holidayDetailDTO.getWorkingDays(), adjustedDateDetailsDTO.getChangedScheduleDate())) {
recursivelyCheckNonWorkingDaysAndHolidaysAndWorkingDaysExemptionToGenerateNextRepaymentPeriodDate(adjustedDateDetailsDTO,
loanApplicationTerms, holidayDetailDTO, isFirstRepayment);
/**
* Check Changed Schedule Date is holiday or is not a working day Then re-call this method to get the non
* holiday and working day
*/
if ((holidayDetailDTO.isHolidayEnabled() && HolidayUtil.getApplicableHoliday(adjustedDateDetailsDTO.getChangedScheduleDate(),
holidayDetailDTO.getHolidays()) != null)
|| WorkingDaysUtil.isNonWorkingDay(holidayDetailDTO.getWorkingDays(),
adjustedDateDetailsDTO.getChangedScheduleDate())) {
recursivelyCheckNonWorkingDaysAndHolidaysAndWorkingDaysExemptionToGenerateNextRepaymentPeriodDate(adjustedDateDetailsDTO,
loanApplicationTerms, holidayDetailDTO, isFirstRepayment);
}
}
return adjustedDateDetailsDTO;
}
@@ -249,6 +249,8 @@ public final class LoanApplicationTerms {
this.inArrearsTolerance = builder.inArrearsTolerance;
this.disbursementDatas = builder.disbursementDatas;
this.downPaymentAmount = builder.downPaymentAmount;
this.submittedOnDate = builder.submittedOnDate;
this.seedDate = builder.seedDate;
}
public static class Builder {
@@ -272,6 +274,8 @@ public final class LoanApplicationTerms {
private Money inArrearsTolerance;
private List<DisbursementData> disbursementDatas;
private Money downPaymentAmount;
private LocalDate submittedOnDate;
private LocalDate seedDate;
public Builder currency(ApplicationCurrency currency) {
this.currency = currency;
@@ -343,11 +347,6 @@ public final class LoanApplicationTerms {
return this;
}
public Builder variationsDataWrapper(LoanTermVariationsDataWrapper variationsDataWrapper) {
this.variationsDataWrapper = variationsDataWrapper;
return this;
}
public Builder fixedLength(Integer fixedLength) {
this.fixedLength = fixedLength;
return this;
@@ -368,23 +367,41 @@ public final class LoanApplicationTerms {
return this;
}
public Builder submittedOnDate(LocalDate submittedOnDate) {
this.submittedOnDate = submittedOnDate;
return this;
}
public Builder seedDate(LocalDate seedDate) {
this.seedDate = seedDate;
return this;
}
public LoanApplicationTerms build() {
return new LoanApplicationTerms(this);
}
}
public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData) {
Money principal = Money.of(modelData.currency().toData(), modelData.disbursementAmount());
Money downPaymentAmount = Money.zero(modelData.currency().toData());
public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData, MathContext mc) {
Money principal = Money.of(modelData.currency().toData(), modelData.disbursementAmount(), mc);
Money downPaymentAmount = Money.zero(modelData.currency().toData(), mc);
if (modelData.downPaymentEnabled()) {
downPaymentAmount = Money.of(modelData.currency().toData(),
MathUtil.percentageOf(principal.getAmount(), modelData.disbursementAmount(), 19));
MathUtil.percentageOf(principal.getAmount(), modelData.disbursementAmount(), mc), mc);
if (modelData.installmentAmountInMultiplesOf() != null) {
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, modelData.installmentAmountInMultiplesOf());
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, modelData.installmentAmountInMultiplesOf(), mc);
}
}
LocalDate seedDate;
if (modelData.disbursementDate() != null) {
seedDate = modelData.disbursementDate();
} else {
seedDate = modelData.scheduleGenerationStartDate();
}
return new Builder().currency(modelData.currency()).loanTermFrequency(modelData.numberOfRepayments())
.loanTermPeriodFrequencyType(PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()))
.numberOfRepayments(modelData.numberOfRepayments()).repaymentEvery(modelData.repaymentFrequency())
@@ -394,8 +411,8 @@ public final class LoanApplicationTerms {
.annualNominalInterestRate(modelData.annualNominalInterestRate()).principal(principal)
.expectedDisbursementDate(modelData.disbursementDate()).repaymentsStartingFromDate(modelData.scheduleGenerationStartDate())
.daysInMonthType(modelData.daysInMonth()).daysInYearType(modelData.daysInYear()).fixedLength(modelData.fixedLength())
.inArrearsTolerance(Money.zero(modelData.currency().toData())).disbursementDatas(new ArrayList<>())
.downPaymentAmount(downPaymentAmount).build();
.inArrearsTolerance(Money.zero(modelData.currency().toData(), mc)).disbursementDatas(new ArrayList<>())
.downPaymentAmount(downPaymentAmount).submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate).build();
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
@@ -46,18 +47,20 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
private final Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails = new HashSet<>();
private boolean isEMIFixedSpecificToInstallment = false;
BigDecimal rescheduleInterestPortion;
private final MathContext mc;
public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate,
final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue,
final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue, boolean recalculatedInterestComponent) {
final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue, boolean recalculatedInterestComponent,
final MathContext mc) {
return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance,
interestDue, feeChargesDue, penaltyChargesDue, totalDue, recalculatedInterestComponent);
interestDue, feeChargesDue, penaltyChargesDue, totalDue, recalculatedInterestComponent, mc);
}
public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate,
final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue,
final Money penaltyChargesDue, final Money totalDue, final boolean recalculatedInterestComponent) {
final Money penaltyChargesDue, final Money totalDue, final boolean recalculatedInterestComponent, final MathContext mc) {
this.periodNumber = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -68,6 +71,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
this.penaltyChargesDue = penaltyChargesDue;
this.totalDue = totalDue;
this.recalculatedInterestComponent = recalculatedInterestComponent;
this.mc = mc;
}
@Override
@@ -144,15 +148,15 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
@Override
public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) {
this.feeChargesDue = this.feeChargesDue.plus(feeCharge);
this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge);
this.totalDue = this.totalDue.plus(feeCharge).plus(penaltyCharge);
this.feeChargesDue = this.feeChargesDue.plus(feeCharge, mc);
this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge, mc);
this.totalDue = this.totalDue.plus(feeCharge, mc).plus(penaltyCharge, mc);
}
@Override
public void addPrincipalAmount(final Money principalDue) {
this.principalDue = this.principalDue.plus(principalDue);
this.totalDue = this.totalDue.plus(principalDue);
this.principalDue = this.principalDue.plus(principalDue, mc);
this.totalDue = this.totalDue.plus(principalDue, mc);
}
@Override
@@ -162,8 +166,8 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
@Override
public void addInterestAmount(Money interestDue) {
this.interestDue = this.interestDue.plus(interestDue);
this.totalDue = this.totalDue.plus(interestDue);
this.interestDue = this.interestDue.plus(interestDue, mc);
this.totalDue = this.totalDue.plus(interestDue, mc);
}
public void setPeriodNumber(int periodNumber) {
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
@@ -27,8 +28,8 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
public interface ScheduledDateGenerator {
List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(LocalDate scheduledDueDate, LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO);
List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(MathContext mc, LocalDate scheduledDueDate,
LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
@@ -64,6 +65,7 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@Test
@@ -142,11 +144,12 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
@NotNull
private LoanRepaymentScheduleInstallment createPeriod(int periodId, LocalDate start, LocalDate end) {
LoanRepaymentScheduleInstallment period = Mockito.mock(LoanRepaymentScheduleInstallment.class);
MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
Mockito.when(period.getInstallmentNumber()).thenReturn(periodId);
Mockito.when(period.getFromDate()).thenReturn(start);
Mockito.when(period.getDueDate()).thenReturn(end);
Money principal = Money.of(currency, new BigDecimal("1000.0"));
Money interest = Money.of(currency, BigDecimal.ZERO);
Money principal = Money.of(currency, new BigDecimal("1000.0"), mc);
Money interest = Money.of(currency, BigDecimal.ZERO, mc);
Mockito.when(period.getPrincipal(eq(currency))).thenReturn(principal);
Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest);
@@ -176,7 +176,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
installmentAmountInMultiplesOf, installments);
installmentAmountInMultiplesOf, installments, overpaymentHolder.getMoneyObject().getMc());
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.Optional;
import lombok.EqualsAndHashCode;
@@ -44,9 +45,10 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
private Money disbursementAmount;
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;
private final MathContext mc;
public InterestPeriod(RepaymentPeriod repaymentPeriod, LocalDate fromDate, LocalDate dueDate, BigDecimal rateFactor,
Money disbursementAmount, Money balanceCorrectionAmount, Money outstandingLoanBalance) {
Money disbursementAmount, Money balanceCorrectionAmount, Money outstandingLoanBalance, MathContext mc) {
this.repaymentPeriod = repaymentPeriod;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -54,12 +56,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
this.disbursementAmount = disbursementAmount;
this.balanceCorrectionAmount = balanceCorrectionAmount;
this.outstandingLoanBalance = outstandingLoanBalance;
this.mc = mc;
}
public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod) {
public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod, MathContext mc) {
this(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(),
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance());
interestPeriod.getOutstandingLoanBalance(), mc);
}
@Override
@@ -72,11 +75,11 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
public void addDisbursementAmount(final Money disbursementAmount) {
this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount);
this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount, mc);
}
public Money getCalculatedDueInterest() {
return getOutstandingLoanBalance().multipliedBy(getRateFactor());
return getOutstandingLoanBalance().multipliedBy(getRateFactor(), mc);
}
public void updateOutstandingLoanBalance() {
@@ -86,17 +89,17 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods()
.get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1);
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()//
.plus(previousInterestPeriod.getDisbursementAmount())//
.plus(previousInterestPeriod.getBalanceCorrectionAmount())//
.minus(previousRepaymentPeriod.get().getDuePrincipal())//
.plus(previousRepaymentPeriod.get().getPaidPrincipal());//
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc);//
}
} else {
int index = getRepaymentPeriod().getInterestPeriods().indexOf(this);
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance() //
.plus(previousInterestPeriod.getBalanceCorrectionAmount()) //
.plus(previousInterestPeriod.getDisbursementAmount()); //
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
.plus(previousInterestPeriod.getDisbursementAmount(), mc); //
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -38,33 +39,36 @@ public class ProgressiveLoanInterestScheduleModel {
private final List<InterestRate> interestRates;
private final LoanProductRelatedDetail loanProductRelatedDetail;
private final Integer installmentAmountInMultiplesOf;
private MathContext mc;
public ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf) {
Integer installmentAmountInMultiplesOf, MathContext mc) {
this.repaymentPeriods = repaymentPeriods;
this.interestRates = new ArrayList<>();
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.mc = mc;
}
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final List<InterestRate> interestRates,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf) {
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
this.mc = mc;
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods);
this.interestRates = new ArrayList<>(interestRates);
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
}
public ProgressiveLoanInterestScheduleModel deepCopy() {
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail,
installmentAmountInMultiplesOf);
installmentAmountInMultiplesOf, mc);
}
private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods) {
final List<RepaymentPeriod> repaymentCopies = new ArrayList<>(repaymentPeriods.size());
RepaymentPeriod previousPeriod = null;
for (RepaymentPeriod repaymentPeriod : repaymentPeriods) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentPeriod);
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentPeriod, mc);
previousPeriod = currentPeriod;
repaymentCopies.add(currentPeriod);
}
@@ -175,11 +179,11 @@ public class ProgressiveLoanInterestScheduleModel {
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, previousInterestPeriod.getDueDate(), originalDueDate,
BigDecimal.ZERO, getZero(), getZero(), getZero());
BigDecimal.ZERO, getZero(mc), getZero(mc), getZero(mc), mc);
repaymentPeriod.getInterestPeriods().add(interestPeriod);
}
private Money getZero() {
return Money.zero(loanProductRelatedDetail.getCurrency());
private Money getZero(MathContext mc) {
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@@ -54,20 +55,23 @@ public class RepaymentPeriod {
private Memo<Money> calculatedDueInterestCalculation;
private Memo<Money> dueInterestCalculation;
private Memo<Money> outstandingBalanceCalculation;
private final MathContext mc;
public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi) {
public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) {
this.previous = previous;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.emi = emi;
this.mc = mc;
this.interestPeriods = new ArrayList<>();
// There is always at least 1 interest period, by default with same from-due date as repayment period
getInterestPeriods().add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, getZero(), getZero(), getZero()));
this.paidInterest = getZero();
this.paidPrincipal = getZero();
getInterestPeriods()
.add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, getZero(mc), getZero(mc), getZero(mc), mc));
this.paidInterest = getZero(mc);
this.paidPrincipal = getZero(mc);
}
public RepaymentPeriod(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod) {
public RepaymentPeriod(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
this.previous = previous;
this.fromDate = repaymentPeriod.fromDate;
this.dueDate = repaymentPeriod.dueDate;
@@ -75,9 +79,10 @@ public class RepaymentPeriod {
this.interestPeriods = new ArrayList<>();
this.paidPrincipal = repaymentPeriod.paidPrincipal;
this.paidInterest = repaymentPeriod.paidInterest;
this.mc = mc;
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
interestPeriods.add(new InterestPeriod(this, interestPeriod));
interestPeriods.add(new InterestPeriod(this, interestPeriod, mc));
}
}
@@ -105,21 +110,21 @@ public class RepaymentPeriod {
}
private Money calculateCalculatedDueInterest() {
Money calculatedDueInterest = getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(getZero(),
Money::plus);
Money calculatedDueInterest = getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(getZero(mc),
(m1, m2) -> m1.plus(m2, mc));
if (getPrevious().isPresent()) {
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest());
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), mc);
}
return calculatedDueInterest;
}
private Money getZero() {
private Money getZero(MathContext mc) {
// EMI is always initiated
return this.emi.zero();
return this.emi.zero(mc);
}
public Money getCalculatedDuePrincipal() {
return getEmi().minus(getCalculatedDueInterest());
return getEmi().minus(getCalculatedDueInterest(), mc);
}
public boolean isFullyPaid() {
@@ -138,11 +143,11 @@ public class RepaymentPeriod {
public Money getDuePrincipal() {
// Due principal might be the maximum paid if there is pay-off or early repayment
return MathUtil.max(getEmi().minus(getDueInterest()), getPaidPrincipal(), false);
return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false);
}
public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest());
return getCalculatedDueInterest().minus(getDueInterest(), mc);
}
public Money getOutstandingLoanBalance() {
@@ -150,22 +155,22 @@ public class RepaymentPeriod {
outstandingBalanceCalculation = Memo.of(() -> {
InterestPeriod lastInstallmentPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
Money calculatedOutStandingLoanBalance = lastInstallmentPeriod.getOutstandingLoanBalance() //
.plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) //
.plus(lastInstallmentPeriod.getDisbursementAmount()) //
.minus(getDuePrincipal())//
.plus(getPaidPrincipal());//
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance);
.plus(lastInstallmentPeriod.getBalanceCorrectionAmount(), mc) //
.plus(lastInstallmentPeriod.getDisbursementAmount(), mc) //
.minus(getDuePrincipal(), mc)//
.plus(getPaidPrincipal(), mc);//
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc);
}, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods });
}
return outstandingBalanceCalculation.get();
}
public void addPaidPrincipalAmount(Money paidPrincipal) {
this.paidPrincipal = MathUtil.plus(this.paidPrincipal, paidPrincipal);
this.paidPrincipal = MathUtil.plus(this.paidPrincipal, paidPrincipal, mc);
}
public void addPaidInterestAmount(Money paidInterest) {
this.paidInterest = MathUtil.plus(this.paidInterest, paidInterest);
this.paidInterest = MathUtil.plus(this.paidInterest, paidInterest, mc);
}
public Money getInitialBalanceForEmiRecalculation() {
@@ -173,10 +178,10 @@ public class RepaymentPeriod {
if (getPrevious().isPresent()) {
initialBalance = getPrevious().get().getOutstandingLoanBalance();
} else {
initialBalance = getZero();
initialBalance = getZero(mc);
}
Money totalDisbursedAmount = getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount).reduce(getZero(),
Money::plus);
return initialBalance.add(totalDisbursedAmount);
Money totalDisbursedAmount = getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount).reduce(getZero(mc),
(m1, m2) -> m1.plus(m2, mc));
return initialBalance.add(totalDisbursedAmount, mc);
}
}
@@ -87,17 +87,17 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
: loanApplicationTerms.getSubmittedOnDate();
final LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency,
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, Money.zero(currency));
Money.of(currency, chargesDueAtTimeOfDisbursement, mc), periodStartDate, Money.zero(currency, mc), mc);
// charges which depends on total loan interest will be added to this
// set and handled separately after all installments generated
final Set<LoanCharge> nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator
.generateRepaymentPeriods(periodStartDate, loanApplicationTerms, holidayDetailDTO);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, holidayDetailDTO);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
@@ -110,7 +110,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate());
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement, false);
chargesDueAtTimeOfDisbursement, false, mc);
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
if (loanApplicationTerms.getLoanTermVariations() != null) {
@@ -135,7 +135,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.addTotalCumulativePrincipal(principalDue);
scheduleParams.addTotalCumulativeInterest(interestDue);
// add everything
scheduleParams.addTotalRepaymentExpected(principalDue.plus(interestDue));
scheduleParams.addTotalRepaymentExpected(principalDue.plus(interestDue, mc));
});
applyChargesForCurrentPeriod(repaymentPeriod, loanCharges, scheduleParams, currency, mc);
@@ -147,7 +147,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
if (loanApplicationTerms.isMultiDisburseLoan()) {
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement, true);
chargesDueAtTimeOfDisbursement, true, mc);
}
// determine fees and penalties for charges which depends on total
@@ -158,8 +158,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final BigDecimal totalOutstanding = BigDecimal.ZERO;
return LoanScheduleModel.from(periods, applicationCurrency, interestScheduleModel.getLoanTermInDays(),
scheduleParams.getPrincipalToBeScheduled().plus(loanApplicationTerms.getDownPaymentAmount()),
scheduleParams.getTotalCumulativePrincipal().plus(loanApplicationTerms.getDownPaymentAmount()).getAmount(),
scheduleParams.getPrincipalToBeScheduled().plus(loanApplicationTerms.getDownPaymentAmount(), mc),
scheduleParams.getTotalCumulativePrincipal().plus(loanApplicationTerms.getDownPaymentAmount(), mc).getAmount(),
totalPrincipalPaid, scheduleParams.getTotalCumulativeInterest().getAmount(),
scheduleParams.getTotalFeeChargesCharged().getAmount(), scheduleParams.getTotalPenaltyChargesCharged().getAmount(),
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
@@ -167,7 +167,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
public LoanScheduleModel generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData);
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return generate(mc, loanApplicationTerms, null, null);
}
@@ -184,7 +184,7 @@ 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 BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
for (DisbursementData disbursementData : disbursementDataList) {
final LocalDate disbursementDate = disbursementData.disbursementDate();
@@ -202,7 +202,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate);
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal(), mc);
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
.disbursement(disbursementData.disbursementDate(), disbursedAmount, chargesDueAtTimeOfDisbursement);
periods.add(disbursementPeriod);
@@ -218,7 +218,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
}
Money downPaymentAmount = Money.zero(loanApplicationTerms.getCurrency());
Money downPaymentAmount = Money.zero(loanApplicationTerms.getCurrency(), mc);
if (loanApplicationTerms.isDownPaymentEnabled()) {
downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(), MathUtil.percentageOf(disbursedAmount.getAmount(),
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
@@ -321,7 +321,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
final MathContext mc) {
Money cumulative = Money.zero(monetaryCurrency);
Money cumulative = Money.zero(monetaryCurrency, mc);
if (loanCharges != null) {
for (final LoanCharge loanCharge : loanCharges) {
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
@@ -356,7 +356,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
final MathContext mc) {
Money cumulative = Money.zero(monetaryCurrency);
Money cumulative = Money.zero(monetaryCurrency, mc);
if (loanCharges != null) {
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
@@ -407,8 +407,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final Collection<LoanScheduleModelPeriod> periods, final Set<LoanCharge> nonCompoundingCharges, MathContext mc) {
for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) {
if (loanScheduleModelPeriod.isRepaymentPeriod()) {
PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()),
Money.of(currency, loanScheduleModelPeriod.interestDue()), null);
PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue(), mc),
Money.of(currency, loanScheduleModelPeriod.interestDue(), mc), null);
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanproduct.calc;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@@ -33,10 +34,10 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
public interface EMICalculator {
ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List<LoanScheduleModelRepaymentPeriod> periods,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf);
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods);
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc);
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanproduct.calc;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.Year;
import java.util.ArrayList;
@@ -29,7 +30,6 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@@ -40,12 +40,10 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLo
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@DependsOn("moneyHelper")
public final class ProgressiveEMICalculator implements EMICalculator {
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
@@ -53,19 +51,19 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List<LoanScheduleModelRepaymentPeriod> periods,
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf) {
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency());
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency(), mc);
final ArrayList<RepaymentPeriod> interestRepaymentModelList = new ArrayList<>(periods.size());
RepaymentPeriod previousPeriod = null;
for (final LoanScheduleModelRepaymentPeriod period : periods) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(),
zeroAmount);
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(), zeroAmount,
mc);
previousPeriod = currentPeriod;
interestRepaymentModelList.add(currentPeriod);
}
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
installmentAmountInMultiplesOf);
installmentAmountInMultiplesOf, mc);
}
@Override
@@ -85,7 +83,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final Money disbursedAmount) {
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency()))
Money.zero(disbursedAmount.getCurrency(), scheduleModel.mc()))
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel));
}
@@ -130,7 +128,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) {
RepaymentPeriod repaymentPeriod = scheduleModel.deepCopy().repaymentPeriods().stream()
MathContext mc = scheduleModel.mc();
RepaymentPeriod repaymentPeriod = scheduleModel.deepCopy(mc).repaymentPeriods().stream()
.filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow();
LocalDate adjustedTargetDate = targetDate;
InterestPeriod interestPeriod;
@@ -151,19 +150,19 @@ public final class ProgressiveEMICalculator implements EMICalculator {
// TODO: gather all the unrecognized interest from previous periods based on target date
Money payableInterest = targetDate.isBefore(repaymentPeriod.getFromDate())
? Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency())
? Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc)
: repaymentPeriod.getDueInterest();
Money outstandingLoanBalance = interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount());
Money calculatedEmi = outstandingLoanBalance.plus(payableInterest);
Money calculatedEmi = outstandingLoanBalance.plus(payableInterest, mc);
if (calculatedEmi.isLessThan(repaymentPeriod.getEmi())) {
// Review this logic
repaymentPeriod.setEmi(outstandingLoanBalance.plus(payableInterest).plus(repaymentPeriod.getPaidInterest())
.plus(repaymentPeriod.getPaidPrincipal()));
repaymentPeriod.setEmi(outstandingLoanBalance.plus(payableInterest).plus(repaymentPeriod.getPaidInterest(), mc)
.plus(repaymentPeriod.getPaidPrincipal(), mc));
}
Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest);
Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest, mc);
return new PayableDetails(repaymentPeriod.getEmi(), payablePrincipal, payableInterest,
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount()));
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc));
}
@Override
@@ -189,18 +188,19 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel) {
MathContext mc = scheduleModel.mc();
Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest)
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 1.46
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc), (m1, m2) -> m1.plus(m2, mc)); // 1.46
Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmi)
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 101.48
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc), (m1, m2) -> m1.plus(m2, mc)); // 101.48
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 100
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc), (m1, m2) -> m1.plus(m2, mc)); // 100
Money diff = totalDisbursedAmount.plus(totalDueInterest).minus(totalEMI);
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff)));
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc)));
}
private void calculateOutstandingBalance(ProgressiveLoanInterestScheduleModel scheduleModel) {
@@ -209,24 +209,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel,
final List<RepaymentPeriod> relatedRepaymentPeriods) {
MathContext mc = scheduleModel.mc();
final Money emiDifference = getDifferenceBetweenLastTwoPeriod(relatedRepaymentPeriods, scheduleModel);
final int numberOfRelatedPeriods = relatedRepaymentPeriods.size();
double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods / 2.0);
if (emiDifference.isZero() || lowerHalfOfRelatedPeriods == 0.0) {
if (emiDifference.isZero(mc) || lowerHalfOfRelatedPeriods == 0.0) {
return;
}
final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEmi();
boolean shouldBeAdjusted = emiDifference.abs().multipliedBy(100)
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods)));
boolean shouldBeAdjusted = emiDifference.abs(mc).multipliedBy(100, mc)
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
if (shouldBeAdjusted) {
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, MoneyHelper.getMathContext().getRoundingMode());
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, originalEmi.plus(adjustment));
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc.getRoundingMode(), mc);
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
originalEmi.plus(adjustment, mc));
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
return;
}
final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate();
final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy();
final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy(mc);
newScheduleModel.repaymentPeriods().forEach(period -> {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) {
period.setEmi(adjustedEqualMonthlyInstallmentValue);
@@ -235,7 +237,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
calculateOutstandingBalance(newScheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(newScheduleModel);
final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repaymentPeriods(), scheduleModel);
final boolean newEmiHasLessDifference = newEmiDifference.abs().isLessThan(emiDifference.abs());
final boolean newEmiHasLessDifference = newEmiDifference.abs(mc).isLessThan(emiDifference.abs(mc));
if (!newEmiHasLessDifference) {
return;
}
@@ -263,8 +265,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*
* @return Rate Interest Rate in fraction format
*/
BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate) {
return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, MoneyHelper.getMathContext());
BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate, MathContext mc) {
return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, mc);
}
/**
@@ -286,8 +288,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*/
BigDecimal calculateRateFactorPerPeriod(final RepaymentPeriod repaymentPeriod, final InterestPeriod interestPeriod,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final MathContext mc = scheduleModel.mc();
final LoanProductRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriod.getFromDate()));
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriod.getFromDate()),
scheduleModel.mc());
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
@@ -306,13 +310,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
// TODO refactor this logic to represent in interest period
if (partialPeriodCalculationNeeded) {
final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriod);
final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriod, mc);
return rateFactorByRepaymentPartialPeriod(interestRate, repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE,
BigDecimal.ONE);
BigDecimal.ONE, mc);
}
return calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth,
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
}
/**
@@ -321,7 +325,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param interestPeriod
* @return
*/
BigDecimal calculatePeriodFractions(InterestPeriod interestPeriod) {
BigDecimal calculatePeriodFractions(InterestPeriod interestPeriod, MathContext mc) {
BigDecimal cumulatedRateFactor = BigDecimal.ZERO;
int actualYear = interestPeriod.getFromDate().getYear();
int endYear = interestPeriod.getDueDate().getYear();
@@ -332,8 +336,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
endOfActualYear = actualYear == endYear ? interestPeriod.getDueDate() : LocalDate.of(actualYear, 12, 31);
BigDecimal numberOfDaysInYear = BigDecimal.valueOf(Year.of(actualYear).length());
BigDecimal calculatedDaysInActualYear = BigDecimal.valueOf(DateUtils.getDifferenceInDays(actualDate, endOfActualYear));
cumulatedRateFactor = cumulatedRateFactor
.add(calculatedDaysInActualYear.divide(numberOfDaysInYear, MoneyHelper.getMathContext()), MoneyHelper.getMathContext());
cumulatedRateFactor = cumulatedRateFactor.add(calculatedDaysInActualYear.divide(numberOfDaysInYear, mc), mc);
actualDate = endOfActualYear;
actualYear++;
}
@@ -354,14 +357,15 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*/
BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final BigDecimal interestRate,
final PeriodFrequencyType repaymentFrequency, final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
return switch (repaymentFrequency) {
case DAYS ->
rateFactorByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
rateFactorByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
case WEEKS ->
rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
case MONTHS -> rateFactorByRepaymentEveryMonth(interestRate, repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
calculatedDaysInPeriod, mc);
default -> throw new UnsupportedOperationException("Invalid repayment frequency"); // not supported yet
};
}
@@ -370,14 +374,15 @@ public final class ProgressiveEMICalculator implements EMICalculator {
if (repaymentPeriods.isEmpty()) {
return;
}
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods));
final MathContext mc = scheduleModel.mc();
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
final RepaymentPeriod startPeriod = repaymentPeriods.get(0);
// TODO: double check
final Money outstandingBalance = startPeriod.getInitialBalanceForEmiRecalculation();
final Money equalMonthlyInstallment = Money.of(outstandingBalance.getCurrency(),
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult));
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc), mc);
final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment));
@@ -392,39 +397,40 @@ public final class ProgressiveEMICalculator implements EMICalculator {
Money getDifferenceBetweenLastTwoPeriod(final List<RepaymentPeriod> repaymentPeriods,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
MathContext mc = scheduleModel.mc();
int numberOfUpcomingPeriods = repaymentPeriods.size();
if (numberOfUpcomingPeriods < 2) {
return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc);
}
final RepaymentPeriod lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1);
final RepaymentPeriod penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2);
return lastPeriod.getEmi().minus(penultimatePeriod.getEmi());
return lastPeriod.getEmi().minus(penultimatePeriod.getEmi(), mc);
}
/**
* Calculate Rate Factor Product from rate factors
*/
BigDecimal calculateRateFactorPlus1N(final List<RepaymentPeriod> periods) {
BigDecimal calculateRateFactorPlus1N(final List<RepaymentPeriod> periods, MathContext mc) {
return periods.stream().map(RepaymentPeriod::getRateFactorPlus1).reduce(BigDecimal.ONE,
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, MoneyHelper.getMathContext()));
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc));
}
/**
* Summarize Fn values
*/
BigDecimal calculateFnResult(final List<RepaymentPeriod> periods) {
BigDecimal calculateFnResult(final List<RepaymentPeriod> periods, final MathContext mc) {
return periods.stream()//
.skip(1)//
.map(RepaymentPeriod::getRateFactorPlus1)//
.reduce(BigDecimal.ONE, this::fnValue);//
.reduce(BigDecimal.ONE, (previousFnValue, currentRateFactor) -> fnValue(previousFnValue, currentRateFactor, mc));//
}
/**
* Calculate the EMI (Equal Monthly Installment) value
*/
BigDecimal calculateEMIValue(final BigDecimal rateFactorPlus1N, final BigDecimal outstandingBalanceForRest, final BigDecimal fnResult) {
return rateFactorPlus1N.multiply(outstandingBalanceForRest, MoneyHelper.getMathContext()).divide(fnResult,
MoneyHelper.getMathContext());
BigDecimal calculateEMIValue(final BigDecimal rateFactorPlus1N, final BigDecimal outstandingBalanceForRest, final BigDecimal fnResult,
MathContext mc) {
return rateFactorPlus1N.multiply(outstandingBalanceForRest, mc).divide(fnResult, mc);
}
/**
@@ -452,9 +458,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @return Rate Factor for period
*/
BigDecimal rateFactorByRepaymentEveryDay(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, MathContext mc) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
calculatedDaysInPeriod, mc);
}
/**
@@ -482,9 +488,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @return Rate Factor for period
*/
BigDecimal rateFactorByRepaymentEveryWeek(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, MathContext mc) {
return rateFactorByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
calculatedDaysInPeriod, mc);
}
/**
@@ -515,9 +521,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @return Rate Factor for period
*/
BigDecimal rateFactorByRepaymentEveryMonth(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
return rateFactorByRepaymentPeriod(interestRate, daysInMonth, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
calculatedDaysInPeriod, mc);
}
/**
@@ -549,14 +556,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*/
BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod) {
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays//
.multiply(repaymentEvery, MoneyHelper.getMathContext())//
.divide(daysInYear, MoneyHelper.getMathContext());//
.multiply(repaymentEvery, mc)//
.divide(daysInYear, mc);//
return interestRate//
.multiply(interestFractionPerPeriod, MoneyHelper.getMathContext())//
.multiply(actualDaysInPeriod, MoneyHelper.getMathContext())//
.divide(calculatedDaysInPeriod, MoneyHelper.getMathContext());//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
}
/**
@@ -564,12 +571,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*
*/
BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio);
return interestRate//
.multiply(interestFractionPerPeriod, MoneyHelper.getMathContext())//
.multiply(actualDaysInPeriod, MoneyHelper.getMathContext())//
.divide(calculatedDaysInPeriod, MoneyHelper.getMathContext());//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
}
/**
@@ -582,13 +590,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param currentRateFactor
*
*/
BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor) {
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, MoneyHelper.getMathContext()), MoneyHelper.getMathContext());
BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor, final MathContext mc) {
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc);
}
@Override
public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods) {
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc) {
List<LoanRepaymentScheduleInstallment> repaymentModelsWithoutDownPayment = repaymentPeriods.stream()
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
@@ -596,11 +604,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
RepaymentPeriod previousPeriod = null;
for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(),
Money.zero(repaymentModel.getLoan().getCurrency()));
Money.zero(repaymentModel.getLoan().getCurrency(), mc), mc);
previousPeriod = currentPeriod;
repaymentModels.add(currentPeriod);
}
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf);
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
}
@@ -35,6 +35,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -90,11 +91,12 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
private final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
@@ -20,77 +20,36 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class LoanScheduleGeneratorTest {
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
private static final MonetaryCurrency MONETARY_CURRENCY = MonetaryCurrency.fromApplicationCurrency(APPLICATION_CURRENCY);
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
private static final BigDecimal NOMINAL_INTEREST_RATE = BigDecimal.valueOf(9.99);
private static final int NUMBER_OF_REPAYMENTS = 6;
private static final int REPAYMENT_FREQUENCY = 1;
private static final String REPAYMENT_FREQUENCY_TYPE = "MONTHS";
private static final LocalDate DISBURSEMENT_DATE = LocalDate.of(2024, 1, 15);
@BeforeAll
public static void init() {
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
moneyHelper.close();
}
private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
@Test
void testGenerateLoanSchedule() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null);
final MathContext mc = MoneyHelper.getMathContext();
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)));
ScheduledDateGenerator mockScheduledDateGenerator = Mockito.mock(ScheduledDateGenerator.class);
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(mockScheduledDateGenerator, emiCalculator);
when(mockScheduledDateGenerator.generateRepaymentPeriods(any(), any(), any())).thenReturn(expectedRepaymentPeriods);
LoanScheduleModel loanSchedule = generator.generate(mc, modelData);
List<LoanScheduleModelPeriod> periods = loanSchedule.getPeriods();
List<LoanScheduleModelPeriod> periods = getLoanScheduleModelPeriods();
assertEquals(7, periods.size(), "Expected 7 periods including the downpayment period.");
@@ -111,6 +70,18 @@ class LoanScheduleGeneratorTest {
BigDecimal.valueOf(0.27), BigDecimal.valueOf(32.87), BigDecimal.ZERO);
}
private static List<LoanScheduleModelPeriod> getLoanScheduleModelPeriods() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator);
LoanScheduleModel loanSchedule = generator.generate(mc, modelData);
return loanSchedule.getPeriods();
}
private void checkPeriod(LoanScheduleModelPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedInterestDue, BigDecimal expectedTotalDue,
BigDecimal expectedOutstandingLoanBalance) {
@@ -123,10 +94,4 @@ class LoanScheduleGeneratorTest {
assertEquals(0, expectedTotalDue.compareTo(repaymentPeriod.getTotalDue().getAmount()));
assertEquals(0, expectedOutstandingLoanBalance.compareTo(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
}
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
final Money zeroAmount = Money.zero(MONETARY_CURRENCY);
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
zeroAmount, zeroAmount, false);
}
}
@@ -59,6 +59,7 @@ class ProgressiveEMICalculatorTest {
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
private static LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
@@ -95,7 +96,7 @@ class ProgressiveEMICalculatorTest {
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);
daysInPeriod, daysInPeriod, mc);
return rateFactor.setScale(12, MoneyHelper.getRoundingMode());
}
@@ -145,7 +146,7 @@ class ProgressiveEMICalculatorTest {
BigDecimal rateFactorPlus1 = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE,
MoneyHelper.getMathContext());
final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactorPlus1);
final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactorPlus1, mc);
fnValuesCalculated.add(currentFnValue);
previousFnValue = currentFnValue;
@@ -171,7 +172,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator
.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf);
.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Assertions.assertTrue(interestScheduleModel != null);
Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null);
@@ -211,7 +212,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -256,7 +257,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -293,7 +294,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -330,7 +331,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -370,7 +371,7 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -414,7 +415,7 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -460,7 +461,7 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -577,7 +578,7 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -661,7 +662,7 @@ class ProgressiveEMICalculatorTest {
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -739,7 +740,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -788,7 +789,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -837,7 +838,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount);
@@ -881,7 +882,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount);
@@ -915,7 +916,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -948,7 +949,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -981,7 +982,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1014,7 +1015,7 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, installmentAmountInMultiplesOf);
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
@@ -1031,7 +1032,7 @@ class ProgressiveEMICalculatorTest {
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
final Money zeroAmount = Money.zero(monetaryCurrency);
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
zeroAmount, zeroAmount, false);
zeroAmount, zeroAmount, false, mc);
}
@NotNull
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -88,6 +89,7 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
}
@@ -29,6 +29,7 @@ import static org.mockito.Mockito.when;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -116,8 +117,12 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
@Mock
private AvroDateTimeMapper mapper;
private MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
@BeforeEach
public void setUp() {
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil
@@ -127,6 +132,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
@AfterEach
public void tearDown() {
ThreadLocalContextUtil.reset();
moneyHelper.close();
}
@Test
@@ -141,7 +147,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
LoanAccountData loanAccountData = mock(LoanAccountData.class);
CollectionData delinquentData = mock(CollectionData.class);
MonetaryCurrency loanCurrency = new MonetaryCurrency("CODE", 1, 1);
MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
String delinquentDateAsStr = "2022-12-01";
LocalDate delinquentDate = LocalDate.parse(delinquentDateAsStr);
@@ -168,8 +173,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
when(loanChargeReadPlatformService.retrieveLoanCharges(anyLong())).thenAnswer(a -> repaymentScheduleInstallments.get(0)
.getInstallmentCharges().stream().map(c -> c.getLoanCharge().toData()).collect(Collectors.toList()));
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
// when
LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event);
@@ -190,8 +193,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
assertEquals(0, data.getAmount().getFeeAmount().compareTo(new BigDecimal("5.0")));
assertEquals(0, data.getAmount().getPenaltyAmount().compareTo(new BigDecimal("50.0")));
assertEquals(delinquentDateAsStr, data.getDelinquentDate());
moneyHelper.close();
}
@Test
@@ -206,7 +207,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
LoanAccountData loanAccountData = mock(LoanAccountData.class);
CollectionData delinquentData = mock(CollectionData.class);
MonetaryCurrency loanCurrency = new MonetaryCurrency("CODE", 1, 1);
MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
String delinquentDateAsStr = "2022-12-01";
LocalDate delinquentDate = LocalDate.parse(delinquentDateAsStr);
when(loanForProcessing.getId()).thenReturn(1L);
@@ -263,8 +263,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream()
.map(c -> c.getLoanCharge()).collect(Collectors.toList()));
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
// when
LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event);
@@ -314,7 +312,6 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getFeeAmount().compareTo(new BigDecimal("0.0")));
assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getPenaltyAmount().compareTo(new BigDecimal("0.0")));
assertEquals(0, installmentDelinquencyBucketDataV1_2.getCharges().size());
moneyHelper.close();
}
@Test
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.when;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -85,7 +86,8 @@ public class LoanRepaymentBusinessEventSerializerTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
}
@AfterEach
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -104,7 +105,8 @@ public class LoanDelinquencyDomainServiceTest {
currency = new MonetaryCurrency("USD", 2, null);
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
}
@AfterEach
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
@@ -55,7 +56,8 @@ class DefaultLoanLifecycleStateMachineTest {
public void setUp() {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService);
}
@@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
@@ -87,6 +88,7 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
}
@@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
@@ -88,6 +89,7 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
}
@@ -37,6 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
@@ -82,6 +84,7 @@ public class DefaultScheduledDateGeneratorTest {
public void test_generateRepaymentPeriods() {
// given
HolidayDetailDTO holidayDetailDTO = createHolidayDTO();
MathContext mathContext = new MathContext(12, RoundingMode.HALF_EVEN);
ApplicationCurrency dollarCurrency = new ApplicationCurrency("USD", "US Dollar", 2, 0, "currency.USD", "$");
Money principalAmount = Money.of(fromApplicationCurrency(dollarCurrency), BigDecimal.valueOf(100));
@@ -98,8 +101,8 @@ public class DefaultScheduledDateGeneratorTest {
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null);
// when
List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms,
holidayDetailDTO);
List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate,
loanApplicationTerms, holidayDetailDTO);
// then
assertThat(result.size()).isEqualTo(4);
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -64,7 +65,8 @@ public class LoanCalculateRepaymentPastDueServiceTest {
ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
underTest = new LoanCalculateRepaymentPastDueService();
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
}
@AfterEach
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.MathContext;
import java.math.RoundingMode;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
@@ -75,7 +76,8 @@ public class LoanDownPaymentHandlerServiceImplTest {
@BeforeEach
public void setUp() {
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService);
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
}
@AfterEach