FINERACT-2326: Add the ability to reschedule non-interest-bearing progressive loans to have interest
This commit is contained in:
+23
-5
@@ -507,7 +507,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
|
||||
|
||||
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
&& loanTransaction.getLoan().isInterestRecalculationEnabled()) {
|
||||
var model = progressiveTransactionCtx.getModel();
|
||||
|
||||
// Copy and paste Logic from super.handleCreditBalanceRefund
|
||||
@@ -778,7 +778,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
if (loanTransaction.getLoan().isInterestRecalculationEnabled()) {
|
||||
processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx);
|
||||
} else if (hasNoCustomCreditAllocationRule(loanTransaction)) {
|
||||
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
|
||||
@@ -1224,8 +1224,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
|
||||
final Loan loan = disbursementTransaction.getLoan();
|
||||
if (loan.isInterestBearing()) {
|
||||
if (shouldUseEmiCalculation(transactionCtx, disbursementTransaction.getTransactionDate())) {
|
||||
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
|
||||
} else {
|
||||
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
|
||||
@@ -1356,7 +1355,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||
if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) {
|
||||
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
|
||||
if (shouldUseEmiCalculation(transactionCtx, capitalizedIncomeTransaction.getTransactionDate())) {
|
||||
handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||
} else {
|
||||
handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||
@@ -1476,6 +1476,24 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldUseEmiCalculation(TransactionCtx transactionCtx, LocalDate transactionDate) {
|
||||
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
|
||||
boolean hasActiveReAmortization = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream()
|
||||
.anyMatch(t -> t.getTypeOf().isReAmortize() && t.isNotReversed());
|
||||
boolean hasActiveReAge = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream()
|
||||
.anyMatch(t -> t.getTypeOf().isReAge() && t.isNotReversed());
|
||||
if (hasActiveReAmortization) {
|
||||
return false;
|
||||
} else {
|
||||
return !hasActiveReAge || !DateUtils.isAfter(transactionDate, progressiveTransactionCtx.getModel().getMaturityDate());
|
||||
}
|
||||
}
|
||||
// From now on we are defaulting to using the EMICalculator on all progressive loans. However currently the
|
||||
// model is not aware of re-aging and re-amortization. So only these specific cases should ignore this
|
||||
// requirement. This method can be removed once these operations are supported by the EMI model.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) {
|
||||
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
}
|
||||
|
||||
+10
-3
@@ -24,7 +24,6 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveRescheduleFromDate;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateInterestRate;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
|
||||
@@ -78,8 +77,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
|
||||
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
|
||||
BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper,
|
||||
jsonElement, dataValidatorBuilder);
|
||||
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
|
||||
|
||||
boolean hasInterestRateChange = interestRate != null;
|
||||
@@ -197,4 +195,13 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
|
||||
"Interest rate change for the provided date is already exists.", rescheduleFromDate);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal validateInterestRate(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
|
||||
DataValidatorBuilder dataValidatorBuilder) {
|
||||
final BigDecimal interestRate = fromJsonHelper
|
||||
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
|
||||
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
|
||||
.zeroOrPositiveAmount();
|
||||
return interestRate;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -182,7 +182,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
}
|
||||
|
||||
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) {
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
|
||||
if (loan.isInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
} else {
|
||||
reprocessLoanTransactionsService.processLatestTransaction(transaction, loan);
|
||||
|
||||
+8
-2
@@ -161,6 +161,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate()))
|
||||
.forEach(rp -> rp.setTotalCapitalizedIncomeAmount(
|
||||
MathUtil.nullToZero(rp.getTotalCapitalizedIncomeAmount()).add(operation.getAmount().getAmount())));
|
||||
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(),
|
||||
scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
|
||||
calculateEMIValueAndRateFactors(
|
||||
@@ -920,9 +924,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
// already repaid principals should be subtracted from total disbursed amount to calculate correct EMI.
|
||||
BigDecimal alreadyRepaidPrincipals = first.getPrevious()
|
||||
.map(rp -> rp.calculateTotalDisbursedAmountTillGivenPeriod(null).subtract(rp.getOutstandingLoanBalance().getAmount()))
|
||||
.map(rp -> rp.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(null)
|
||||
.subtract(rp.getOutstandingLoanBalance().getAmount()))
|
||||
.orElse(BigDecimal.ZERO);
|
||||
Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod()))
|
||||
Money total = Money
|
||||
.of(currency, first.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(first.getLastInterestPeriod()))
|
||||
.plus(sumOfInterest).minus(alreadyRepaidPrincipals);
|
||||
|
||||
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal baseAmount = switch (method) {
|
||||
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this));
|
||||
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this));
|
||||
case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount();
|
||||
default -> throw new UnsupportedOperationException("Method not implemented: " + method);
|
||||
};
|
||||
|
||||
+14
-5
@@ -84,6 +84,10 @@ public final class RepaymentPeriod {
|
||||
@Getter
|
||||
private BigDecimal totalDisbursedAmount;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private BigDecimal totalCapitalizedIncomeAmount;
|
||||
|
||||
@JsonExclude
|
||||
@Getter
|
||||
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
|
||||
@@ -389,16 +393,21 @@ public final class RepaymentPeriod {
|
||||
/**
|
||||
* @param tillPeriod
|
||||
* can be null. if null it calculates total disbursement including last interest period.
|
||||
* @return disbursed amount til interest period.
|
||||
* @return disbursed and capitalized income amount till interest period.
|
||||
*/
|
||||
public BigDecimal calculateTotalDisbursedAmountTillGivenPeriod(InterestPeriod tillPeriod) {
|
||||
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount());
|
||||
public BigDecimal calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) {
|
||||
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()).add(MathUtil.nullToZero(getTotalCapitalizedIncomeAmount()));
|
||||
for (InterestPeriod interestPeriod : this.interestPeriods) {
|
||||
if (interestPeriod.equals(tillPeriod)) {
|
||||
break;
|
||||
}
|
||||
if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) {
|
||||
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
|
||||
if (!interestPeriod.getDueDate().equals(getFromDate())) {
|
||||
if (interestPeriod.getDisbursementAmount() != null) {
|
||||
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
|
||||
}
|
||||
if (interestPeriod.getCapitalizedIncomePrincipal() != null) {
|
||||
res = res.add(interestPeriod.getCapitalizedIncomePrincipal().getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
|
||||
-1
@@ -535,7 +535,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
Loan loan = mock(Loan.class);
|
||||
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loan.isInterestBearing()).thenReturn(true);
|
||||
|
||||
LoanRepaymentScheduleInstallment installment1 = spy(
|
||||
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(200.0),
|
||||
|
||||
Reference in New Issue
Block a user