FINERACT-1981: Generate loan schedule as dto
This commit is contained in:
+1
-7
@@ -463,13 +463,7 @@ public final class MathUtil {
|
||||
* @return
|
||||
*/
|
||||
public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage, final int precision) {
|
||||
BigDecimal percentageOf = BigDecimal.ZERO;
|
||||
if (isGreaterThanZero(value)) {
|
||||
final MathContext mc = new MathContext(precision, MoneyHelper.getRoundingMode());
|
||||
final BigDecimal multiplicand = percentage.divide(BigDecimal.valueOf(100L), mc);
|
||||
percentageOf = value.multiply(multiplicand, mc);
|
||||
}
|
||||
return percentageOf;
|
||||
return percentageOf(value, percentage, new MathContext(precision, MoneyHelper.getRoundingMode()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+70
-126
@@ -18,30 +18,19 @@
|
||||
*/
|
||||
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)
|
||||
private String currencyCode;
|
||||
|
||||
@Column(name = "currency_digits")
|
||||
private int currencyDigitsAfterDecimal;
|
||||
|
||||
@Column(name = "currency_multiplesof")
|
||||
private Integer inMultiplesOf;
|
||||
|
||||
@Column(name = "amount", scale = 6, precision = 19)
|
||||
private BigDecimal amount;
|
||||
private final String currencyCode;
|
||||
private final int currencyDigitsAfterDecimal;
|
||||
private final Integer inMultiplesOf;
|
||||
private final BigDecimal amount;
|
||||
|
||||
private final transient MathContext mc;
|
||||
|
||||
@@ -53,6 +42,24 @@ public class Money implements Comparable<Money> {
|
||||
this.mc = getMc();
|
||||
}
|
||||
|
||||
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);
|
||||
BigDecimal amountScaled = amountZeroed.stripTrailingZeros();
|
||||
|
||||
// round monetary amounts into multiples of say 20/50.
|
||||
if (inMultiplesOf != null && this.currencyDigitsAfterDecimal == 0 && inMultiplesOf > 0 && amountScaled.doubleValue() > 0) {
|
||||
final double existingVal = amountScaled.doubleValue();
|
||||
amountScaled = BigDecimal.valueOf(roundToMultiplesOf(existingVal, inMultiplesOf));
|
||||
}
|
||||
this.amount = amountScaled.setScale(this.currencyDigitsAfterDecimal, getMc().getRoundingMode());
|
||||
}
|
||||
|
||||
public static Money total(final Money... monies) {
|
||||
if (monies.length == 0) {
|
||||
throw new IllegalArgumentException("Money array must not be empty");
|
||||
@@ -82,13 +89,11 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public static Money of(final MonetaryCurrency currency, final BigDecimal newAmount) {
|
||||
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), defaultToZeroIfNull(newAmount),
|
||||
currency.getCurrencyInMultiplesOf(), null);
|
||||
return of(currency, newAmount, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
public static Money of(final CurrencyData currency, final BigDecimal newAmount) {
|
||||
return new Money(currency.getCode(), currency.getDecimalPlaces(), defaultToZeroIfNull(newAmount), currency.getInMultiplesOf(),
|
||||
null);
|
||||
return of(currency, newAmount, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
public static Money of(final CurrencyData currency, final BigDecimal newAmount, final MathContext mc) {
|
||||
@@ -96,7 +101,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public static Money zero(final MonetaryCurrency currency) {
|
||||
return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO, currency.getCurrencyInMultiplesOf(), null);
|
||||
return zero(currency, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
public static Money zero(final MonetaryCurrency currency, MathContext mc) {
|
||||
@@ -108,30 +113,11 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public static Money zero(final CurrencyData currency) {
|
||||
return new Money(currency.getCode(), currency.getDecimalPlaces(), BigDecimal.ZERO, currency.getInMultiplesOf(), null);
|
||||
}
|
||||
|
||||
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();
|
||||
BigDecimal amountScaled = amountStripped;
|
||||
|
||||
// round monetary amounts into multiplesof say 20/50.
|
||||
if (inMultiplesOf != null && this.currencyDigitsAfterDecimal == 0 && inMultiplesOf > 0 && amountScaled.doubleValue() > 0) {
|
||||
final double existingVal = amountScaled.doubleValue();
|
||||
amountScaled = BigDecimal.valueOf(roundToMultiplesOf(existingVal, inMultiplesOf));
|
||||
}
|
||||
this.amount = amountScaled.setScale(this.currencyDigitsAfterDecimal, getMc().getRoundingMode());
|
||||
return zero(currency, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
public static double roundToMultiplesOf(final double existingVal, final Integer inMultiplesOf) {
|
||||
double amountScaled = existingVal;
|
||||
double amountScaled;
|
||||
final double ceilingOfValue = ceiling(existingVal, inMultiplesOf);
|
||||
final double floorOfValue = floor(existingVal, inMultiplesOf);
|
||||
|
||||
@@ -148,7 +134,7 @@ public class Money implements Comparable<Money> {
|
||||
|
||||
public static BigDecimal roundToMultiplesOf(final BigDecimal existingVal, final Integer inMultiplesOf) {
|
||||
BigDecimal amountScaled = existingVal;
|
||||
BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf.intValue());
|
||||
BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf);
|
||||
if (inMultiplesOfValue.compareTo(BigDecimal.ZERO) > 0) {
|
||||
amountScaled = existingVal.divide(inMultiplesOfValue, 0, MoneyHelper.getRoundingMode()).multiply(inMultiplesOfValue);
|
||||
}
|
||||
@@ -161,7 +147,7 @@ public class Money implements Comparable<Money> {
|
||||
|
||||
public static Money roundToMultiplesOf(final Money existingVal, final Integer inMultiplesOf, final MathContext mc) {
|
||||
BigDecimal amountScaled = existingVal.getAmount();
|
||||
BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf.intValue());
|
||||
BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf);
|
||||
if (inMultiplesOfValue.compareTo(BigDecimal.ZERO) > 0) {
|
||||
amountScaled = amountScaled.divide(inMultiplesOfValue, 0, mc.getRoundingMode()).multiply(inMultiplesOfValue);
|
||||
}
|
||||
@@ -186,7 +172,7 @@ public class Money implements Comparable<Money> {
|
||||
if ((n < 0 && s > 0) || (n > 0 && s < 0) || (s == 0 && n != 0)) {
|
||||
f = Double.NaN;
|
||||
} else {
|
||||
f = (n == 0 || s == 0) ? 0 : Math.floor(n / s) * s;
|
||||
f = n == 0 ? 0 : Math.floor(n / s) * s;
|
||||
}
|
||||
|
||||
return f;
|
||||
@@ -200,6 +186,14 @@ public class Money implements Comparable<Money> {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BigDecimal defaultToNullIfZero(final BigDecimal value) {
|
||||
BigDecimal result = value;
|
||||
if (value != null && BigDecimal.ZERO.compareTo(value) == 0) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Money copy() {
|
||||
return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros(), this.inMultiplesOf, this.mc);
|
||||
}
|
||||
@@ -214,8 +208,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money plus(final Money moneyToAdd) {
|
||||
final Money toAdd = checkCurrencyEqual(moneyToAdd);
|
||||
return this.plus(toAdd.getAmount());
|
||||
return plus(moneyToAdd, getMc());
|
||||
}
|
||||
|
||||
public Money plus(final Money moneyToAdd, final MathContext mc) {
|
||||
@@ -224,11 +217,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money plus(final BigDecimal amountToAdd) {
|
||||
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.add(amountToAdd);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
return plus(amountToAdd, getMc());
|
||||
}
|
||||
|
||||
public Money plus(final BigDecimal amountToAdd, MathContext mc) {
|
||||
@@ -248,8 +237,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money minus(final Money moneyToSubtract) {
|
||||
final Money toSubtract = checkCurrencyEqual(moneyToSubtract);
|
||||
return this.minus(toSubtract.getAmount());
|
||||
return minus(moneyToSubtract, getMc());
|
||||
}
|
||||
|
||||
public Money minus(final Money moneyToSubtract, final MathContext mc) {
|
||||
@@ -258,8 +246,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money add(final Money moneyToAdd) {
|
||||
final Money toAdd = checkCurrencyEqual(moneyToAdd);
|
||||
return this.add(toAdd.getAmount());
|
||||
return add(moneyToAdd, getMc());
|
||||
}
|
||||
|
||||
public Money add(final Money moneyToAdd, final MathContext mc) {
|
||||
@@ -268,11 +255,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money add(final BigDecimal amountToAdd) {
|
||||
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.add(amountToAdd);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
return add(amountToAdd, getMc());
|
||||
}
|
||||
|
||||
public Money add(final BigDecimal amountToAdd, final MathContext mc) {
|
||||
@@ -284,11 +267,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money minus(final BigDecimal amountToSubtract) {
|
||||
if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.subtract(amountToSubtract);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
return minus(amountToSubtract, getMc());
|
||||
}
|
||||
|
||||
public Money minus(final BigDecimal amountToSubtract, final MathContext mc) {
|
||||
@@ -310,59 +289,39 @@ public class Money implements Comparable<Money> {
|
||||
return this.currencyCode.equals(money.getCurrencyCode());
|
||||
}
|
||||
|
||||
public Money dividedBy(final BigDecimal valueToDivideBy, final RoundingMode roundingMode) {
|
||||
public Money dividedBy(final BigDecimal valueToDivideBy, final MathContext mc) {
|
||||
if (valueToDivideBy.compareTo(BigDecimal.ONE) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.divide(valueToDivideBy, roundingMode);
|
||||
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);
|
||||
final BigDecimal newAmount = this.amount.divide(valueToDivideBy, mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money dividedBy(final double valueToDivideBy, final RoundingMode roundingMode) {
|
||||
public Money dividedBy(final double valueToDivideBy, final MathContext mc) {
|
||||
if (valueToDivideBy == 1) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money dividedBy(final long valueToDivideBy, final RoundingMode roundingMode) {
|
||||
public Money dividedBy(final long valueToDivideBy, final MathContext mc) {
|
||||
if (valueToDivideBy == 1) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), roundingMode);
|
||||
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);
|
||||
final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money multipliedBy(final BigDecimal valueToMultiplyBy) {
|
||||
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
return multipliedBy(valueToMultiplyBy, getMc());
|
||||
}
|
||||
|
||||
public Money multipliedBy(final BigDecimal valueToMultiplyBy, final MathContext mc) {
|
||||
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
|
||||
final BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy, mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
@@ -375,37 +334,33 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money multipliedBy(final long valueToMultiplyBy) {
|
||||
if (valueToMultiplyBy == 1) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy));
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
return multipliedBy(valueToMultiplyBy, getMc());
|
||||
}
|
||||
|
||||
public Money multipliedBy(final long valueToMultiplyBy, final MathContext mc) {
|
||||
if (valueToMultiplyBy == 1) {
|
||||
return this;
|
||||
}
|
||||
final BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy));
|
||||
final BigDecimal newAmount = this.amount.multiply(BigDecimal.valueOf(valueToMultiplyBy), mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money multiplyRetainScale(final BigDecimal valueToMultiplyBy, final RoundingMode roundingMode) {
|
||||
public Money multiplyRetainScale(final BigDecimal valueToMultiplyBy, final MathContext mc) {
|
||||
if (valueToMultiplyBy.compareTo(BigDecimal.ONE) == 0) {
|
||||
return this;
|
||||
}
|
||||
BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy);
|
||||
newAmount = newAmount.setScale(this.currencyDigitsAfterDecimal, roundingMode);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
BigDecimal newAmount = this.amount.multiply(valueToMultiplyBy, mc);
|
||||
newAmount = newAmount.setScale(this.currencyDigitsAfterDecimal, mc.getRoundingMode());
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
public Money multiplyRetainScale(final double valueToMultiplyBy, final RoundingMode roundingMode) {
|
||||
return this.multiplyRetainScale(BigDecimal.valueOf(valueToMultiplyBy), roundingMode);
|
||||
public Money multiplyRetainScale(final double valueToMultiplyBy, final MathContext mc) {
|
||||
return this.multiplyRetainScale(BigDecimal.valueOf(valueToMultiplyBy), mc);
|
||||
}
|
||||
|
||||
public Money percentageOf(BigDecimal percentage, final RoundingMode roundingMode) {
|
||||
final BigDecimal newAmount = this.amount.multiply(percentage).divide(BigDecimal.valueOf(100), roundingMode);
|
||||
return Money.of(monetaryCurrency(), newAmount);
|
||||
public Money percentageOf(BigDecimal percentage, final MathContext mc) {
|
||||
final BigDecimal newAmount = this.amount.multiply(percentage).divide(BigDecimal.valueOf(100), mc);
|
||||
return Money.of(monetaryCurrency(), newAmount, mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -417,7 +372,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public boolean isZero() {
|
||||
return isEqualTo(Money.zero(getCurrency()));
|
||||
return isZero(getMc());
|
||||
}
|
||||
|
||||
public boolean isZero(final MathContext mc) {
|
||||
@@ -441,7 +396,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public boolean isGreaterThanZero() {
|
||||
return isGreaterThan(Money.zero(getCurrency()));
|
||||
return isGreaterThanZero(getMc());
|
||||
}
|
||||
|
||||
public boolean isGreaterThanZero(MathContext mc) {
|
||||
@@ -453,7 +408,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public boolean isLessThanZero() {
|
||||
return isLessThan(Money.zero(getCurrency()));
|
||||
return isLessThanZero(getMc());
|
||||
}
|
||||
|
||||
public boolean isLessThanZero(final MathContext mc) {
|
||||
@@ -468,24 +423,13 @@ public class Money implements Comparable<Money> {
|
||||
return defaultToNullIfZero(this.amount);
|
||||
}
|
||||
|
||||
private static BigDecimal defaultToNullIfZero(final BigDecimal value) {
|
||||
BigDecimal result = value;
|
||||
if (value != null && BigDecimal.ZERO.compareTo(value) == 0) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder().append(this.currencyCode).append(' ').append(this.amount.toPlainString()).toString();
|
||||
}
|
||||
|
||||
public Money negated() {
|
||||
if (isZero()) {
|
||||
return this;
|
||||
}
|
||||
return Money.of(monetaryCurrency(), this.amount.negate());
|
||||
return negated(getMc());
|
||||
}
|
||||
|
||||
public Money negated(final MathContext mc) {
|
||||
@@ -496,7 +440,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money abs() {
|
||||
return isLessThanZero() ? negated() : this;
|
||||
return abs(getMc());
|
||||
}
|
||||
|
||||
public Money abs(MathContext mc) {
|
||||
@@ -512,7 +456,7 @@ public class Money implements Comparable<Money> {
|
||||
}
|
||||
|
||||
public Money zero() {
|
||||
return Money.zero(getCurrency());
|
||||
return zero(getMc());
|
||||
}
|
||||
|
||||
public Money zero(MathContext mc) {
|
||||
|
||||
+2
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
|
||||
@Getter
|
||||
public final class LoanScheduleModelDownPaymentPeriod implements LoanScheduleModelPeriod {
|
||||
|
||||
private final int periodNumber;
|
||||
|
||||
+49
-37
@@ -248,9 +248,20 @@ public final class LoanApplicationTerms {
|
||||
this.fixedLength = builder.fixedLength;
|
||||
this.inArrearsTolerance = builder.inArrearsTolerance;
|
||||
this.disbursementDatas = builder.disbursementDatas;
|
||||
this.downPaymentAmount = builder.downPaymentAmount;
|
||||
this.submittedOnDate = builder.submittedOnDate;
|
||||
this.seedDate = builder.seedDate;
|
||||
this.isDownPaymentEnabled = builder.isDownPaymentEnabled;
|
||||
this.disbursedAmountPercentageForDownPayment = builder.downPaymentPercentage;
|
||||
if (isDownPaymentEnabled) {
|
||||
this.downPaymentAmount = Money.of(getCurrency(),
|
||||
MathUtil.percentageOf(getPrincipal().getAmount(), getDisbursedAmountPercentageForDownPayment(), builder.mc),
|
||||
builder.mc);
|
||||
if (getInstallmentAmountInMultiplesOf() != null) {
|
||||
this.downPaymentAmount = Money.roundToMultiplesOf(this.downPaymentAmount, getInstallmentAmountInMultiplesOf(), builder.mc);
|
||||
}
|
||||
} else {
|
||||
this.downPaymentAmount = Money.zero(getCurrency(), builder.mc);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@@ -273,9 +284,11 @@ public final class LoanApplicationTerms {
|
||||
private Integer fixedLength;
|
||||
private Money inArrearsTolerance;
|
||||
private List<DisbursementData> disbursementDatas;
|
||||
private Money downPaymentAmount;
|
||||
private BigDecimal downPaymentPercentage;
|
||||
private boolean isDownPaymentEnabled;
|
||||
private LocalDate submittedOnDate;
|
||||
private LocalDate seedDate;
|
||||
private MathContext mc;
|
||||
|
||||
public Builder currency(ApplicationCurrency currency) {
|
||||
this.currency = currency;
|
||||
@@ -362,8 +375,8 @@ public final class LoanApplicationTerms {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder downPaymentAmount(Money downPaymentAmount) {
|
||||
this.downPaymentAmount = downPaymentAmount;
|
||||
public Builder downPaymentPercentage(BigDecimal downPaymentPercentage) {
|
||||
this.downPaymentPercentage = downPaymentPercentage;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -377,6 +390,16 @@ public final class LoanApplicationTerms {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isDownPaymentEnabled(boolean isDownPaymentEnabled) {
|
||||
this.isDownPaymentEnabled = isDownPaymentEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder mc(MathContext mc) {
|
||||
this.mc = mc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanApplicationTerms build() {
|
||||
return new LoanApplicationTerms(this);
|
||||
}
|
||||
@@ -384,15 +407,7 @@ public final class LoanApplicationTerms {
|
||||
|
||||
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(), mc), mc);
|
||||
if (modelData.installmentAmountInMultiplesOf() != null) {
|
||||
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, modelData.installmentAmountInMultiplesOf(), mc);
|
||||
}
|
||||
}
|
||||
BigDecimal downPaymentPercentage = modelData.downPaymentPercentage();
|
||||
|
||||
LocalDate seedDate;
|
||||
|
||||
@@ -412,7 +427,8 @@ public final class LoanApplicationTerms {
|
||||
.expectedDisbursementDate(modelData.disbursementDate()).repaymentsStartingFromDate(modelData.scheduleGenerationStartDate())
|
||||
.daysInMonthType(modelData.daysInMonth()).daysInYearType(modelData.daysInYear()).fixedLength(modelData.fixedLength())
|
||||
.inArrearsTolerance(Money.zero(modelData.currency().toData(), mc)).disbursementDatas(new ArrayList<>())
|
||||
.downPaymentAmount(downPaymentAmount).submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate).build();
|
||||
.isDownPaymentEnabled(modelData.downPaymentEnabled()).downPaymentPercentage(downPaymentPercentage)
|
||||
.submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate).mc(mc).build();
|
||||
}
|
||||
|
||||
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
|
||||
@@ -679,7 +695,7 @@ public final class LoanApplicationTerms {
|
||||
} else if (this.actualFixedEmiAmount != null) {
|
||||
final Money difference = this.principal.minus(totalCumulativePrincipalToDate);
|
||||
final Money principalThreshold = principalForPeriod.multipliedBy(this.principalThresholdForLastInstalment).dividedBy(100,
|
||||
MoneyHelper.getRoundingMode());
|
||||
MoneyHelper.getMathContext());
|
||||
if (difference.isLessThan(principalThreshold)) {
|
||||
adjusted = principalForPeriod.plus(difference.abs());
|
||||
}
|
||||
@@ -734,8 +750,7 @@ public final class LoanApplicationTerms {
|
||||
|
||||
final Money totalInterestPerInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc);
|
||||
|
||||
final Money totalGraceOnInterestCharged = totalInterestPerInstallment.multiplyRetainScale(getInterestChargingGrace(),
|
||||
mc.getRoundingMode());
|
||||
final Money totalGraceOnInterestCharged = totalInterestPerInstallment.multiplyRetainScale(getInterestChargingGrace(), mc);
|
||||
|
||||
totalInterestCharged = totalInterestChargedForLoanTerm.minus(totalGraceOnInterestCharged);
|
||||
break;
|
||||
@@ -901,7 +916,7 @@ public final class LoanApplicationTerms {
|
||||
case FLAT:
|
||||
final BigDecimal interestRateForLoanTerm = calculateFlatInterestRateForLoanTerm(calculator, mc);
|
||||
totalInterestDue = this.disbursedPrincipal.minus(totalPrincipalAccountedForInterestCalcualtion)
|
||||
.multiplyRetainScale(interestRateForLoanTerm, mc.getRoundingMode());
|
||||
.multiplyRetainScale(interestRateForLoanTerm, mc);
|
||||
|
||||
break;
|
||||
case DECLINING_BALANCE:
|
||||
@@ -1041,12 +1056,12 @@ public final class LoanApplicationTerms {
|
||||
}
|
||||
|
||||
private Money flatInterestPerInstallment(final MathContext mc, final Money totalInterestForLoanTerm) {
|
||||
Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(
|
||||
Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode());
|
||||
Money interestPerInstallment = totalInterestForLoanTerm
|
||||
.dividedBy(Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc);
|
||||
if (this.excludePeriodsForCalculation < this.periodsCompleted) {
|
||||
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
|
||||
interestPerInstallment = interestLeft.dividedBy(
|
||||
Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.periodsCompleted), mc.getRoundingMode());
|
||||
interestPerInstallment = interestLeft
|
||||
.dividedBy(Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.periodsCompleted), mc);
|
||||
}
|
||||
|
||||
return interestPerInstallment;
|
||||
@@ -1059,19 +1074,17 @@ public final class LoanApplicationTerms {
|
||||
if (getFixedEmiAmount() == null) {
|
||||
if (this.fixedPrincipalPercentagePerInstallment != null) {
|
||||
principalPerPeriod = this.principal.minus(totalPrincipalAccounted)
|
||||
.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc.getRoundingMode())
|
||||
.plus(this.adjustPrincipalForFlatLoans);
|
||||
.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc).plus(this.adjustPrincipalForFlatLoans);
|
||||
} else {
|
||||
principalPerPeriod = this.principal.minus(totalPrincipalAccounted)
|
||||
.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()).plus(this.adjustPrincipalForFlatLoans);
|
||||
principalPerPeriod = this.principal.minus(totalPrincipalAccounted).dividedBy(totalRepaymentsWithCapitalPayment, mc)
|
||||
.plus(this.adjustPrincipalForFlatLoans);
|
||||
}
|
||||
if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) {
|
||||
principalPerPeriod = principalPerPeriod.zero();
|
||||
}
|
||||
if (!isPrincipalGraceApplicableForThisPeriod(periodNumber) && currentPeriodFixedPrincipalAmount != null) {
|
||||
this.adjustPrincipalForFlatLoans = this.adjustPrincipalForFlatLoans
|
||||
.plus(principalPerPeriod.minus(currentPeriodFixedPrincipalAmount)
|
||||
.dividedBy(this.actualNumberOfRepayments - periodNumber, mc.getRoundingMode()));
|
||||
this.adjustPrincipalForFlatLoans = this.adjustPrincipalForFlatLoans.plus(principalPerPeriod
|
||||
.minus(currentPeriodFixedPrincipalAmount).dividedBy(this.actualNumberOfRepayments - periodNumber, mc));
|
||||
principalPerPeriod = this.principal.zero().plus(currentPeriodFixedPrincipalAmount);
|
||||
|
||||
}
|
||||
@@ -1146,13 +1159,12 @@ public final class LoanApplicationTerms {
|
||||
|
||||
Integer interestPaymentDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
|
||||
this.excludePeriodsForCalculation);
|
||||
interestForInstallment = realTotalInterestForLoan.dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods),
|
||||
mc.getRoundingMode());
|
||||
interestForInstallment = realTotalInterestForLoan.dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods), mc);
|
||||
if (this.excludePeriodsForCalculation < this.periodsCompleted) {
|
||||
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
|
||||
Integer interestDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
|
||||
this.periodsCompleted);
|
||||
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc.getRoundingMode());
|
||||
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc);
|
||||
}
|
||||
if (!this.periodNumbersApplicableForInterestGrace.isEmpty()) {
|
||||
int periodsElapsed = calculateLastInterestGracePeriod(periodNumber);
|
||||
@@ -1160,7 +1172,7 @@ public final class LoanApplicationTerms {
|
||||
Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted);
|
||||
Integer interestDuePeriods = calculateNumberOfRemainingInterestPaymentPeriods(this.actualNumberOfRepayments,
|
||||
periodsElapsed);
|
||||
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc.getRoundingMode());
|
||||
interestForInstallment = interestLeft.dividedBy(Long.valueOf(interestDuePeriods), mc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,7 +1428,7 @@ public final class LoanApplicationTerms {
|
||||
|
||||
final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType,
|
||||
periodStartDate, periodEndDate);// 0.021232877 ob:14911.64
|
||||
interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode());
|
||||
interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc);
|
||||
|
||||
return interestDue;
|
||||
}
|
||||
@@ -1463,11 +1475,11 @@ public final class LoanApplicationTerms {
|
||||
if (this.fixedPrincipalAmount == null) {
|
||||
final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments,
|
||||
periodNumber);
|
||||
principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode());
|
||||
principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc);
|
||||
this.fixedPrincipalAmount = principal.getAmount();
|
||||
}
|
||||
if (this.fixedPrincipalPercentagePerInstallment != null) {
|
||||
principal = this.principal.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc.getRoundingMode());
|
||||
principal = this.principal.percentageOf(this.fixedPrincipalPercentagePerInstallment, mc);
|
||||
} else {
|
||||
principal = Money.of(getCurrency(), getFixedPrincipalAmount());
|
||||
}
|
||||
@@ -1480,7 +1492,7 @@ public final class LoanApplicationTerms {
|
||||
public void updateFixedPrincipalAmount(final MathContext mc, final int periodNumber, final Money outstandingAmount) {
|
||||
final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments,
|
||||
periodNumber - 1);
|
||||
Money principal = outstandingAmount.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode());
|
||||
Money principal = outstandingAmount.dividedBy(numberOfPrincipalPaymentPeriods, mc);
|
||||
this.fixedPrincipalAmount = principal.getAmount();
|
||||
}
|
||||
|
||||
|
||||
+5
-12
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@@ -30,16 +31,20 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
||||
/**
|
||||
* Domain representation of a Loan Schedule (not used for persistence)
|
||||
*/
|
||||
@Getter
|
||||
public final class LoanScheduleModel {
|
||||
|
||||
@Getter
|
||||
private final List<LoanScheduleModelPeriod> periods;
|
||||
private final ApplicationCurrency applicationCurrency;
|
||||
private final int loanTermInDays;
|
||||
private final Money totalPrincipalDisbursed;
|
||||
private final BigDecimal totalPrincipalExpected;
|
||||
private final BigDecimal totalPrincipalPaid;
|
||||
@Getter
|
||||
private final BigDecimal totalInterestCharged;
|
||||
private final BigDecimal totalFeeChargesCharged;
|
||||
@Getter
|
||||
private final BigDecimal totalPenaltyChargesCharged;
|
||||
private final BigDecimal totalRepaymentExpected;
|
||||
private final BigDecimal totalOutstanding;
|
||||
@@ -114,16 +119,4 @@ public final class LoanScheduleModel {
|
||||
totalPaidInAdvance, totalPaidLate, this.totalOutstanding, totalCredits);
|
||||
}
|
||||
|
||||
public List<LoanScheduleModelPeriod> getPeriods() {
|
||||
return this.periods;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalPenaltyChargesCharged() {
|
||||
return this.totalPenaltyChargesCharged;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalInterestCharged() {
|
||||
return this.totalInterestCharged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||
@@ -28,6 +29,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
||||
/**
|
||||
* Domain representation of a Loan Schedule Disbursement Period (not used for persistence)
|
||||
*/
|
||||
@Getter
|
||||
public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleModelPeriod {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
+6
-6
@@ -828,7 +828,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
candidateRepaymentInstallments.forEach(i -> {
|
||||
@@ -1194,7 +1194,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case IN_ADVANCE -> {
|
||||
int numberOfInstallments = inAdvanceInstallments.size();
|
||||
if (numberOfInstallments > 0) {
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
||||
@@ -1262,7 +1262,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
refundedPortion = zero;
|
||||
if (numberOfInstallments > 0) {
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
currentInstallment = internalCurrentInstallment;
|
||||
@@ -1467,7 +1467,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
@@ -1645,7 +1645,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
@@ -1723,7 +1723,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Money adjustCalculatedPrincipal = Money.zero(currency);
|
||||
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
|
||||
calculatedPrincipal = outstandingPrincipalBalance.get()
|
||||
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getRoundingMode());
|
||||
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext());
|
||||
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
|
||||
@Data
|
||||
public class LoanSchedulePlan {
|
||||
|
||||
private final List<LoanSchedulePlanPeriod> periods;
|
||||
private final CurrencyData currency;
|
||||
private final int loanTermInDays;
|
||||
private final BigDecimal totalDisbursedAmount;
|
||||
private final BigDecimal totalPrincipalAmount;
|
||||
private final BigDecimal totalInterestAmount;
|
||||
private final BigDecimal totalFeeAmount;
|
||||
private final BigDecimal totalPenaltyAmount;
|
||||
private final BigDecimal totalRepaymentAmount;
|
||||
private final BigDecimal totalOutstandingAmount;
|
||||
|
||||
public static LoanSchedulePlan from(LoanScheduleModel model) {
|
||||
List<LoanSchedulePlanPeriod> periods = new ArrayList<>();
|
||||
|
||||
model.getPeriods().forEach(periodModel -> {
|
||||
LoanSchedulePlanPeriod periodPlan = null;
|
||||
if (periodModel instanceof LoanScheduleModelDisbursementPeriod disbursementPeriod) {
|
||||
periodPlan = new LoanSchedulePlanDisbursementPeriod(disbursementPeriod.getDisbursementDate(), //
|
||||
disbursementPeriod.getDisbursementDate(), //
|
||||
disbursementPeriod.getPrincipalDisbursed().getAmount(), //
|
||||
disbursementPeriod.getPrincipalDisbursed().getAmount());//
|
||||
} else if (periodModel instanceof LoanScheduleModelDownPaymentPeriod downPaymentPeriod) {
|
||||
periodPlan = new LoanSchedulePlanDownPaymentPeriod(downPaymentPeriod.getPeriodNumber(), //
|
||||
downPaymentPeriod.getPeriodDate(), //
|
||||
downPaymentPeriod.getPeriodDate(), //
|
||||
downPaymentPeriod.getPrincipalDue().getAmount(), //
|
||||
downPaymentPeriod.getPrincipalDue().getAmount(), //
|
||||
downPaymentPeriod.getOutstandingLoanBalance().getAmount());//
|
||||
} else if (periodModel instanceof LoanScheduleModelRepaymentPeriod repaymentPeriod) {
|
||||
periodPlan = new LoanSchedulePlanRepaymentPeriod(repaymentPeriod.getPeriodNumber(), //
|
||||
repaymentPeriod.getFromDate(), //
|
||||
repaymentPeriod.getDueDate(), //
|
||||
repaymentPeriod.getPrincipalDue().getAmount(), //
|
||||
repaymentPeriod.getInterestDue().getAmount(), //
|
||||
repaymentPeriod.getFeeChargesDue().getAmount(), //
|
||||
repaymentPeriod.getPenaltyChargesDue().getAmount(), //
|
||||
repaymentPeriod.getTotalDue().getAmount(), //
|
||||
repaymentPeriod.getOutstandingLoanBalance().getAmount());//
|
||||
}
|
||||
if (periodPlan != null) {
|
||||
periods.add(periodPlan);
|
||||
}
|
||||
});
|
||||
|
||||
return new LoanSchedulePlan(periods, //
|
||||
model.getApplicationCurrency().toData(), //
|
||||
model.getLoanTermInDays(), //
|
||||
model.getTotalPrincipalDisbursed().getAmount(), //
|
||||
model.getTotalPrincipalExpected(), //
|
||||
model.getTotalInterestCharged(), //
|
||||
model.getTotalFeeChargesCharged(), //
|
||||
model.getTotalPenaltyChargesCharged(), //
|
||||
model.getTotalRepaymentExpected(), //
|
||||
model.getTotalOutstanding()//
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class LoanSchedulePlanDisbursementPeriod implements LoanSchedulePlanPeriod {
|
||||
|
||||
private final LocalDate periodFromDate;
|
||||
private final LocalDate periodDueDate;
|
||||
private final BigDecimal principalAmount;
|
||||
private final BigDecimal outstandingLoanBalance;
|
||||
|
||||
@Override
|
||||
public Integer periodNumber() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodFromDate() {
|
||||
return periodFromDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodDueDate() {
|
||||
return periodDueDate;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class LoanSchedulePlanDownPaymentPeriod implements LoanSchedulePlanPeriod {
|
||||
|
||||
private final int periodNumber;
|
||||
private final LocalDate periodFromDate;
|
||||
private final LocalDate periodDueDate;
|
||||
private final BigDecimal principalAmount;
|
||||
private final BigDecimal totalDueAmount;
|
||||
private final BigDecimal outstandingLoanBalance;
|
||||
|
||||
@Override
|
||||
public Integer periodNumber() {
|
||||
return periodNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodFromDate() {
|
||||
return periodFromDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodDueDate() {
|
||||
return periodDueDate;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public interface LoanSchedulePlanPeriod {
|
||||
|
||||
Integer periodNumber();
|
||||
|
||||
LocalDate periodFromDate();
|
||||
|
||||
LocalDate periodDueDate();
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class LoanSchedulePlanRepaymentPeriod implements LoanSchedulePlanPeriod {
|
||||
|
||||
private final int periodNumber;
|
||||
private final LocalDate periodFromDate;
|
||||
private final LocalDate periodDueDate;
|
||||
private final BigDecimal principalAmount;
|
||||
private final BigDecimal interestAmount;
|
||||
private final BigDecimal feeAmount;
|
||||
private final BigDecimal penaltyAmount;
|
||||
private final BigDecimal totalDueAmount;
|
||||
private final BigDecimal outstandingLoanBalance;
|
||||
|
||||
@Override
|
||||
public Integer periodNumber() {
|
||||
return periodNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodFromDate() {
|
||||
return periodFromDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate periodDueDate() {
|
||||
return periodDueDate;
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
@@ -165,11 +166,10 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
|
||||
}
|
||||
|
||||
public LoanScheduleModel generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
|
||||
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
|
||||
|
||||
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
|
||||
|
||||
return generate(mc, loanApplicationTerms, null, null);
|
||||
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
|
||||
}
|
||||
|
||||
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
|
||||
@@ -221,22 +221,22 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
Money downPaymentAmount = Money.zero(loanApplicationTerms.getCurrency(), mc);
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(), MathUtil.percentageOf(disbursedAmount.getAmount(),
|
||||
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
|
||||
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), mc), mc);
|
||||
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
|
||||
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount,
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
|
||||
}
|
||||
|
||||
LoanScheduleModelDownPaymentPeriod downPaymentPeriod = LoanScheduleModelDownPaymentPeriod.downPayment(
|
||||
scheduleParams.getInstalmentNumber(), disbursementDate, downPaymentAmount,
|
||||
outstandingBalance.plus(disbursedAmount).minus(downPaymentAmount));
|
||||
outstandingBalance.plus(disbursedAmount, mc).minus(downPaymentAmount, mc));
|
||||
periods.add(downPaymentPeriod);
|
||||
|
||||
scheduleParams.addTotalRepaymentExpected(downPaymentAmount);
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
}
|
||||
|
||||
final Money disbursementRemainingBalance = disbursedAmount.minus(downPaymentAmount);
|
||||
final Money disbursementRemainingBalance = disbursedAmount.minus(downPaymentAmount, mc);
|
||||
scheduleParams.addPrincipalToBeScheduled(disbursementRemainingBalance);
|
||||
emiCalculator.addDisbursement(interestScheduleModel, disbursementDate, disbursementRemainingBalance);
|
||||
}
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
|
||||
|
||||
if (shouldBeAdjusted) {
|
||||
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc.getRoundingMode(), mc);
|
||||
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc);
|
||||
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
|
||||
originalEmi.plus(adjustment, mc));
|
||||
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
|
||||
|
||||
+95
-39
@@ -19,79 +19,135 @@
|
||||
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 java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoanScheduleGeneratorTest {
|
||||
|
||||
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
|
||||
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
|
||||
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
|
||||
private static final BigDecimal DISBURSEMENT_AMOUNT_100 = BigDecimal.valueOf(100);
|
||||
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);
|
||||
private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
|
||||
private static final BigDecimal DOWN_PAYMENT_PORTION = BigDecimal.valueOf(25);
|
||||
|
||||
@Test
|
||||
void testGenerateLoanSchedule() {
|
||||
List<LoanScheduleModelPeriod> periods = getLoanScheduleModelPeriods();
|
||||
|
||||
assertEquals(7, periods.size(), "Expected 7 periods including the downpayment period.");
|
||||
|
||||
LoanScheduleModelDisbursementPeriod disbursementPeriod = (LoanScheduleModelDisbursementPeriod) periods.get(0);
|
||||
assertNotNull(disbursementPeriod);
|
||||
|
||||
checkPeriod(periods.get(1), 1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(31.97),
|
||||
BigDecimal.valueOf(0.88), BigDecimal.valueOf(32.85), BigDecimal.valueOf(160.25));
|
||||
checkPeriod(periods.get(2), 2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(31.52),
|
||||
BigDecimal.valueOf(1.33), BigDecimal.valueOf(32.85), BigDecimal.valueOf(128.73));
|
||||
checkPeriod(periods.get(3), 3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(31.78),
|
||||
BigDecimal.valueOf(1.07), BigDecimal.valueOf(32.85), BigDecimal.valueOf(96.95));
|
||||
checkPeriod(periods.get(4), 4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(32.04),
|
||||
BigDecimal.valueOf(0.81), BigDecimal.valueOf(32.85), BigDecimal.valueOf(64.91));
|
||||
checkPeriod(periods.get(5), 5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(32.31),
|
||||
BigDecimal.valueOf(0.54), BigDecimal.valueOf(32.85), BigDecimal.valueOf(32.60));
|
||||
checkPeriod(periods.get(6), 6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(32.60),
|
||||
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);
|
||||
NOMINAL_INTEREST_RATE, false, 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();
|
||||
LoanSchedulePlan loanSchedule = generator.generate(mc, modelData);
|
||||
|
||||
assertEquals(7, loanSchedule.getPeriods().size(), "Expected 7 periods without the downpayment period.");
|
||||
|
||||
checkDisbursementPeriod((LoanSchedulePlanDisbursementPeriod) loanSchedule.getPeriods().get(0), LocalDate.of(2024, 1, 15),
|
||||
DISBURSEMENT_AMOUNT, DISBURSEMENT_AMOUNT);
|
||||
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(1), 1, LocalDate.of(2024, 1, 1),
|
||||
LocalDate.of(2024, 2, 1), BigDecimal.valueOf(31.97), BigDecimal.valueOf(0.88), BigDecimal.valueOf(32.85),
|
||||
BigDecimal.valueOf(160.25));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(2), 2, LocalDate.of(2024, 2, 1),
|
||||
LocalDate.of(2024, 3, 1), BigDecimal.valueOf(31.52), BigDecimal.valueOf(1.33), BigDecimal.valueOf(32.85),
|
||||
BigDecimal.valueOf(128.73));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(3), 3, LocalDate.of(2024, 3, 1),
|
||||
LocalDate.of(2024, 4, 1), BigDecimal.valueOf(31.78), BigDecimal.valueOf(1.07), BigDecimal.valueOf(32.85),
|
||||
BigDecimal.valueOf(96.95));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(4), 4, LocalDate.of(2024, 4, 1),
|
||||
LocalDate.of(2024, 5, 1), BigDecimal.valueOf(32.04), BigDecimal.valueOf(0.81), BigDecimal.valueOf(32.85),
|
||||
BigDecimal.valueOf(64.91));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(5), 5, LocalDate.of(2024, 5, 1),
|
||||
LocalDate.of(2024, 6, 1), BigDecimal.valueOf(32.31), BigDecimal.valueOf(0.54), BigDecimal.valueOf(32.85),
|
||||
BigDecimal.valueOf(32.60));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(6), 6, LocalDate.of(2024, 6, 1),
|
||||
LocalDate.of(2024, 7, 1), BigDecimal.valueOf(32.60), BigDecimal.valueOf(0.27), BigDecimal.valueOf(32.87), BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
private void checkPeriod(LoanScheduleModelPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
|
||||
@Test
|
||||
void testGenerateLoanScheduleWithDownPayment() {
|
||||
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
|
||||
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
|
||||
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null);
|
||||
|
||||
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
|
||||
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator);
|
||||
|
||||
LoanSchedulePlan loanSchedule = generator.generate(mc, modelData);
|
||||
|
||||
assertEquals(8, loanSchedule.getPeriods().size(), "Expected 8 periods with the downpayment period.");
|
||||
|
||||
checkDisbursementPeriod((LoanSchedulePlanDisbursementPeriod) loanSchedule.getPeriods().get(0), LocalDate.of(2024, 1, 1),
|
||||
DISBURSEMENT_AMOUNT_100, DISBURSEMENT_AMOUNT_100);
|
||||
checkDownPaymentPeriod((LoanSchedulePlanDownPaymentPeriod) loanSchedule.getPeriods().get(1), 1, LocalDate.of(2024, 1, 1),
|
||||
LocalDate.of(2024, 1, 1), BigDecimal.valueOf(25.0), BigDecimal.valueOf(25.0), BigDecimal.valueOf(75.0));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(2), 2, LocalDate.of(2024, 1, 1),
|
||||
LocalDate.of(2024, 2, 1), BigDecimal.valueOf(12.25), BigDecimal.valueOf(0.62), BigDecimal.valueOf(12.87),
|
||||
BigDecimal.valueOf(62.75));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(3), 3, LocalDate.of(2024, 2, 1),
|
||||
LocalDate.of(2024, 3, 1), BigDecimal.valueOf(12.35), BigDecimal.valueOf(0.52), BigDecimal.valueOf(12.87),
|
||||
BigDecimal.valueOf(50.40));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(4), 4, LocalDate.of(2024, 3, 1),
|
||||
LocalDate.of(2024, 4, 1), BigDecimal.valueOf(12.45), BigDecimal.valueOf(0.42), BigDecimal.valueOf(12.87),
|
||||
BigDecimal.valueOf(37.95));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(5), 5, LocalDate.of(2024, 4, 1),
|
||||
LocalDate.of(2024, 5, 1), BigDecimal.valueOf(12.55), BigDecimal.valueOf(0.32), BigDecimal.valueOf(12.87),
|
||||
BigDecimal.valueOf(25.40));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(6), 6, LocalDate.of(2024, 5, 1),
|
||||
LocalDate.of(2024, 6, 1), BigDecimal.valueOf(12.66), BigDecimal.valueOf(0.21), BigDecimal.valueOf(12.87),
|
||||
BigDecimal.valueOf(12.74));
|
||||
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(7), 7, LocalDate.of(2024, 6, 1),
|
||||
LocalDate.of(2024, 7, 1), BigDecimal.valueOf(12.74), BigDecimal.valueOf(0.11), BigDecimal.valueOf(12.85), BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
private void checkDisbursementPeriod(LoanSchedulePlanDisbursementPeriod period, LocalDate disbursementDate, BigDecimal principalAmount,
|
||||
BigDecimal outstandingAmount) {
|
||||
assertEquals(disbursementDate, period.getPeriodFromDate());
|
||||
assertEquals(disbursementDate, period.getPeriodDueDate());
|
||||
assertEquals(0, principalAmount.compareTo(period.getPrincipalAmount()));
|
||||
assertEquals(0, outstandingAmount.compareTo(period.getOutstandingLoanBalance()));
|
||||
|
||||
}
|
||||
|
||||
private void checkRepaymentPeriod(LoanSchedulePlanRepaymentPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
|
||||
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedInterestDue, BigDecimal expectedTotalDue,
|
||||
BigDecimal expectedOutstandingLoanBalance) {
|
||||
LoanScheduleModelRepaymentPeriod repaymentPeriod = (LoanScheduleModelRepaymentPeriod) period;
|
||||
assertEquals(expectedPeriodNumber, repaymentPeriod.getPeriodNumber());
|
||||
assertEquals(expectedFromDate, repaymentPeriod.getFromDate());
|
||||
assertEquals(expectedDueDate, repaymentPeriod.getDueDate());
|
||||
assertEquals(0, expectedPrincipalDue.compareTo(repaymentPeriod.getPrincipalDue().getAmount()));
|
||||
assertEquals(0, expectedInterestDue.compareTo(repaymentPeriod.getInterestDue().getAmount()));
|
||||
assertEquals(0, expectedTotalDue.compareTo(repaymentPeriod.getTotalDue().getAmount()));
|
||||
assertEquals(0, expectedOutstandingLoanBalance.compareTo(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
|
||||
assertEquals(expectedPeriodNumber, period.getPeriodNumber());
|
||||
assertEquals(expectedFromDate, period.getPeriodFromDate());
|
||||
assertEquals(expectedDueDate, period.getPeriodDueDate());
|
||||
assertEquals(0, expectedPrincipalDue.compareTo(period.getPrincipalAmount()));
|
||||
assertEquals(0, expectedInterestDue.compareTo(period.getInterestAmount()));
|
||||
assertEquals(0, expectedTotalDue.compareTo(period.getTotalDueAmount()));
|
||||
assertEquals(0, expectedOutstandingLoanBalance.compareTo(period.getOutstandingLoanBalance()));
|
||||
}
|
||||
|
||||
private void checkDownPaymentPeriod(LoanSchedulePlanDownPaymentPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
|
||||
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedTotalDue,
|
||||
BigDecimal expectedOutstandingLoanBalance) {
|
||||
assertEquals(expectedPeriodNumber, period.getPeriodNumber());
|
||||
assertEquals(expectedFromDate, period.getPeriodFromDate());
|
||||
assertEquals(expectedDueDate, period.getPeriodDueDate());
|
||||
assertEquals(0, expectedPrincipalDue.compareTo(period.getPrincipalAmount()));
|
||||
assertEquals(0, expectedTotalDue.compareTo(period.getTotalDueAmount()));
|
||||
assertEquals(0, expectedOutstandingLoanBalance.compareTo(period.getOutstandingLoanBalance()));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ public class ProvisioningEntriesWritePlatformServiceJpaRepositoryImpl implements
|
||||
GLAccount expenseAccount = glAccountRepository.findById(data.getExpenseAccount()).orElseThrow();
|
||||
MonetaryCurrency currency = loanProduct.getPrincipalAmount().getCurrency();
|
||||
Money money = Money.of(currency, data.getBalance());
|
||||
Money amountToReserve = money.percentageOf(data.getPercentage(), MoneyHelper.getRoundingMode());
|
||||
Money amountToReserve = money.percentageOf(data.getPercentage(), MoneyHelper.getMathContext());
|
||||
Long criteraId = data.getCriteriaId();
|
||||
LoanProductProvisioningEntry entry = new LoanProductProvisioningEntry().setLoanProduct(loanProduct).setOffice(office)
|
||||
.setCurrencyCode(data.getCurrencyCode()).setProvisioningCategory(provisioningCategory)
|
||||
|
||||
Reference in New Issue
Block a user