FINERACT-1981: Repayment Schedule Interest Model with multi disbursement support, AdvPymnTxProcessor fix
This commit is contained in:
committed by
Adam Saghy
parent
26388ea518
commit
69dd19c74b
@@ -5237,8 +5237,8 @@ Feature: Loan
|
||||
| 2 | 15 | 16 January 2024 | 16 January 2024 | 250.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 125.0 | 0.0 | 0.0 | 0.0 |
|
||||
| | | 16 January 2024 | | 500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 3 | 0 | 16 January 2024 | 16 January 2024 | 625.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 125.0 | 0.0 | 0.0 | 0.0 |
|
||||
| 4 | 15 | 31 January 2024 | | 312.0 | 313.0 | 0.0 | 0.0 | 0.0 | 313.0 | 125.0 | 125.0 | 0.0 | 188.0 |
|
||||
| 5 | 15 | 15 February 2024 | | 0.0 | 312.0 | 0.0 | 0.0 | 0.0 | 312.0 | 125.0 | 125.0 | 0.0 | 187.0 |
|
||||
| 4 | 15 | 31 January 2024 | | 313.0 | 312.0 | 0.0 | 0.0 | 0.0 | 312.0 | 125.0 | 125.0 | 0.0 | 187.0 |
|
||||
| 5 | 15 | 15 February 2024 | | 0.0 | 313.0 | 0.0 | 0.0 | 0.0 | 313.0 | 125.0 | 125.0 | 0.0 | 188.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 0 | 0.0 | 0 | 1000.0 | 625.0 | 250.0 | 0 | 375.0 |
|
||||
@@ -5274,8 +5274,8 @@ Feature: Loan
|
||||
| 2 | 15 | 16 January 2024 | 16 January 2024 | 250.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 125.0 | 0.0 | 0.0 | 0.0 |
|
||||
| | | 16 January 2024 | | 500.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 3 | 0 | 16 January 2024 | 16 January 2024 | 625.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 125.0 | 0.0 | 0.0 | 0.0 |
|
||||
| 4 | 15 | 31 January 2024 | | 312.0 | 313.0 | 0.0 | 0.0 | 0.0 | 313.0 | 125.0 | 125.0 | 0.0 | 188.0 |
|
||||
| 5 | 15 | 15 February 2024 | | 0.0 | 312.0 | 0.0 | 0.0 | 0.0 | 312.0 | 125.0 | 125.0 | 0.0 | 187.0 |
|
||||
| 4 | 15 | 31 January 2024 | | 313.0 | 312.0 | 0.0 | 0.0 | 0.0 | 312.0 | 125.0 | 125.0 | 0.0 | 187.0 |
|
||||
| 5 | 15 | 15 February 2024 | | 0.0 | 313.0 | 0.0 | 0.0 | 0.0 | 313.0 | 125.0 | 125.0 | 0.0 | 188.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 0 | 0.0 | 0 | 1000.0 | 625.0 | 250.0 | 0 | 375.0 |
|
||||
|
||||
@@ -2446,8 +2446,8 @@ Feature: Loan DownPayment
|
||||
| 2 | 15 | 16 February 2024 | 16 February 2024 | 250.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | 125.0 | 0.0 | 0.0 | 0.0 |
|
||||
| | | 20 February 2024 | | 625.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 3 | 0 | 20 February 2024 | 20 February 2024 | 719.0 | 156.0 | 0.0 | 0.0 | 0.0 | 156.0 | 156.0 | 0.0 | 0.0 | 0.0 |
|
||||
| 4 | 15 | 02 March 2024 | 20 February 2024 | 360.0 | 359.0 | 0.0 | 0.0 | 0.0 | 359.0 | 359.0 | 359.0 | 0.0 | 0.0 |
|
||||
| 5 | 15 | 17 March 2024 | 20 February 2024 | 0.0 | 360.0 | 0.0 | 0.0 | 0.0 | 360.0 | 360.0 | 360.0 | 0.0 | 0.0 |
|
||||
| 4 | 15 | 02 March 2024 | 20 February 2024 | 359.0 | 360.0 | 0.0 | 0.0 | 0.0 | 360.0 | 360.0 | 360.0 | 0.0 | 0.0 |
|
||||
| 5 | 15 | 17 March 2024 | 20 February 2024 | 0.0 | 359.0 | 0.0 | 0.0 | 0.0 | 359.0 | 359.0 | 359.0 | 0.0 | 0.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1125.0 | 0.0 | 0.0 | 0.0 | 1125.0 | 1125.0 | 719.0 | 0.0 | 0.0 |
|
||||
|
||||
+9
-9
@@ -111,8 +111,8 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
|
||||
@@ -801,8 +801,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
final Set<LoanCharge> loanCharges = new HashSet<>(1);
|
||||
loanCharges.add(charge);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment,
|
||||
new TransactionCtx(getCurrency(), chargePaymentInstallments, loanCharges, new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment, new TransactionCtx(getCurrency(),
|
||||
chargePaymentInstallments, loanCharges, new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
|
||||
@@ -1887,7 +1887,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) {
|
||||
repaymentPeriod.updateDerivedFields(loanCurrency(), actualDisbursementDate);
|
||||
repaymentPeriod.updateObligationsMet(loanCurrency(), actualDisbursementDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2395,7 +2395,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
if (isTransactionChronologicallyLatest && adjustedTransaction == null
|
||||
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
reprocess = false;
|
||||
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
||||
@@ -2843,7 +2843,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
}
|
||||
@@ -2945,7 +2945,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
} else if (totalOutstanding.isGreaterThanZero()) {
|
||||
@@ -4858,7 +4858,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
// If it's a refund
|
||||
if (adjustedTransaction == null) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
} else {
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
@@ -4888,7 +4888,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
addLoanTransaction(chargebackTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())));
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
|
||||
|
||||
+19
-2
@@ -743,10 +743,13 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
this.penaltyAccrued = defaultToNullIfZero(penalityCharges.getAmount());
|
||||
}
|
||||
|
||||
public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate actualDisbursementDate) {
|
||||
public void updateObligationsMet(final MonetaryCurrency currency, final LocalDate transactionDate) {
|
||||
if (!this.obligationsMet && getTotalOutstanding(currency).isZero()) {
|
||||
this.obligationsMet = true;
|
||||
this.obligationsMetOnDate = actualDisbursementDate;
|
||||
this.obligationsMetOnDate = transactionDate;
|
||||
} else if (this.obligationsMet && !getTotalOutstanding(currency).isZero()) {
|
||||
this.obligationsMet = false;
|
||||
this.obligationsMetOnDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,6 +846,15 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
|
||||
}
|
||||
|
||||
public void addToInterest(final LocalDate transactionDate, final Money transactionAmount) {
|
||||
if (this.interestCharged == null) {
|
||||
this.interestCharged = transactionAmount.getAmount();
|
||||
} else {
|
||||
this.interestCharged = this.interestCharged.add(transactionAmount.getAmount());
|
||||
}
|
||||
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
|
||||
}
|
||||
|
||||
public void addToCreditedPrincipal(final BigDecimal amount) {
|
||||
if (this.creditedPrincipal == null) {
|
||||
this.creditedPrincipal = amount;
|
||||
@@ -1051,6 +1063,11 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
resetDerivedComponents();
|
||||
resetPrincipalDue();
|
||||
resetChargesCharged();
|
||||
resetInterestDue();
|
||||
}
|
||||
|
||||
public void resetInterestDue() {
|
||||
this.interestCharged = null;
|
||||
}
|
||||
|
||||
public void resetPrincipalDue() {
|
||||
|
||||
+4
-3
@@ -83,7 +83,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetDerivedComponents();
|
||||
currentInstallment.updateDerivedFields(currency, disbursementDate);
|
||||
currentInstallment.updateObligationsMet(currency, disbursementDate);
|
||||
}
|
||||
|
||||
// re-process loan charges over repayment periods (picking up on waived
|
||||
@@ -164,7 +164,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
// pass through for new transactions
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder));
|
||||
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
|
||||
loanTransaction.adjustInterestComponent(currency);
|
||||
} else {
|
||||
/**
|
||||
@@ -175,7 +175,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
|
||||
// Reset derived component of new loan transaction and
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder));
|
||||
processLatestTransaction(newLoanTransaction,
|
||||
new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
/**
|
||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||
|
||||
-18
@@ -21,8 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
@@ -32,22 +30,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public interface LoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
class TransactionCtx {
|
||||
|
||||
private final MonetaryCurrency currency;
|
||||
private final List<LoanRepaymentScheduleInstallment> installments;
|
||||
private final Set<LoanCharge> charges;
|
||||
private final MoneyHolder overpaymentHolder;
|
||||
private final ChangedTransactionDetail changedTransactionDetail;
|
||||
|
||||
public TransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
|
||||
MoneyHolder overpaymentHolder) {
|
||||
this(currency, installments, charges, overpaymentHolder, null);
|
||||
}
|
||||
}
|
||||
|
||||
String getCode();
|
||||
|
||||
String getName();
|
||||
|
||||
+16
-10
@@ -16,18 +16,24 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
|
||||
@Getter
|
||||
public class RepaymentScheduleModel {
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class TransactionCtx {
|
||||
|
||||
List<RepaymentPeriodModel> scheduleList = new ArrayList<>();
|
||||
|
||||
public void addRepaymentPeriodModel(final RepaymentPeriodModel repaymentPeriodModel) {
|
||||
scheduleList.add(repaymentPeriodModel);
|
||||
}
|
||||
private final MonetaryCurrency currency;
|
||||
private final List<LoanRepaymentScheduleInstallment> installments;
|
||||
private final Set<LoanCharge> charges;
|
||||
private final MoneyHolder overpaymentHolder;
|
||||
private final ChangedTransactionDetail changedTransactionDetail;
|
||||
}
|
||||
+3
-2
@@ -2001,8 +2001,9 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
|
||||
final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
|
||||
if (!loanApplicationTerms.isMultiDisburseLoan()) {
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
.disbursement(loanApplicationTerms, chargesDueAtTimeOfDisbursement);
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement(
|
||||
loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal(),
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
|
||||
|
||||
-7
@@ -37,13 +37,6 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo
|
||||
private final BigDecimal chargesDueAtTimeOfDisbursement;
|
||||
private boolean isEMIFixedSpecificToInstallment = false;
|
||||
|
||||
public static LoanScheduleModelDisbursementPeriod disbursement(final LoanApplicationTerms loanApplicationTerms,
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
|
||||
return new LoanScheduleModelDisbursementPeriod(null, loanApplicationTerms.getExpectedDisbursementDate(),
|
||||
loanApplicationTerms.getPrincipal(), chargesDueAtTimeOfDisbursement);
|
||||
}
|
||||
|
||||
public static LoanScheduleModelDisbursementPeriod disbursement(final LocalDate disbursementDate, final Money principalDisbursed,
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
return new LoanScheduleModelDisbursementPeriod(null, disbursementDate, principalDisbursed, chargesDueAtTimeOfDisbursement);
|
||||
|
||||
+10
-2
@@ -33,11 +33,11 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
||||
@Getter
|
||||
public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod {
|
||||
|
||||
private final int periodNumber;
|
||||
private int periodNumber;
|
||||
private final LocalDate fromDate;
|
||||
private final LocalDate dueDate;
|
||||
private Money principalDue;
|
||||
private final Money outstandingLoanBalance;
|
||||
private Money outstandingLoanBalance;
|
||||
private Money interestDue;
|
||||
private Money feeChargesDue;
|
||||
private Money penaltyChargesDue;
|
||||
@@ -166,6 +166,14 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
|
||||
this.totalDue = this.totalDue.plus(interestDue);
|
||||
}
|
||||
|
||||
public void setPeriodNumber(int periodNumber) {
|
||||
this.periodNumber = periodNumber;
|
||||
}
|
||||
|
||||
public void setOutstandingLoanBalance(Money outstandingLoanBalance) {
|
||||
this.outstandingLoanBalance = outstandingLoanBalance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
|
||||
return this.loanCompoundingDetails;
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
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;
|
||||
|
||||
@Data
|
||||
@Accessors(fluent = true)
|
||||
public class PreGeneratedLoanSchedulePeriod implements LoanScheduleModelPeriod {
|
||||
|
||||
private Integer periodNumber;
|
||||
private boolean isRepaymentPeriod;
|
||||
private boolean isDownPaymentPeriod;
|
||||
private LocalDate periodFromDate;
|
||||
private LocalDate periodDueDate;
|
||||
private BigDecimal principalDue;
|
||||
private BigDecimal interestDue;
|
||||
private BigDecimal feeChargesDue;
|
||||
private BigDecimal penaltyChargesDue;
|
||||
private BigDecimal rescheduleInterestPortion;
|
||||
private boolean isRecalculatedInterestComponent;
|
||||
private boolean isEMIFixedSpecificToInstallment;
|
||||
|
||||
public PreGeneratedLoanSchedulePeriod(Integer periodNumber, LocalDate periodFromDate, LocalDate periodDueDate) {
|
||||
this.periodNumber = periodNumber;
|
||||
this.isRepaymentPeriod = true;
|
||||
this.isDownPaymentPeriod = false;
|
||||
this.periodFromDate = periodFromDate;
|
||||
this.periodDueDate = periodDueDate;
|
||||
this.principalDue = BigDecimal.ZERO;
|
||||
this.interestDue = BigDecimal.ZERO;
|
||||
this.feeChargesDue = BigDecimal.ZERO;
|
||||
this.penaltyChargesDue = BigDecimal.ZERO;
|
||||
this.rescheduleInterestPortion = BigDecimal.ZERO;
|
||||
this.isRecalculatedInterestComponent = false;
|
||||
this.isEMIFixedSpecificToInstallment = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanSchedulePeriodData toData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrincipalAmount(Money principalDue) {
|
||||
this.principalDue = this.principalDue.add(principalDue.getAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterestAmount(Money interestDue) {
|
||||
this.interestDue = this.principalDue.add(interestDue.getAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEMIFixedSpecificToInstallmentTrue() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRescheduleInterestPortion(BigDecimal rescheduleInterestPortion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
|
||||
public interface ScheduledDateGenerator {
|
||||
|
||||
List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(LocalDate scheduledDueDate, LoanApplicationTerms loanApplicationTerms,
|
||||
List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(LocalDate scheduledDueDate, LoanApplicationTerms loanApplicationTerms,
|
||||
HolidayDetailDTO holidayDetailDTO);
|
||||
|
||||
LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
|
||||
|
||||
+167
-104
@@ -74,10 +74,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
|
||||
@@ -95,7 +97,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
|
||||
private final LoanReAgingParameterRepository reAgingParameterRepository;
|
||||
public final EMICalculator emiCalculator;
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
@@ -144,6 +146,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
if (loanTransactions.isEmpty()) {
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
if (charges != null) {
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isDueAtDisbursement()) {
|
||||
@@ -160,16 +166,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetBalances();
|
||||
currentInstallment.updateDerivedFields(currency, disbursementDate);
|
||||
currentInstallment.updateObligationsMet(currency, disbursementDate);
|
||||
}
|
||||
|
||||
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
final Loan loan = loanTransactions.get(0).getLoan();
|
||||
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, installments, mc);
|
||||
|
||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
|
||||
installments, charges, changedTransactionDetail, overpaymentHolder));
|
||||
installments, charges, changedTransactionDetail, overpaymentHolder, scheduleModel));
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
}
|
||||
@@ -182,7 +194,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
@Override
|
||||
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx);
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
|
||||
@@ -190,12 +202,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
|
||||
handleRepayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
|
||||
ctx.getOverpaymentHolder());
|
||||
handleRepayment(loanTransaction, ctx);
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx);
|
||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx);
|
||||
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
||||
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
// TODO: Cover rest of the transaction types
|
||||
@@ -205,28 +216,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReAmortization(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
List<LoanRepaymentScheduleInstallment> previousInstallments = installments.stream() //
|
||||
List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.getDueDate().isAfter(transactionDate)) //
|
||||
.toList();
|
||||
List<LoanRepaymentScheduleInstallment> futureInstallments = installments.stream() //
|
||||
List<LoanRepaymentScheduleInstallment> futureInstallments = transactionCtx.getInstallments().stream() //
|
||||
.filter(installment -> installment.getDueDate().isAfter(transactionDate)) //
|
||||
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment() && !installment.isReAged()) //
|
||||
.toList();
|
||||
|
||||
BigDecimal overallOverDuePrincipal = ZERO;
|
||||
for (LoanRepaymentScheduleInstallment installment : previousInstallments) {
|
||||
Money principalCompleted = installment.getPrincipalCompleted(currency);
|
||||
overallOverDuePrincipal = overallOverDuePrincipal.add(installment.getPrincipal(currency).minus(principalCompleted).getAmount());
|
||||
installment.updatePrincipal(installment.getPrincipalCompleted(currency).getAmount());
|
||||
installment.updateDerivedFields(currency, transactionDate);
|
||||
Money principalCompleted = installment.getPrincipalCompleted(transactionCtx.getCurrency());
|
||||
overallOverDuePrincipal = overallOverDuePrincipal
|
||||
.add(installment.getPrincipal(transactionCtx.getCurrency()).minus(principalCompleted).getAmount());
|
||||
installment.updatePrincipal(installment.getPrincipalCompleted(transactionCtx.getCurrency()).getAmount());
|
||||
installment.updateObligationsMet(transactionCtx.getCurrency(), transactionDate);
|
||||
}
|
||||
|
||||
loanTransaction.resetDerivedComponents();
|
||||
loanTransaction.updateComponentsAndTotal(Money.of(currency, overallOverDuePrincipal), Money.zero(currency), Money.zero(currency),
|
||||
Money.zero(currency));
|
||||
loanTransaction.updateComponentsAndTotal(Money.of(transactionCtx.getCurrency(), overallOverDuePrincipal),
|
||||
Money.zero(transactionCtx.getCurrency()), Money.zero(transactionCtx.getCurrency()),
|
||||
Money.zero(transactionCtx.getCurrency()));
|
||||
|
||||
LoanRepaymentScheduleInstallment lastFutureInstallment = futureInstallments.stream()
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).get();
|
||||
@@ -236,13 +248,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
for (LoanRepaymentScheduleInstallment installment : futureInstallments) {
|
||||
if (lastFutureInstallment.equals(installment)) {
|
||||
installment.addToPrincipal(transactionDate, Money.of(currency, overallOverDuePrincipal));
|
||||
installment.addToPrincipal(transactionDate, Money.of(transactionCtx.getCurrency(), overallOverDuePrincipal));
|
||||
} else {
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
reAmortizationAmountPerInstallment = Money.roundToMultiplesOf(reAmortizationAmountPerInstallment,
|
||||
installmentAmountInMultiplesOf);
|
||||
}
|
||||
installment.addToPrincipal(transactionDate, Money.of(currency, reAmortizationAmountPerInstallment));
|
||||
installment.addToPrincipal(transactionDate, Money.of(transactionCtx.getCurrency(), reAmortizationAmountPerInstallment));
|
||||
overallOverDuePrincipal = overallOverDuePrincipal.subtract(reAmortizationAmountPerInstallment);
|
||||
}
|
||||
}
|
||||
@@ -361,8 +373,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||
}
|
||||
}
|
||||
allocateOverpayment(loanTransaction, ctx.getCurrency(), loanTransaction.getLoan().getRepaymentScheduleInstallments(),
|
||||
ctx.getOverpaymentHolder());
|
||||
allocateOverpayment(loanTransaction, ctx);
|
||||
} else {
|
||||
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
|
||||
}
|
||||
@@ -544,8 +555,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
|
||||
MoneyHolder overpaymentHolder) {
|
||||
TransactionCtx ctx = new TransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
|
||||
MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail,
|
||||
scheduleModel);
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, ctx);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
@@ -613,63 +625,113 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return chargeOrTransactions;
|
||||
}
|
||||
|
||||
private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
|
||||
updateLoanSchedule(loanTransaction, currency, installments, overpaymentHolder);
|
||||
}
|
||||
|
||||
private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
|
||||
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx;
|
||||
if (progressiveTransactionCtx.getModel() == null) {
|
||||
throw new IllegalStateException("TransactionCtx has no model");
|
||||
}
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency());
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = progressiveTransactionCtx.getInstallments().stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(progressiveTransactionCtx.getCurrency()).isZero()).findFirst()
|
||||
.orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(currency, downPaymentAmt);
|
||||
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
|
||||
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
|
||||
}
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
|
||||
|
||||
emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(),
|
||||
amortizableAmount);
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
progressiveTransactionCtx.getModel().repayments().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream()
|
||||
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()
|
||||
&& !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate()))
|
||||
.findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getPrincipalDue().getAmount());
|
||||
installment.updateInterestCharged(rm.getInterestDue().getAmount());
|
||||
installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
|
||||
if (disbursementTransaction.getLoan().isInterestBearing()) {
|
||||
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
|
||||
} else {
|
||||
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(transactionCtx.getCurrency());
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = transactionCtx.getInstallments().stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.getCurrency()).isZero()).findFirst().orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
|
||||
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
|
||||
}
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf);
|
||||
}
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
Money remainingAmount = amortizableAmount
|
||||
.minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()));
|
||||
|
||||
Money finalIncreasePrincipalBy = increasePrincipalBy;
|
||||
candidateRepaymentInstallments
|
||||
.forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy));
|
||||
candidateRepaymentInstallments.forEach(i -> {
|
||||
Money previousPrincipal = i.getPrincipal(transactionCtx.getCurrency());
|
||||
Money newPrincipal = previousPrincipal.add(increasePrincipalBy);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
newPrincipal = Money.roundToMultiplesOf(newPrincipal, installmentAmountInMultiplesOf);
|
||||
}
|
||||
i.updatePrincipal(newPrincipal.getAmount());
|
||||
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
|
||||
i.updateObligationsMet(transactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
|
||||
});
|
||||
// Hence the rounding, we might need to amend the last installment amount
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount);
|
||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject());
|
||||
}
|
||||
|
||||
allocateOverpayment(disbursementTransaction, currency, installments, overpaymentHolder);
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void allocateOverpayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
|
||||
if (overpaymentHolder.getMoneyObject().isGreaterThanZero()) {
|
||||
if (overpaymentHolder.getMoneyObject().isGreaterThan(loanTransaction.getAmount(currency))) {
|
||||
loanTransaction.setOverPayments(loanTransaction.getAmount(currency));
|
||||
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject().isGreaterThanZero()) {
|
||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject()
|
||||
.isGreaterThan(loanTransaction.getAmount(transactionCtx.getCurrency()))) {
|
||||
loanTransaction.setOverPayments(loanTransaction.getAmount(transactionCtx.getCurrency()));
|
||||
} else {
|
||||
loanTransaction.setOverPayments(overpaymentHolder.getMoneyObject());
|
||||
loanTransaction.setOverPayments(transactionCtx.getOverpaymentHolder().getMoneyObject());
|
||||
}
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
@@ -677,29 +739,30 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
|
||||
Money transactionAmountUnprocessed = null;
|
||||
Money zero = Money.zero(currency);
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments,
|
||||
overpaymentHolder.getMoneyObject(), defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(),
|
||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments,
|
||||
overpaymentHolder.getMoneyObject(), defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(),
|
||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
}
|
||||
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
||||
transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
|
||||
private void handleRepayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
}
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges, overpaymentHolder);
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(transactionCtx.getCurrency());
|
||||
processTransaction(loanTransaction, transactionCtx, transactionAmountUnprocessed);
|
||||
}
|
||||
|
||||
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
|
||||
@@ -781,44 +844,44 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChargeOff(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
private void handleChargeOff(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltychargesPortion = Money.zero(currency);
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
Money principalPortion = Money.zero(transactionCtx.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionCtx.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionCtx.getCurrency());
|
||||
Money penaltychargesPortion = Money.zero(transactionCtx.getCurrency());
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
|
||||
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
|
||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
|
||||
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesOutstanding(currency));
|
||||
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(transactionCtx.getCurrency()));
|
||||
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()));
|
||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(transactionCtx.getCurrency()));
|
||||
penaltychargesPortion = penaltychargesPortion
|
||||
.plus(currentInstallment.getPenaltyChargesOutstanding(transactionCtx.getCurrency()));
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||
}
|
||||
|
||||
private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
|
||||
Money zero = Money.zero(currency);
|
||||
private void handleChargePayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
Money feeChargesPortion = zero;
|
||||
Money penaltyChargesPortion = zero;
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
LoanChargePaidBy loanChargePaidBy = loanTransaction.getLoanChargesPaid().stream().findFirst().get();
|
||||
LoanCharge loanCharge = loanChargePaidBy.getLoanCharge();
|
||||
Money amountToBePaid = Money.of(currency, loanTransaction.getAmount());
|
||||
if (loanCharge.getAmountOutstanding(currency).isLessThan(amountToBePaid)) {
|
||||
amountToBePaid = loanCharge.getAmountOutstanding(currency);
|
||||
Money amountToBePaid = Money.of(transactionCtx.getCurrency(), loanTransaction.getAmount());
|
||||
if (loanCharge.getAmountOutstanding(transactionCtx.getCurrency()).isLessThan(amountToBePaid)) {
|
||||
amountToBePaid = loanCharge.getAmountOutstanding(transactionCtx.getCurrency());
|
||||
}
|
||||
|
||||
LocalDate startDate = loanTransaction.getLoan().getDisbursementDate();
|
||||
|
||||
Money unprocessed = loanTransaction.getAmount(currency);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
Money unprocessed = loanTransaction.getAmount(transactionCtx.getCurrency());
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(transactionCtx.getInstallments());
|
||||
for (final LoanRepaymentScheduleInstallment installment : transactionCtx.getInstallments()) {
|
||||
boolean isDue = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
|
||||
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
|
||||
@@ -827,7 +890,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Money paidAmount = loanCharge.updatePaidAmountBy(amountToBePaid, installmentNumber, zero);
|
||||
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, installment, currency);
|
||||
transactionMappings, loanTransaction, installment, transactionCtx.getCurrency());
|
||||
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = installment.payPenaltyChargesComponent(loanTransaction.getTransactionDate(), paidAmount);
|
||||
@@ -842,13 +905,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(zero, zero, feeChargesPortion, penaltyChargesPortion);
|
||||
unprocessed = loanTransaction.getAmount(currency).minus(paidAmount);
|
||||
unprocessed = loanTransaction.getAmount(transactionCtx.getCurrency()).minus(paidAmount);
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
}
|
||||
|
||||
if (unprocessed.isGreaterThanZero()) {
|
||||
processTransaction(loanTransaction, currency, installments, unprocessed, charges, overpaymentHolder);
|
||||
processTransaction(loanTransaction, transactionCtx, unprocessed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1032,10 +1095,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return inAdvanceInstallments;
|
||||
}
|
||||
|
||||
private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, Set<LoanCharge> charges,
|
||||
MoneyHolder overpaymentHolder) {
|
||||
Money zero = Money.zero(currency);
|
||||
private void processTransaction(LoanTransaction loanTransaction, TransactionCtx transactionCtx, Money transactionAmountUnprocessed) {
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
@@ -1048,18 +1109,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, charges, balances);
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings,
|
||||
transactionCtx.getCharges(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, charges, balances);
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings,
|
||||
transactionCtx.getCharges(), balances);
|
||||
}
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, overpaymentHolder);
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
|
||||
}
|
||||
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
@@ -1339,7 +1402,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(),
|
||||
loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount());
|
||||
installments.add(reAgedInstallment);
|
||||
reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate());
|
||||
reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate());
|
||||
|
||||
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) {
|
||||
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(),
|
||||
@@ -1348,7 +1411,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate,
|
||||
calculatedPrincipal.getAmount());
|
||||
installments.add(reAgedInstallment);
|
||||
reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate());
|
||||
reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate());
|
||||
}
|
||||
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
|
||||
reprocessInstallmentsOrder(installments);
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
|
||||
public class ProgressiveTransactionCtx extends TransactionCtx {
|
||||
|
||||
private final ProgressiveLoanInterestScheduleModel model;
|
||||
|
||||
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
|
||||
ProgressiveLoanInterestScheduleModel model) {
|
||||
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public ProgressiveLoanInterestScheduleModel getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+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.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class ProgressiveLoanInterestRepaymentInterestPeriod implements Comparable<ProgressiveLoanInterestRepaymentInterestPeriod> {
|
||||
|
||||
private LocalDate fromDate;
|
||||
private LocalDate dueDate;
|
||||
|
||||
private BigDecimal rateFactorMinus1;
|
||||
|
||||
private Money disbursedAmount;
|
||||
private Money interestDue;
|
||||
|
||||
public ProgressiveLoanInterestRepaymentInterestPeriod(final ProgressiveLoanInterestRepaymentInterestPeriod period) {
|
||||
this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.interestDue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ProgressiveLoanInterestRepaymentInterestPeriod o) {
|
||||
return dueDate.compareTo(o.dueDate);
|
||||
}
|
||||
|
||||
public void addDisbursedAmount(final Money outstandingBalance) {
|
||||
this.disbursedAmount = this.disbursedAmount.add(outstandingBalance);
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 java.util.LinkedList;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
public class ProgressiveLoanInterestRepaymentModel {
|
||||
|
||||
private final LocalDate fromDate;
|
||||
private final LocalDate dueDate;
|
||||
|
||||
private LinkedList<ProgressiveLoanInterestRepaymentInterestPeriod> interestPeriods;
|
||||
|
||||
private Money equalMonthlyInstallment;
|
||||
private Money principalDue;
|
||||
private Money remainingBalance;
|
||||
|
||||
public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final LocalDate dueDate, final Money equalMonthlyInstallment) {
|
||||
this.fromDate = fromDate;
|
||||
this.dueDate = dueDate;
|
||||
this.equalMonthlyInstallment = equalMonthlyInstallment;
|
||||
|
||||
final Money zeroAmount = Money.zero(equalMonthlyInstallment.getCurrency());
|
||||
this.remainingBalance = zeroAmount;
|
||||
this.principalDue = zeroAmount;
|
||||
this.interestPeriods = new LinkedList<>();
|
||||
this.interestPeriods
|
||||
.add(new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount));
|
||||
}
|
||||
|
||||
public ProgressiveLoanInterestRepaymentModel(ProgressiveLoanInterestRepaymentModel repaymentModel) {
|
||||
this.fromDate = repaymentModel.fromDate;
|
||||
this.dueDate = repaymentModel.dueDate;
|
||||
this.equalMonthlyInstallment = repaymentModel.equalMonthlyInstallment;
|
||||
this.remainingBalance = repaymentModel.remainingBalance;
|
||||
this.principalDue = repaymentModel.principalDue;
|
||||
this.interestPeriods = new LinkedList<>();
|
||||
for (final ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentModel.interestPeriods) {
|
||||
this.interestPeriods.add(new ProgressiveLoanInterestRepaymentInterestPeriod(interestPeriod));
|
||||
}
|
||||
}
|
||||
|
||||
public BigDecimal getRateFactor() {
|
||||
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getRateFactorMinus1).reduce(BigDecimal.ONE,
|
||||
BigDecimal::add);
|
||||
}
|
||||
|
||||
public Money getDisbursedAmountInPeriod() {
|
||||
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getDisbursedAmount)
|
||||
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
|
||||
}
|
||||
|
||||
public Money getInterestDue() {
|
||||
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getInterestDue)
|
||||
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
|
||||
}
|
||||
|
||||
public Money getOutstandingBalance() {
|
||||
return remainingBalance.plus(principalDue);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.MathContext;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
|
||||
public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
|
||||
|
||||
public int getLoanTermInDays() {
|
||||
if (repayments.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
final var firstPeriod = repayments.get(0);
|
||||
final var lastPeriod = repayments.size() > 1 ? repayments.get(repayments.size() - 1) : firstPeriod;
|
||||
return Math.toIntExact(ChronoUnit.DAYS.between(firstPeriod.getFromDate(), lastPeriod.getDueDate()));
|
||||
}
|
||||
}
|
||||
-620
@@ -1,620 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
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.workingdays.data.AdjustedDateDetailsDTO;
|
||||
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
|
||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
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.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
|
||||
public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
|
||||
@Override
|
||||
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
|
||||
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
|
||||
// TODO: handle interest calculation
|
||||
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
|
||||
// generate list of proposed schedule due dates
|
||||
LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
|
||||
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
|
||||
.fetchLoanTermDueDateVariationsData(loanEndDate);
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
}
|
||||
loanApplicationTerms.updateLoanEndDate(loanEndDate);
|
||||
|
||||
// determine the total charges due at time of disbursement
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
|
||||
|
||||
// setup variables for tracking important facts required for loan
|
||||
// schedule generation.
|
||||
|
||||
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
|
||||
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
|
||||
? loanApplicationTerms.getExpectedDisbursementDate()
|
||||
: loanApplicationTerms.getSubmittedOnDate();
|
||||
|
||||
LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency,
|
||||
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, Money.zero(currency));
|
||||
|
||||
List<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams,
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
|
||||
EMICalculationResult emiCalculationResult = null;
|
||||
boolean isFirstRepayment = true;
|
||||
|
||||
// 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);
|
||||
boolean isNextRepaymentAvailable = true;
|
||||
|
||||
boolean thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
|
||||
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
|
||||
while (!scheduleParams.getOutstandingBalance().isZero() || thereIsDisbursementBeforeOrOnLoanEndDate) {
|
||||
scheduleParams.setActualRepaymentDate(getScheduledDateGenerator().generateNextRepaymentDate(
|
||||
scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment, scheduleParams.getPeriodNumber()));
|
||||
AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator()
|
||||
.adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO);
|
||||
scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate());
|
||||
LocalDate scheduledDueDate = adjustedDateDetailsDTO.getChangedScheduleDate();
|
||||
|
||||
// Loan Schedule Exceptions that need to be applied for Loan Account
|
||||
LoanTermVariationParams termVariationParams = applyLoanTermVariations(loanApplicationTerms, scheduleParams, scheduledDueDate);
|
||||
scheduledDueDate = termVariationParams.scheduledDueDate();
|
||||
|
||||
// Updates total days in term
|
||||
scheduleParams
|
||||
.addLoanTermInDays(Math.toIntExact(ChronoUnit.DAYS.between(scheduleParams.getPeriodStartDate(), scheduledDueDate)));
|
||||
|
||||
ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO);
|
||||
final boolean hasAnyProcessedDisbursement = processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement,
|
||||
scheduleParams, periods, scheduledDueDate);
|
||||
if (isFirstRepayment || hasAnyProcessedDisbursement) {
|
||||
final LocalDate startDate = isFirstRepayment ? loanApplicationTerms.getRepaymentStartDate()
|
||||
: scheduleParams.getPeriodStartDate();
|
||||
List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods = getScheduledDateGenerator()
|
||||
.generateRepaymentPeriods(startDate, loanApplicationTerms, holidayDetailDTO);
|
||||
emiCalculationResult = getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams.getOutstandingBalanceAsPerRest(),
|
||||
loanApplicationTerms.toLoanProductRelatedDetail(), expectedRepaymentPeriods, scheduleParams.getPeriodNumber(),
|
||||
loanApplicationTerms.getNumberOfRepayments(), mc);
|
||||
}
|
||||
|
||||
// 5 determine principal,interest of repayment period
|
||||
PrincipalInterest principalInterestForThisPeriod = getEMICalculator().calculatePrincipalInterestComponentsForPeriod(
|
||||
emiCalculationResult, scheduleParams.getOutstandingBalanceAsPerRest(),
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), scheduleParams.getPeriodNumber(),
|
||||
loanApplicationTerms.getActualNoOfRepaymnets(), mc);
|
||||
|
||||
// update cumulative fields for principal
|
||||
currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal());
|
||||
currentPeriodParams.setInterestForThisPeriod(principalInterestForThisPeriod.interest());
|
||||
updateOutstandingBalance(scheduleParams, currentPeriodParams);
|
||||
|
||||
if (scheduleParams.getOutstandingBalance().isLessThanZero() || !isNextRepaymentAvailable) {
|
||||
currentPeriodParams.plusPrincipalForThisPeriod(scheduleParams.getOutstandingBalance());
|
||||
scheduleParams.setOutstandingBalance(Money.zero(currency));
|
||||
}
|
||||
|
||||
if (!isNextRepaymentAvailable) {
|
||||
scheduleParams.getDisburseDetailMap().clear();
|
||||
}
|
||||
|
||||
// applies charges for the period
|
||||
applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams, mc);
|
||||
|
||||
// sum up real totalInstallmentDue from components
|
||||
final Money totalInstallmentDue = currentPeriodParams.fetchTotalAmountForPeriod();
|
||||
|
||||
// if previous installment is last then add interest to same
|
||||
// installment
|
||||
if (currentPeriodParams.getLastInstallment() != null && currentPeriodParams.getPrincipalForThisPeriod().isZero()) {
|
||||
currentPeriodParams.getLastInstallment().addInterestAmount(currentPeriodParams.getInterestForThisPeriod());
|
||||
continue;
|
||||
}
|
||||
|
||||
// create repayment period from parts
|
||||
LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(),
|
||||
scheduleParams.getPeriodStartDate(), scheduledDueDate, currentPeriodParams.getPrincipalForThisPeriod(),
|
||||
scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(),
|
||||
currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(),
|
||||
totalInstallmentDue, false);
|
||||
if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) {
|
||||
installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount());
|
||||
}
|
||||
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
|
||||
|
||||
if (loanApplicationTerms.getCurrentPeriodFixedEmiAmount() != null) {
|
||||
installment.setEMIFixedSpecificToInstallmentTrue();
|
||||
}
|
||||
|
||||
periods.add(installment);
|
||||
|
||||
// Updates principal paid map with effective date for reducing
|
||||
// the amount from outstanding balance(interest calculation)
|
||||
updateAmountsWithEffectiveDate(loanApplicationTerms, holidayDetailDTO, scheduleParams, scheduledDueDate, currentPeriodParams,
|
||||
installment);
|
||||
|
||||
// handle cumulative fields
|
||||
|
||||
scheduleParams.addTotalCumulativePrincipal(currentPeriodParams.getPrincipalForThisPeriod());
|
||||
scheduleParams.addTotalRepaymentExpected(totalInstallmentDue);
|
||||
scheduleParams.addTotalCumulativeInterest(currentPeriodParams.getInterestForThisPeriod());
|
||||
scheduleParams.setPeriodStartDate(scheduledDueDate);
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
scheduleParams.incrementPeriodNumber();
|
||||
// if (termVariationParams.isRecalculateAmounts()) {
|
||||
// loanApplicationTerms.setCurrentPeriodFixedEmiAmount(null);
|
||||
// loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(null);
|
||||
// adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(),
|
||||
// scheduleParams.getPeriodNumber(), mc);
|
||||
// }
|
||||
thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
|
||||
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
|
||||
isFirstRepayment = false;
|
||||
}
|
||||
|
||||
// If the disbursement happened after maturity date
|
||||
if (loanApplicationTerms.isMultiDisburseLoan()) {
|
||||
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods,
|
||||
DateUtils.getBusinessLocalDate().plusDays(1));
|
||||
}
|
||||
|
||||
// determine fees and penalties for charges which depends on total
|
||||
// loan interest
|
||||
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
|
||||
|
||||
final BigDecimal totalPrincipalPaid = BigDecimal.ZERO;
|
||||
final BigDecimal totalOutstanding = BigDecimal.ZERO;
|
||||
|
||||
return LoanScheduleModel.from(periods, applicationCurrency, scheduleParams.getLoanTermInDays(),
|
||||
scheduleParams.getPrincipalToBeScheduled().plus(loanApplicationTerms.getDownPaymentAmount()),
|
||||
scheduleParams.getTotalCumulativePrincipal().plus(loanApplicationTerms.getDownPaymentAmount()).getAmount(),
|
||||
totalPrincipalPaid, scheduleParams.getTotalCumulativeInterest().getAmount(),
|
||||
scheduleParams.getTotalFeeChargesCharged().getAmount(), scheduleParams.getTotalPenaltyChargesCharged().getAmount(),
|
||||
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
|
||||
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
LocalDate rescheduleFrom) {
|
||||
|
||||
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
|
||||
return LoanScheduleDTO.from(null, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Abstract methods
|
||||
public abstract ScheduledDateGenerator getScheduledDateGenerator();
|
||||
|
||||
public abstract PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator();
|
||||
|
||||
protected abstract EMICalculator getEMICalculator();
|
||||
|
||||
// Private, internal methods
|
||||
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
|
||||
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isDueAtDisbursement()) {
|
||||
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
|
||||
}
|
||||
}
|
||||
return chargesDueAtTimeOfDisbursement;
|
||||
}
|
||||
|
||||
private List<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms,
|
||||
final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
|
||||
|
||||
// In case of `disallowExpectedDisbursementDetails = true`, anyway at least 1 disbursement details must exist
|
||||
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
|
||||
loanApplicationTerms.getDisbursementDatas()
|
||||
.add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(),
|
||||
loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null,
|
||||
null, null));
|
||||
}
|
||||
for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) {
|
||||
Money principalMoney = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
|
||||
if (disbursementData.disbursementDate().equals(loanScheduleParams.getPeriodStartDate())) {
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement(
|
||||
disbursementData.disbursementDate(), Money.of(loanScheduleParams.getCurrency(), disbursementData.getPrincipal()),
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
loanScheduleParams.addOutstandingBalance(principalMoney);
|
||||
loanScheduleParams.addOutstandingBalanceAsPerRest(principalMoney);
|
||||
loanScheduleParams.addPrincipalToBeScheduled(principalMoney);
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
|
||||
loanScheduleParams, loanApplicationTerms.getExpectedDisbursementDate(), disbursementData.getPrincipal());
|
||||
periods.add(downPaymentPeriod);
|
||||
}
|
||||
} else {
|
||||
Money disbursedAmount = loanScheduleParams.getDisburseDetailMap().getOrDefault(disbursementData.disbursementDate(),
|
||||
Money.zero(loanApplicationTerms.getCurrency()));
|
||||
loanScheduleParams.getDisburseDetailMap().put(disbursementData.disbursementDate(), disbursedAmount.add(principalMoney));
|
||||
}
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
|
||||
private LoanScheduleModelDownPaymentPeriod createDownPaymentPeriod(LoanApplicationTerms loanApplicationTerms,
|
||||
LoanScheduleParams scheduleParams, LocalDate date, BigDecimal periodBaseAmount) {
|
||||
Money downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(),
|
||||
MathUtil.percentageOf(periodBaseAmount, loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
|
||||
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
|
||||
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, loanApplicationTerms.getInstallmentAmountInMultiplesOf());
|
||||
}
|
||||
LoanScheduleModelDownPaymentPeriod installment = LoanScheduleModelDownPaymentPeriod.downPayment(
|
||||
scheduleParams.getInstalmentNumber(), date, downPaymentAmount,
|
||||
scheduleParams.getOutstandingBalance().minus(downPaymentAmount));
|
||||
|
||||
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
|
||||
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
scheduleParams.addTotalRepaymentExpected(downPaymentAmount);
|
||||
scheduleParams.reduceOutstandingBalance(downPaymentAmount);
|
||||
scheduleParams.reduceOutstandingBalanceAsPerRest(downPaymentAmount);
|
||||
scheduleParams.setPrincipalToBeScheduled(scheduleParams.getPrincipalToBeScheduled().minus(downPaymentAmount));
|
||||
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().minus(downPaymentAmount));
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
return installment;
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
|
||||
final LoanScheduleModelPeriod scheduledLoanInstallment) {
|
||||
LoanRepaymentScheduleInstallment installment = null;
|
||||
if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) {
|
||||
installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(),
|
||||
scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(),
|
||||
scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(),
|
||||
scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(),
|
||||
scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(),
|
||||
scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod());
|
||||
installments.add(installment);
|
||||
}
|
||||
return installment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method add extra disbursement periods (if applicable) and update the schedule params
|
||||
*/
|
||||
private boolean processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement,
|
||||
LoanScheduleParams scheduleParams, final Collection<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) {
|
||||
Iterator<Map.Entry<LocalDate, Money>> iter = scheduleParams.getDisburseDetailMap().entrySet().iterator();
|
||||
boolean hasProcessedDisbursement = false;
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<LocalDate, Money> disburseDetail = iter.next();
|
||||
if ((disburseDetail.getKey().isEqual(scheduleParams.getPeriodStartDate())
|
||||
|| disburseDetail.getKey().isAfter(scheduleParams.getPeriodStartDate()))
|
||||
&& disburseDetail.getKey().isBefore(scheduledDueDate)) {
|
||||
// validation check for amount not exceeds specified max
|
||||
// amount as per the configuration
|
||||
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getMaxOutstandingBalance() != null) {
|
||||
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalanceMoney();
|
||||
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()).isGreaterThan(maxOutstandingBalance)) {
|
||||
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
|
||||
throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance(),
|
||||
disburseDetail.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// creates and add disbursement detail to the repayments
|
||||
// period
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
.disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
|
||||
BigDecimal downPaymentAmt = BigDecimal.ZERO;
|
||||
// updates actual outstanding balance with new
|
||||
// disbursement detail
|
||||
scheduleParams.addOutstandingBalance(disburseDetail.getValue());
|
||||
scheduleParams.addOutstandingBalanceAsPerRest(disburseDetail.getValue());
|
||||
scheduleParams.addPrincipalToBeScheduled(disburseDetail.getValue());
|
||||
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue()));
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
// get list of disbursements done on same day and create down payment periods
|
||||
List<DisbursementData> disbursementsOnSameDate = loanApplicationTerms.getDisbursementDatas().stream()
|
||||
.filter(disbursementData -> DateUtils.isEqual(disbursementData.disbursementDate(), disburseDetail.getKey()))
|
||||
.toList();
|
||||
for (DisbursementData disbursementData : disbursementsOnSameDate) {
|
||||
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
|
||||
scheduleParams, disbursementData.disbursementDate(), disbursementData.getPrincipal());
|
||||
periods.add(downPaymentPeriod);
|
||||
downPaymentAmt = downPaymentAmt.add(downPaymentPeriod.principalDue());
|
||||
}
|
||||
}
|
||||
iter.remove();
|
||||
hasProcessedDisbursement = true;
|
||||
}
|
||||
}
|
||||
return hasProcessedDisbursement;
|
||||
}
|
||||
|
||||
private void applyChargesForCurrentPeriod(final Set<LoanCharge> loanCharges, final MonetaryCurrency currency,
|
||||
LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams,
|
||||
final MathContext mc) {
|
||||
PrincipalInterest principalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(),
|
||||
currentPeriodParams.getInterestForThisPeriod(), null);
|
||||
currentPeriodParams.setFeeChargesForInstallment(cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), scheduledDueDate,
|
||||
loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(),
|
||||
scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc));
|
||||
currentPeriodParams.setPenaltyChargesForInstallment(cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(),
|
||||
scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(),
|
||||
scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc));
|
||||
scheduleParams.addTotalFeeChargesCharged(currentPeriodParams.getFeeChargesForInstallment());
|
||||
scheduleParams.addTotalPenaltyChargesCharged(currentPeriodParams.getPenaltyChargesForInstallment());
|
||||
}
|
||||
|
||||
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
|
||||
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);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate periodEnd, PrincipalInterest principalInterestForThisPeriod,
|
||||
Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable,
|
||||
boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) {
|
||||
boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd)
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
|
||||
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
|
||||
cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc);
|
||||
} else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = cumulative.plus(loanCharge.chargeAmount());
|
||||
} else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, cumulative,
|
||||
loanCharge, mc);
|
||||
} else if (isDue) {
|
||||
cumulative = cumulative.plus(loanCharge.amount());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
|
||||
final Set<LoanCharge> loanCharges, 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);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative,
|
||||
final LoanCharge loanCharge, final MathContext mc) {
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount())
|
||||
.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
} else {
|
||||
cumulative = cumulative.plus(loanCharge.amountOrPercentage());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed,
|
||||
final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalDisbursed.getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private void updateAmountsWithEffectiveDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO,
|
||||
LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams,
|
||||
LoanScheduleModelPeriod installment) {
|
||||
LocalDate amountApplicableDate = installment.periodDueDate();
|
||||
if (loanApplicationTerms.isInterestRecalculationEnabled()) {
|
||||
amountApplicableDate = getNextRestScheduleDate(installment.periodDueDate().minusDays(1), loanApplicationTerms,
|
||||
holidayDetailDTO);
|
||||
}
|
||||
updateMapWithAmount(scheduleParams.getPrincipalPortionMap(),
|
||||
currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()), amountApplicableDate);
|
||||
|
||||
// update outstanding balance for interest calculation
|
||||
updateOutstandingBalanceAsPerRest(scheduleParams, scheduledDueDate);
|
||||
}
|
||||
|
||||
private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms,
|
||||
final HolidayDetailDTO holidayDetailDTO) {
|
||||
LocalDate nextScheduleDate;
|
||||
if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) {
|
||||
nextScheduleDate = getScheduledDateGenerator().generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate,
|
||||
loanApplicationTerms, holidayDetailDTO);
|
||||
} else {
|
||||
CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance();
|
||||
nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate);
|
||||
}
|
||||
|
||||
return nextScheduleDate;
|
||||
}
|
||||
|
||||
private void updateMapWithAmount(final Map<LocalDate, Money> map, final Money amount, final LocalDate amountApplicableDate) {
|
||||
Money principalPaid = amount;
|
||||
if (map.containsKey(amountApplicableDate)) {
|
||||
principalPaid = map.get(amountApplicableDate).plus(principalPaid);
|
||||
}
|
||||
map.put(amountApplicableDate, principalPaid);
|
||||
|
||||
}
|
||||
|
||||
private void updateOutstandingBalanceAsPerRest(final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) {
|
||||
scheduleParams.setOutstandingBalanceAsPerRest(updateBalanceForInterestCalculation(scheduleParams.getPrincipalPortionMap(),
|
||||
scheduledDueDate, scheduleParams.getOutstandingBalanceAsPerRest(), false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies all the past date principal changes and apply them on outstanding balance for future calculations
|
||||
*/
|
||||
private Money updateBalanceForInterestCalculation(final Map<LocalDate, Money> principalPortionMap, final LocalDate scheduledDueDate,
|
||||
final Money outstandingBalanceAsPerRest, boolean addMapDetails) {
|
||||
List<LocalDate> removeFromPrincipalPortionMap = new ArrayList<>();
|
||||
Money outstandingBalance = outstandingBalanceAsPerRest;
|
||||
for (Map.Entry<LocalDate, Money> principal : principalPortionMap.entrySet()) {
|
||||
if (!principal.getKey().isAfter(scheduledDueDate)) {
|
||||
if (addMapDetails) {
|
||||
outstandingBalance = outstandingBalance.plus(principal.getValue());
|
||||
} else {
|
||||
outstandingBalance = outstandingBalance.minus(principal.getValue());
|
||||
}
|
||||
removeFromPrincipalPortionMap.add(principal.getKey());
|
||||
}
|
||||
}
|
||||
for (LocalDate date : removeFromPrincipalPortionMap) {
|
||||
principalPortionMap.remove(date);
|
||||
}
|
||||
return outstandingBalance;
|
||||
}
|
||||
|
||||
private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams,
|
||||
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);
|
||||
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment);
|
||||
scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment);
|
||||
scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment));
|
||||
loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
|
||||
Set<LoanCharge> interestCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|
||||
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
|
||||
interestCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
loanCharges.removeAll(interestCharges);
|
||||
return interestCharges;
|
||||
}
|
||||
|
||||
private void updateOutstandingBalance(LoanScheduleParams scheduleParams, ScheduleCurrentPeriodParams currentPeriodParams) {
|
||||
// update outstandingLoanBlance using current period
|
||||
// 'principalDue'
|
||||
scheduleParams
|
||||
.reduceOutstandingBalance(currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loanApplicationTerms
|
||||
* @param scheduleParams
|
||||
* @param scheduledDueDate
|
||||
* @return
|
||||
*/
|
||||
private LoanTermVariationParams applyLoanTermVariations(final LoanApplicationTerms loanApplicationTerms,
|
||||
final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) {
|
||||
boolean skipPeriod = false;
|
||||
boolean recalculateAmounts = false;
|
||||
LocalDate modifiedScheduledDueDate = scheduledDueDate;
|
||||
ArrayList<LoanTermVariationsData> variationsData = null;
|
||||
|
||||
// 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 (!loanTermVariationsData.isSpecificToInstallment()) {
|
||||
scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate);
|
||||
loanApplicationTerms.setNewScheduledDueDateStart(modifiedScheduledDueDate);
|
||||
}
|
||||
loanTermVariationsData.setProcessed(true);
|
||||
}
|
||||
}
|
||||
|
||||
return new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, variationsData);
|
||||
}
|
||||
}
|
||||
+336
-8
@@ -18,30 +18,358 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
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.portfolio.loanaccount.data.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
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.ProgressiveLoanInterestRepaymentModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanScheduleGenerator {
|
||||
public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
|
||||
private final ScheduledDateGenerator scheduledDateGenerator;
|
||||
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
|
||||
private final EMICalculator emiCalculator;
|
||||
|
||||
@Override
|
||||
public ScheduledDateGenerator getScheduledDateGenerator() {
|
||||
return scheduledDateGenerator;
|
||||
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
|
||||
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
|
||||
|
||||
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
|
||||
// generate list of proposed schedule due dates
|
||||
LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
|
||||
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
|
||||
.fetchLoanTermDueDateVariationsData(loanEndDate);
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
}
|
||||
loanApplicationTerms.updateLoanEndDate(loanEndDate);
|
||||
|
||||
// determine the total charges due at time of disbursement
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
|
||||
|
||||
// setup variables for tracking important facts required for loan
|
||||
// schedule generation.
|
||||
|
||||
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
|
||||
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
|
||||
? loanApplicationTerms.getExpectedDisbursementDate()
|
||||
: loanApplicationTerms.getSubmittedOnDate();
|
||||
|
||||
final LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency,
|
||||
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, Money.zero(currency));
|
||||
|
||||
// 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 ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(),
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
|
||||
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size() + 2);
|
||||
|
||||
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
|
||||
|
||||
for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) {
|
||||
scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate());
|
||||
scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate());
|
||||
|
||||
processDisbursements(loanApplicationTerms, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement);
|
||||
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
|
||||
|
||||
emiCalculator.findInterestRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate())
|
||||
.ifPresent(interestRepaymentPeriod -> {
|
||||
final Money principalDue = interestRepaymentPeriod.getPrincipalDue();
|
||||
final Money interestDue = interestRepaymentPeriod.getInterestDue();
|
||||
|
||||
repaymentPeriod.addPrincipalAmount(principalDue);
|
||||
repaymentPeriod.addInterestAmount(interestDue);
|
||||
repaymentPeriod.setOutstandingLoanBalance(interestRepaymentPeriod.getRemainingBalance());
|
||||
|
||||
scheduleParams.addTotalCumulativePrincipal(principalDue);
|
||||
scheduleParams.addTotalCumulativeInterest(interestDue);
|
||||
// add everything
|
||||
scheduleParams.addTotalRepaymentExpected(principalDue.plus(interestDue));
|
||||
});
|
||||
|
||||
applyChargesForCurrentPeriod(repaymentPeriod, loanCharges, scheduleParams, currency, mc);
|
||||
periods.add(repaymentPeriod);
|
||||
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
scheduleParams.incrementPeriodNumber();
|
||||
}
|
||||
|
||||
if (loanApplicationTerms.isMultiDisburseLoan()) {
|
||||
processDisbursements(loanApplicationTerms, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement);
|
||||
}
|
||||
|
||||
// determine fees and penalties for charges which depends on total
|
||||
// loan interest
|
||||
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
|
||||
|
||||
final BigDecimal totalPrincipalPaid = BigDecimal.ZERO;
|
||||
final BigDecimal totalOutstanding = BigDecimal.ZERO;
|
||||
|
||||
return LoanScheduleModel.from(periods, applicationCurrency, interestScheduleModel.getLoanTermInDays(),
|
||||
scheduleParams.getPrincipalToBeScheduled().plus(loanApplicationTerms.getDownPaymentAmount()),
|
||||
scheduleParams.getTotalCumulativePrincipal().plus(loanApplicationTerms.getDownPaymentAmount()).getAmount(),
|
||||
totalPrincipalPaid, scheduleParams.getTotalCumulativeInterest().getAmount(),
|
||||
scheduleParams.getTotalFeeChargesCharged().getAmount(), scheduleParams.getTotalPenaltyChargesCharged().getAmount(),
|
||||
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
|
||||
}
|
||||
|
||||
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
|
||||
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
|
||||
loanApplicationTerms.getDisbursementDatas()
|
||||
.add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(),
|
||||
loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null,
|
||||
null, null));
|
||||
}
|
||||
}
|
||||
|
||||
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams scheduleParams,
|
||||
final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List<LoanScheduleModelPeriod> periods,
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
|
||||
for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) {
|
||||
final LocalDate disbursementDate = disbursementData.disbursementDate();
|
||||
final LocalDate periodFromDate = scheduleParams.getPeriodStartDate();
|
||||
final LocalDate periodDueDate = scheduleParams.getActualRepaymentDate();
|
||||
|
||||
boolean hasDisbursementAfterLastRepaymentPeriod = interestScheduleModel == null && !disbursementDate.isBefore(periodDueDate);
|
||||
boolean hasDisbursementInCurrentRepaymentPeriod = interestScheduleModel != null && !disbursementDate.isBefore(periodFromDate)
|
||||
&& disbursementDate.isBefore(periodDueDate);
|
||||
if (!hasDisbursementAfterLastRepaymentPeriod && !hasDisbursementInCurrentRepaymentPeriod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Money outstandingBalance = emiCalculator.findInterestRepaymentPeriod(interestScheduleModel, periodDueDate)
|
||||
.map(ProgressiveLoanInterestRepaymentModel::getOutstandingBalance)
|
||||
.orElse(Money.zero(loanApplicationTerms.getCurrency()));
|
||||
|
||||
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
.disbursement(disbursementData.disbursementDate(), disbursedAmount, chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
|
||||
// validation check for amount not exceeds specified max
|
||||
// amount as per the configuration
|
||||
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getMaxOutstandingBalance() != null) {
|
||||
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalanceMoney();
|
||||
if (outstandingBalance.plus(disbursedAmount).isGreaterThan(maxOutstandingBalance)) {
|
||||
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
|
||||
throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance(),
|
||||
disbursedAmount);
|
||||
}
|
||||
}
|
||||
|
||||
Money downPaymentAmount = Money.zero(loanApplicationTerms.getCurrency());
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(), MathUtil.percentageOf(disbursedAmount.getAmount(),
|
||||
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
|
||||
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
|
||||
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount,
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
|
||||
}
|
||||
|
||||
LoanScheduleModelDownPaymentPeriod downPaymentPeriod = LoanScheduleModelDownPaymentPeriod.downPayment(
|
||||
scheduleParams.getInstalmentNumber(), disbursementDate, downPaymentAmount,
|
||||
outstandingBalance.plus(disbursedAmount).minus(downPaymentAmount));
|
||||
periods.add(downPaymentPeriod);
|
||||
|
||||
scheduleParams.addTotalRepaymentExpected(downPaymentAmount);
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
}
|
||||
|
||||
final Money disbursementRemainingBalance = disbursedAmount.minus(downPaymentAmount);
|
||||
scheduleParams.addPrincipalToBeScheduled(disbursementRemainingBalance);
|
||||
emiCalculator.addDisbursement(interestScheduleModel, disbursementDate, disbursementRemainingBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator() {
|
||||
return paymentPeriodsInOneYearCalculator;
|
||||
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
|
||||
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
LocalDate rescheduleFrom) {
|
||||
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
|
||||
return LoanScheduleDTO.from(null, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EMICalculator getEMICalculator() {
|
||||
return emiCalculator;
|
||||
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Private, internal methods
|
||||
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
|
||||
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isDueAtDisbursement()) {
|
||||
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
|
||||
}
|
||||
}
|
||||
return chargesDueAtTimeOfDisbursement;
|
||||
}
|
||||
|
||||
private void applyChargesForCurrentPeriod(final LoanScheduleModelRepaymentPeriod repaymentPeriod, final Set<LoanCharge> loanCharges,
|
||||
final LoanScheduleParams scheduleParams, final MonetaryCurrency currency, final MathContext mc) {
|
||||
final PrincipalInterest principalInterest = new PrincipalInterest(repaymentPeriod.getPrincipalDue(),
|
||||
repaymentPeriod.getInterestDue(), null);
|
||||
|
||||
final Money fees = cumulativeFeeChargesDueWithin(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate(), loanCharges, currency,
|
||||
principalInterest, scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), true,
|
||||
scheduleParams.isFirstPeriod(), mc);
|
||||
final Money penalties = cumulativePenaltyChargesDueWithin(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate(), loanCharges,
|
||||
currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), true,
|
||||
scheduleParams.isFirstPeriod(), mc);
|
||||
|
||||
repaymentPeriod.addLoanCharges(fees.getAmount(), penalties.getAmount());
|
||||
scheduleParams.addTotalFeeChargesCharged(fees);
|
||||
scheduleParams.addTotalPenaltyChargesCharged(penalties);
|
||||
}
|
||||
|
||||
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
|
||||
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);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate periodEnd, PrincipalInterest principalInterestForThisPeriod,
|
||||
Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable,
|
||||
boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) {
|
||||
boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd)
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
|
||||
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
|
||||
cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc);
|
||||
} else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = cumulative.plus(loanCharge.chargeAmount());
|
||||
} else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, cumulative,
|
||||
loanCharge, mc);
|
||||
} else if (isDue) {
|
||||
cumulative = cumulative.plus(loanCharge.amount());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
|
||||
final Set<LoanCharge> loanCharges, 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);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative,
|
||||
final LoanCharge loanCharge, final MathContext mc) {
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount())
|
||||
.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
} else {
|
||||
cumulative = cumulative.plus(loanCharge.amountOrPercentage());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed,
|
||||
final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalDisbursed.getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams,
|
||||
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);
|
||||
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment);
|
||||
scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment);
|
||||
scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment));
|
||||
loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
|
||||
Set<LoanCharge> interestCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|
||||
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
|
||||
interestCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
loanCharges.removeAll(interestCharges);
|
||||
return interestCharges;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-8
@@ -19,19 +19,31 @@
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
|
||||
public interface EMICalculator {
|
||||
|
||||
EMICalculationResult calculateEMIValueAndRateFactors(Money outstandingBalanceAsPerRest,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
|
||||
Integer actualPeriodNumber, Integer numberOfRepayments, MathContext mc);
|
||||
ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
|
||||
PrincipalInterest calculatePrincipalInterestComponentsForPeriod(EMICalculationResult emiCalculationResult,
|
||||
Money outstandingBalanceAsPerRest, Integer installmentAmountInMultiplesOf, Integer actualPeriodNumber,
|
||||
Integer actualNoOfRepayments, MathContext mc);
|
||||
ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc);
|
||||
|
||||
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LocalDate dueDate);
|
||||
|
||||
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
}
|
||||
|
||||
+326
-197
@@ -22,43 +22,208 @@ import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
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.portfolio.common.domain.DaysInMonthType;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentInterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.mapper.ProgressiveLoanInterestRepaymentModelMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
private final ProgressiveLoanInterestRepaymentModelMapper progressiveLoanInterestRepaymentModelMapper;
|
||||
|
||||
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
|
||||
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency());
|
||||
final ArrayList<ProgressiveLoanInterestRepaymentModel> interestRepaymentModelList = new ArrayList<>(periods.size());
|
||||
for (final LoanScheduleModelRepaymentPeriod period : periods) {
|
||||
interestRepaymentModelList
|
||||
.add(new ProgressiveLoanInterestRepaymentModel(period.periodFromDate(), period.periodDueDate(), zeroAmount));
|
||||
}
|
||||
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriod(
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate) {
|
||||
if (scheduleModel == null || repaymentPeriodDueDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return scheduleModel.repayments().stream()//
|
||||
.filter(interestRepaymentPeriodItem -> interestRepaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriodForDisbursement(
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate) {
|
||||
if (scheduleModel == null || disbursementDueDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return scheduleModel.repayments().stream()//
|
||||
.filter(repaymentPeriod -> !disbursementDueDate.isBefore(repaymentPeriod.getFromDate())
|
||||
&& disbursementDueDate.isBefore(repaymentPeriod.getDueDate()))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
Optional<ProgressiveLoanInterestRepaymentInterestPeriod> findInterestPeriodForDisbursement(
|
||||
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate disbursementDueDate) {
|
||||
if (repaymentPeriod == null || disbursementDueDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return repaymentPeriod.getInterestPeriods().stream()//
|
||||
.filter(interestPeriod -> disbursementDueDate.isEqual(interestPeriod.getFromDate()))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add disbursement to Interest Period
|
||||
*/
|
||||
@Override
|
||||
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
|
||||
final Money disbursedAmount) {
|
||||
final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForDisbursement(scheduleModel,
|
||||
disbursementDueDate).orElse(null);
|
||||
if (repaymentPeriod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var interestPeriodOptional = findInterestPeriodForDisbursement(repaymentPeriod, disbursementDueDate);
|
||||
if (interestPeriodOptional.isPresent()) {
|
||||
interestPeriodOptional.get().addDisbursedAmount(disbursedAmount);
|
||||
} else {
|
||||
insertInterestPeriod(repaymentPeriod, disbursementDueDate, disbursedAmount);
|
||||
}
|
||||
|
||||
calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel);
|
||||
}
|
||||
|
||||
void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate disbursementDueDate,
|
||||
final Money disbursedAmount) {
|
||||
// disbursementDueDate is after disb.date because this case when disbursement date is different than interest
|
||||
// period start date
|
||||
final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(interestPeriod -> disbursementDueDate.isAfter(interestPeriod.getFromDate())
|
||||
&& disbursementDueDate.isBefore(interestPeriod.getDueDate()))//
|
||||
.findFirst()//
|
||||
.get();//
|
||||
|
||||
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(disbursementDueDate,
|
||||
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, disbursedAmount, Money.zero(disbursedAmount.getCurrency()));
|
||||
|
||||
previousInterestPeriod.setDueDate(disbursementDueDate);
|
||||
|
||||
repaymentPeriod.getInterestPeriods().add(interestPeriod);
|
||||
Collections.sort(repaymentPeriod.getInterestPeriods());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
return makeScheduleModelDeepCopy(scheduleModel, scheduleModel.loanProductRelatedDetail(),
|
||||
scheduleModel.installmentAmountInMultiplesOf(), scheduleModel.mc());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
List<ProgressiveLoanInterestRepaymentModel> repayments = new ArrayList<>(scheduleModel.repayments().size());
|
||||
for (var repaymentModel : scheduleModel.repayments()) {
|
||||
repayments.add(new ProgressiveLoanInterestRepaymentModel(repaymentModel));
|
||||
}
|
||||
return new ProgressiveLoanInterestScheduleModel(repayments, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
|
||||
*/
|
||||
@Override
|
||||
public EMICalculationResult calculateEMIValueAndRateFactors(final Money outstandingBalanceAsPerRest,
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail, final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
|
||||
final Integer actualPeriodNumber, final Integer numberOfRepayments, final MathContext mc) {
|
||||
final BigDecimal nominalInterestRatePerPeriod = calcNominalInterestRatePerPeriod(
|
||||
loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc);
|
||||
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
|
||||
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
|
||||
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
|
||||
final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
|
||||
void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
final List<ProgressiveLoanInterestRepaymentModel> relatedRepaymentPeriods = getRelatedRepaymentPeriods(
|
||||
calculateFromRepaymentPeriodDueDate, scheduleModel);
|
||||
calculateRateFactorMinus1ForPeriods(relatedRepaymentPeriods, scheduleModel);
|
||||
calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel);
|
||||
calculatePrincipalInterestComponentsForPeriods(scheduleModel);
|
||||
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
|
||||
}
|
||||
|
||||
final List<BigDecimal> rateFactorList = getRateFactorList(expectedRepaymentPeriods, nominalInterestRatePerPeriod, daysInYearType,
|
||||
daysInMonthType, repaymentFrequency, repaymentEvery, mc);
|
||||
private static List<ProgressiveLoanInterestRepaymentModel> getRelatedRepaymentPeriods(LocalDate calculateFromRepaymentPeriodDueDate,
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
return calculateFromRepaymentPeriodDueDate == null ? scheduleModel.repayments()
|
||||
: scheduleModel.repayments().stream().filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return calculateEMI(rateFactorList, actualPeriodNumber, numberOfRepayments, outstandingBalanceAsPerRest, mc);
|
||||
private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final List<ProgressiveLoanInterestRepaymentModel> relatedRepaymentPeriods) {
|
||||
final Money emiDifference = getDifferenceBetweenLastTwoPeriod(relatedRepaymentPeriods, scheduleModel);
|
||||
final int numberOfRelatedPeriods = relatedRepaymentPeriods.size();
|
||||
double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods / 2.0);
|
||||
if (emiDifference.isZero() || lowerHalfOfRelatedPeriods == 0.0) {
|
||||
return;
|
||||
}
|
||||
final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEqualMonthlyInstallment();
|
||||
boolean shouldBeAdjusted = emiDifference.abs().multipliedBy(100)
|
||||
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods)));
|
||||
|
||||
final MathContext mc = scheduleModel.mc();
|
||||
if (shouldBeAdjusted) {
|
||||
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc.getRoundingMode());
|
||||
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, originalEmi.plus(adjustment));
|
||||
if (adjustedEqualMonthlyInstallmentValue.compareTo(originalEmi) == 0) {
|
||||
return;
|
||||
}
|
||||
final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate();
|
||||
final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel);
|
||||
newScheduleModel.repayments().forEach(period -> {
|
||||
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) {
|
||||
period.setEqualMonthlyInstallment(adjustedEqualMonthlyInstallmentValue);
|
||||
}
|
||||
});
|
||||
calculatePrincipalInterestComponentsForPeriods(newScheduleModel);
|
||||
final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repayments(), scheduleModel);
|
||||
final boolean newEmiHasLessDifference = newEmiDifference.abs().compareTo(emiDifference.abs()) < 0;
|
||||
if (!newEmiHasLessDifference) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Iterator<ProgressiveLoanInterestRepaymentModel> relatedPeriodFromNewModelIterator = newScheduleModel//
|
||||
.repayments().stream()//
|
||||
.filter(period -> !period.getDueDate().isBefore(relatedPeriodsFirstDueDate))//
|
||||
.toList().iterator();//
|
||||
|
||||
relatedRepaymentPeriods.forEach(relatedRepaymentPeriod -> {
|
||||
if (!relatedPeriodFromNewModelIterator.hasNext()) {
|
||||
return;
|
||||
}
|
||||
final ProgressiveLoanInterestRepaymentModel newRepaymentPeriod = relatedPeriodFromNewModelIterator.next();
|
||||
relatedRepaymentPeriod.setEqualMonthlyInstallment(newRepaymentPeriod.getEqualMonthlyInstallment());
|
||||
relatedRepaymentPeriod.setPrincipalDue(newRepaymentPeriod.getInterestDue());
|
||||
relatedRepaymentPeriod.setPrincipalDue(newRepaymentPeriod.getPrincipalDue());
|
||||
relatedRepaymentPeriod.setRemainingBalance(newRepaymentPeriod.getRemainingBalance());
|
||||
relatedRepaymentPeriod.setInterestPeriods(newRepaymentPeriod.getInterestPeriods());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,83 +236,73 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
*
|
||||
* @return Rate Interest Rate in fraction format
|
||||
*/
|
||||
BigDecimal calcNominalInterestRatePerPeriod(final BigDecimal interestRate, MathContext mc) {
|
||||
BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate, MathContext mc) {
|
||||
return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* * Calculate rate factors from ONLY repayment periods
|
||||
*
|
||||
* @param expectedRepaymentPeriods
|
||||
* @param interestRate
|
||||
* @param daysInYearType
|
||||
* @param daysInMonthType
|
||||
* @param repaymentFrequency
|
||||
* @param repaymentEvery
|
||||
* @param mc
|
||||
* @return
|
||||
*/
|
||||
List<BigDecimal> getRateFactorList(final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
|
||||
final BigDecimal interestRate, final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
|
||||
final PeriodFrequencyType repaymentFrequency, final BigDecimal repaymentEvery, final MathContext mc) {
|
||||
return expectedRepaymentPeriods.stream().filter(LoanScheduleModelPeriod::isRepaymentPeriod)
|
||||
.map(period -> MathUtil.stripTrailingZeros(calculateRateFactorPerPeriod(period, interestRate, daysInYearType,
|
||||
daysInMonthType, repaymentFrequency, repaymentEvery, mc)))
|
||||
.toList();
|
||||
void calculateRateFactorMinus1ForPeriods(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
for (var repaymentPeriod : repaymentPeriods) {
|
||||
for (var interestPeriod : repaymentPeriod.getInterestPeriods()) {
|
||||
interestPeriod.setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Rate Factor for an exact Period
|
||||
*
|
||||
* @param repaymentPeriod
|
||||
* @param interestRate
|
||||
* @param daysInYearType
|
||||
* @param daysInMonthType
|
||||
* @param repaymentFrequency
|
||||
* @param repaymentEvery
|
||||
* @param mc
|
||||
* @return
|
||||
* Calculate Rate Factor-1 for an exact Period
|
||||
*/
|
||||
BigDecimal calculateRateFactorPerPeriod(final LoanScheduleModelPeriod repaymentPeriod, final BigDecimal interestRate,
|
||||
final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType, final PeriodFrequencyType repaymentFrequency,
|
||||
final BigDecimal repaymentEvery, final MathContext mc) {
|
||||
BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(repaymentPeriod.periodFromDate()));
|
||||
BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(repaymentPeriod.periodFromDate()));
|
||||
BigDecimal calculateRateFactorMinus1PerPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
|
||||
final ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod, final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
final MathContext mc = scheduleModel.mc();
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
|
||||
final BigDecimal interestRate = calcNominalInterestRatePercentage(loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc);
|
||||
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
|
||||
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
|
||||
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
|
||||
final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
|
||||
|
||||
final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(interestPeriod.getFromDate()));
|
||||
final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(interestPeriod.getFromDate()));
|
||||
final BigDecimal actualDaysInPeriod = BigDecimal
|
||||
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(), repaymentPeriod.periodDueDate()));
|
||||
.valueOf(DateUtils.getDifferenceInDays(interestPeriod.getFromDate(), interestPeriod.getDueDate()));
|
||||
final BigDecimal calculatedDaysInPeriod = BigDecimal
|
||||
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(), repaymentPeriod.periodDueDate()));
|
||||
final int numberOfYearsDifferenceInPeriod = repaymentPeriod.periodDueDate().getYear() - repaymentPeriod.periodFromDate().getYear();
|
||||
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
|
||||
final int numberOfYearsDifferenceInPeriod = interestPeriod.getDueDate().getYear() - interestPeriod.getFromDate().getYear();
|
||||
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0;
|
||||
|
||||
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
|
||||
// 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(repaymentPeriod, mc);
|
||||
final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriod, mc);
|
||||
return rateFactorByRepaymentPartialPeriod(interestRate, repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE,
|
||||
BigDecimal.ONE, mc);
|
||||
BigDecimal.ONE, mc).setScale(mc.getPrecision(), mc.getRoundingMode());
|
||||
}
|
||||
|
||||
return calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth,
|
||||
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
|
||||
return calculateRateFactorMinus1PerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth,
|
||||
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Period fractions part based on how much year has in the period
|
||||
*
|
||||
* @param repaymentPeriod
|
||||
* @param interestPeriod
|
||||
* @param mc
|
||||
* @return
|
||||
*/
|
||||
BigDecimal calculatePeriodFractions(LoanScheduleModelPeriod repaymentPeriod, MathContext mc) {
|
||||
BigDecimal calculatePeriodFractions(ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod, MathContext mc) {
|
||||
BigDecimal cumulatedRateFactor = BigDecimal.ZERO;
|
||||
int actualYear = repaymentPeriod.periodFromDate().getYear();
|
||||
int endYear = repaymentPeriod.periodDueDate().getYear();
|
||||
LocalDate actualDate = repaymentPeriod.periodFromDate();
|
||||
int actualYear = interestPeriod.getFromDate().getYear();
|
||||
int endYear = interestPeriod.getDueDate().getYear();
|
||||
LocalDate actualDate = interestPeriod.getFromDate();
|
||||
LocalDate endOfActualYear;
|
||||
|
||||
while (actualYear <= endYear) {
|
||||
endOfActualYear = actualYear == endYear ? repaymentPeriod.periodDueDate() : LocalDate.of(actualYear, 12, 31);
|
||||
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, mc), mc);
|
||||
@@ -170,127 +325,81 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
* @param mc
|
||||
* @return
|
||||
*/
|
||||
BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final BigDecimal interestRate,
|
||||
BigDecimal calculateRateFactorMinus1PerPeriodBasedOnRepaymentFrequency(final BigDecimal interestRate,
|
||||
final PeriodFrequencyType repaymentFrequency, final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
|
||||
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
|
||||
final MathContext mc) {
|
||||
return switch (repaymentFrequency) {
|
||||
case DAYS ->
|
||||
rateFactorByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
|
||||
case WEEKS ->
|
||||
rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
|
||||
case MONTHS -> rateFactorByRepaymentEveryMonth(interestRate, repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
|
||||
case DAYS -> rateFactorMinus1ByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
case WEEKS -> rateFactorMinus1ByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
case MONTHS -> rateFactorMinus1ByRepaymentEveryMonth(interestRate, repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
default -> throw new UnsupportedOperationException("Invalid repayment frequency"); // not supported yet
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate EMI parts and return an EMI calculation result object with repayment installment rate factors
|
||||
*/
|
||||
EMICalculationResult calculateEMI(final List<BigDecimal> rateFactorList, final Integer actualPeriodNumber,
|
||||
final Integer numberOfRepayments, final Money outstandingBalanceForRest, final MathContext mc) {
|
||||
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorN(rateFactorList, mc));
|
||||
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(rateFactorList, mc));
|
||||
void calculateEMIOnPeriods(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
if (repaymentPeriods.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final MathContext mc = scheduleModel.mc();
|
||||
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorN(repaymentPeriods, mc));
|
||||
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
|
||||
final var startPeriod = repaymentPeriods.get(0);
|
||||
final Money remainingBalanceFromPreviousPeriod = getRemainingBalanceFromPreviousPeriod(scheduleModel, startPeriod);
|
||||
final Money outstandingBalance = remainingBalanceFromPreviousPeriod.add(startPeriod.getDisbursedAmountInPeriod());
|
||||
|
||||
final Money emiValue = Money.of(outstandingBalanceForRest.getCurrency(),
|
||||
calculateEMIValue(rateFactorN, outstandingBalanceForRest.getAmount(), fnResult, mc));
|
||||
final List<BigDecimal> rateFactorMinus1List = getRateFactorMinus1List(rateFactorList, mc);
|
||||
final Money equalMonthlyInstallment = Money.of(outstandingBalance.getCurrency(),
|
||||
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc));
|
||||
final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
|
||||
|
||||
final Money adjustedEqualMonthlyInstallmentValue = adjustEMIForMoreStreamlinedRepaymentSchedule(actualPeriodNumber,
|
||||
numberOfRepayments, outstandingBalanceForRest, emiValue, rateFactorMinus1List, mc);
|
||||
|
||||
return new EMICalculationResult(adjustedEqualMonthlyInstallmentValue, rateFactorMinus1List);
|
||||
repaymentPeriods.forEach(period -> period.setEqualMonthlyInstallment(finalEqualMonthlyInstallment));
|
||||
}
|
||||
|
||||
/**
|
||||
* Due to rounding or unequal installments, the first calculated EMI might not be the best one! Reiterate with
|
||||
* adjusted EMI to get a better streamlined repayment schedule (less difference between calculated EMI and last
|
||||
* installment EMI).
|
||||
*/
|
||||
Money adjustEMIForMoreStreamlinedRepaymentSchedule(final Integer actualPeriodNumber, final Integer numberOfRepayments,
|
||||
final Money outstandingBalanceAsPerRest, final Money equalMonthlyInstallmentValue, List<BigDecimal> rateFactorMinus1List,
|
||||
final MathContext mc) {
|
||||
int numberOfUpcomingPeriods = numberOfRepayments - actualPeriodNumber + 1;
|
||||
Money getRemainingBalanceFromPreviousPeriod(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
ProgressiveLoanInterestRepaymentModel startPeriod) {
|
||||
return scheduleModel.repayments().stream().filter(period -> period.getDueDate().isEqual(startPeriod.getFromDate()))
|
||||
.map(ProgressiveLoanInterestRepaymentModel::getRemainingBalance).findFirst()
|
||||
.orElse(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()));
|
||||
}
|
||||
|
||||
Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final Money equalMonthlyInstallment) {
|
||||
return scheduleModel.installmentAmountInMultiplesOf() != null
|
||||
? Money.roundToMultiplesOf(equalMonthlyInstallment, scheduleModel.installmentAmountInMultiplesOf())
|
||||
: equalMonthlyInstallment;
|
||||
}
|
||||
|
||||
Money getDifferenceBetweenLastTwoPeriod(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
int numberOfUpcomingPeriods = repaymentPeriods.size();
|
||||
if (numberOfUpcomingPeriods < 2) {
|
||||
return equalMonthlyInstallmentValue;
|
||||
return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
|
||||
}
|
||||
|
||||
RepaymentScheduleModel repaymentScheduleModel = generateRepaymentScheduleModel(numberOfRepayments, outstandingBalanceAsPerRest,
|
||||
equalMonthlyInstallmentValue, rateFactorMinus1List);
|
||||
Money calculatedLastEMI = repaymentScheduleModel.getScheduleList().get(repaymentScheduleModel.getScheduleList().size() - 1).emi();
|
||||
Money originalDifference = calculatedLastEMI.minus(equalMonthlyInstallmentValue);
|
||||
if (originalDifference.isZero()) {
|
||||
return equalMonthlyInstallmentValue;
|
||||
}
|
||||
double lowerHalfOfUpcomingPeriods = Math.floor((double) numberOfUpcomingPeriods / 2);
|
||||
if (lowerHalfOfUpcomingPeriods == 0.0) {
|
||||
return equalMonthlyInstallmentValue;
|
||||
}
|
||||
boolean shouldBeAdjusted = originalDifference.abs().multipliedBy(100)
|
||||
.isGreaterThan(Money.of(equalMonthlyInstallmentValue.getCurrency(), BigDecimal.valueOf(lowerHalfOfUpcomingPeriods)));
|
||||
// Reiterate only when needed
|
||||
if (shouldBeAdjusted) {
|
||||
Money adjustment = originalDifference.dividedBy(numberOfUpcomingPeriods, mc.getRoundingMode());
|
||||
|
||||
Money adjustedEqualMonthlyInstallmentValue = equalMonthlyInstallmentValue.plus(adjustment);
|
||||
RepaymentScheduleModel repaymentScheduleModelWithAdjustedEMI = generateRepaymentScheduleModel(numberOfRepayments,
|
||||
outstandingBalanceAsPerRest, adjustedEqualMonthlyInstallmentValue, rateFactorMinus1List);
|
||||
Money calculatedLastEMIAfterAdjustment = repaymentScheduleModelWithAdjustedEMI.getScheduleList()
|
||||
.get(repaymentScheduleModelWithAdjustedEMI.getScheduleList().size() - 1).emi();
|
||||
Money differenceAfterEMIAdjustment = calculatedLastEMIAfterAdjustment.minus(adjustedEqualMonthlyInstallmentValue);
|
||||
// Use the adjusted EMI only if it is better than the original one
|
||||
return differenceAfterEMIAdjustment.abs().isLessThan(originalDifference.abs()) ? adjustedEqualMonthlyInstallmentValue
|
||||
: equalMonthlyInstallmentValue;
|
||||
} else {
|
||||
return equalMonthlyInstallmentValue;
|
||||
}
|
||||
}
|
||||
|
||||
RepaymentScheduleModel generateRepaymentScheduleModel(final Integer numberOfRepayments, final Money outstandingBalanceAsPerRest,
|
||||
Money equalMonthlyInstallmentValue, List<BigDecimal> rateFactorMinus1List) {
|
||||
RepaymentScheduleModel repaymentScheduleModel = new RepaymentScheduleModel();
|
||||
Money balanceOfLoan = outstandingBalanceAsPerRest;
|
||||
for (int i = 0; i < numberOfRepayments; i++) {
|
||||
final Money calculatedInterest = balanceOfLoan.multipliedBy(rateFactorMinus1List.get(i));
|
||||
// WE need to calculate EMI differently for last installment (decided by number of repayments or when
|
||||
// schedule got shorter then planned)
|
||||
if (balanceOfLoan.isLessThan(equalMonthlyInstallmentValue.minus(calculatedInterest)) || i == numberOfRepayments - 1) {
|
||||
equalMonthlyInstallmentValue = balanceOfLoan.plus(calculatedInterest);
|
||||
}
|
||||
final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest);
|
||||
repaymentScheduleModel.addRepaymentPeriodModel(
|
||||
new RepaymentPeriodModel(balanceOfLoan, equalMonthlyInstallmentValue, calculatedInterest, calculatedPrincipal));
|
||||
balanceOfLoan = balanceOfLoan.minus(calculatedPrincipal);
|
||||
// We can stop processing if there is no outstanding principal
|
||||
if (balanceOfLoan.isZero()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repaymentScheduleModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate factor -1 values
|
||||
*
|
||||
*/
|
||||
List<BigDecimal> getRateFactorMinus1List(final List<BigDecimal> rateFactors, final MathContext mc) {
|
||||
return rateFactors.stream().map(it -> it.subtract(BigDecimal.ONE, mc)).toList();
|
||||
final var lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1);
|
||||
final var penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2);
|
||||
return lastPeriod.getEqualMonthlyInstallment().minus(penultimatePeriod.getEqualMonthlyInstallment());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Rate Factor Product from rate factors
|
||||
*/
|
||||
BigDecimal calculateRateFactorN(final List<BigDecimal> rateFactors, final MathContext mc) {
|
||||
return rateFactors.stream().reduce(BigDecimal.ONE, (BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc));
|
||||
BigDecimal calculateRateFactorN(final List<ProgressiveLoanInterestRepaymentModel> periods, final MathContext mc) {
|
||||
return periods.stream().map(ProgressiveLoanInterestRepaymentModel::getRateFactor).reduce(BigDecimal.ONE,
|
||||
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize Fn values
|
||||
*/
|
||||
BigDecimal calculateFnResult(final List<BigDecimal> rateFactors, final MathContext mc) {
|
||||
return rateFactors.stream().skip(1).reduce(BigDecimal.ONE,
|
||||
(BigDecimal previousValue, BigDecimal rateFactor) -> fnValue(previousValue, rateFactor, mc));
|
||||
BigDecimal calculateFnResult(final List<ProgressiveLoanInterestRepaymentModel> periods, final MathContext mc) {
|
||||
return periods.stream()//
|
||||
.skip(1)//
|
||||
.map(ProgressiveLoanInterestRepaymentModel::getRateFactor)//
|
||||
.reduce(BigDecimal.ONE, (BigDecimal previousValue, BigDecimal rateFactor) -> fnValue(previousValue, rateFactor, mc));//
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,9 +435,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
* @param mc
|
||||
* @return Rate Factor for period
|
||||
*/
|
||||
BigDecimal rateFactorByRepaymentEveryDay(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
|
||||
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
|
||||
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
BigDecimal rateFactorMinus1ByRepaymentEveryDay(final BigDecimal interestRate, final BigDecimal repaymentEvery,
|
||||
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
|
||||
final MathContext mc) {
|
||||
return rateFactorMinus1ByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
}
|
||||
|
||||
@@ -357,9 +467,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
* @param mc
|
||||
* @return Rate Factor for period
|
||||
*/
|
||||
BigDecimal rateFactorByRepaymentEveryWeek(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
|
||||
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
|
||||
return rateFactorByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
BigDecimal rateFactorMinus1ByRepaymentEveryWeek(final BigDecimal interestRate, final BigDecimal repaymentEvery,
|
||||
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
|
||||
final MathContext mc) {
|
||||
return rateFactorMinus1ByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
}
|
||||
|
||||
@@ -391,10 +502,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
* @param mc
|
||||
* @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 MathContext mc) {
|
||||
return rateFactorByRepaymentPeriod(interestRate, daysInMonth, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
BigDecimal rateFactorMinus1ByRepaymentEveryMonth(final BigDecimal interestRate, final BigDecimal repaymentEvery,
|
||||
final BigDecimal daysInMonth, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
|
||||
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
|
||||
return rateFactorMinus1ByRepaymentPeriod(interestRate, daysInMonth, repaymentEvery, daysInYear, actualDaysInPeriod,
|
||||
calculatedDaysInPeriod, mc);
|
||||
}
|
||||
|
||||
@@ -426,13 +537,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
* @param mc
|
||||
* @return Rate Factor for period
|
||||
*/
|
||||
BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
|
||||
BigDecimal rateFactorMinus1ByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
|
||||
final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
|
||||
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
|
||||
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays.multiply(repaymentEvery, mc).divide(daysInYear, mc);
|
||||
return BigDecimal.ONE.add(
|
||||
interestRate.multiply(interestFractionPerPeriod, mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, mc),
|
||||
mc);
|
||||
return interestRate//
|
||||
.multiply(interestFractionPerPeriod, mc)//
|
||||
.multiply(actualDaysInPeriod, mc)//
|
||||
.divide(calculatedDaysInPeriod, mc);//
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,10 +554,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery,
|
||||
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
|
||||
final MathContext mc) {
|
||||
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio);
|
||||
return BigDecimal.ONE.add(
|
||||
interestRate.multiply(interestFractionPerPeriod, mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, mc),
|
||||
mc);
|
||||
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio, mc);
|
||||
return interestRate//
|
||||
.multiply(interestFractionPerPeriod, mc)//
|
||||
.multiply(actualDaysInPeriod, mc)//
|
||||
.divide(calculatedDaysInPeriod, mc);//
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,30 +577,46 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final EMICalculationResult emiCalculationResult,
|
||||
final Money outstandingBalanceAsPerRest, final Integer installmentAmountInMultiplesOf, final Integer actualPeriodNumber,
|
||||
final Integer actualNoOfRepayments, final MathContext mc) {
|
||||
|
||||
final Money equalMonthlyInstallmentValue = installmentAmountInMultiplesOf != null
|
||||
? Money.roundToMultiplesOf(emiCalculationResult.getEqualMonthlyInstallmentValue(), installmentAmountInMultiplesOf)
|
||||
: emiCalculationResult.getEqualMonthlyInstallmentValue();
|
||||
final BigDecimal rateFactorMinus1 = emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1();
|
||||
final Money calculatedInterest = outstandingBalanceAsPerRest.multipliedBy(rateFactorMinus1);
|
||||
final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest);
|
||||
return new PrincipalInterest(
|
||||
adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(calculatedPrincipal, outstandingBalanceAsPerRest,
|
||||
actualPeriodNumber, actualNoOfRepayments),
|
||||
calculatedInterest, Money.zero(equalMonthlyInstallmentValue.getCurrency()));
|
||||
void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
Money outstandingBalance = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
|
||||
int numberOfPeriods = scheduleModel.repayments().size();
|
||||
int period = 0;
|
||||
for (var repaymentPeriod : scheduleModel.repayments()) {
|
||||
final boolean isLastPeriod = ++period == numberOfPeriods;
|
||||
outstandingBalance = calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, outstandingBalance, isLastPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
Money adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(final Money calculatedPrincipal,
|
||||
final Money outstandingBalanceAsPerRest, final Integer actualPeriodNumber, final Integer actualNoOfRepayments) {
|
||||
final boolean isLastRepaymentPeriod = Objects.equals(actualPeriodNumber, actualNoOfRepayments);
|
||||
if (isLastRepaymentPeriod) {
|
||||
final Money remainingAmount = outstandingBalanceAsPerRest.minus(calculatedPrincipal);
|
||||
return calculatedPrincipal.plus(remainingAmount);
|
||||
Money calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
|
||||
final Money initBalance, final boolean isLastPeriod) {
|
||||
Money outstandingBalance = initBalance;
|
||||
for (ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
|
||||
outstandingBalance = outstandingBalance.plus(interestPeriod.getDisbursedAmount());
|
||||
final Money calculatedInterest = outstandingBalance.multipliedBy(interestPeriod.getRateFactorMinus1());
|
||||
interestPeriod.setInterestDue(calculatedInterest);
|
||||
}
|
||||
return calculatedPrincipal;
|
||||
|
||||
final Money cumulatedInterest = repaymentPeriod.getInterestDue();
|
||||
final Money calculatedPrincipal = isLastPeriod ? outstandingBalance
|
||||
: repaymentPeriod.getEqualMonthlyInstallment().minus(cumulatedInterest);
|
||||
|
||||
if (isLastPeriod) {
|
||||
repaymentPeriod.setEqualMonthlyInstallment(calculatedPrincipal.add(cumulatedInterest));
|
||||
}
|
||||
|
||||
repaymentPeriod.setPrincipalDue(calculatedPrincipal);
|
||||
repaymentPeriod.setRemainingBalance(outstandingBalance.minus(calculatedPrincipal));
|
||||
|
||||
return repaymentPeriod.getRemainingBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc) {
|
||||
List<LoanRepaymentScheduleInstallment> repaymentModelsWithoutDownPayment = repaymentPeriods.stream()
|
||||
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
|
||||
List<ProgressiveLoanInterestRepaymentModel> repaymentModels = progressiveLoanInterestRepaymentModelMapper
|
||||
.map(repaymentModelsWithoutDownPayment);
|
||||
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
}
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
public record RepaymentPeriodModel(Money balanceOfLoan, Money emi, Money principal, Money interest) {
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.loanproduct.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface ProgressiveLoanInterestRepaymentModelMapper {
|
||||
|
||||
List<ProgressiveLoanInterestRepaymentModel> map(List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
|
||||
|
||||
default ProgressiveLoanInterestRepaymentModel map(LoanRepaymentScheduleInstallment repaymentScheduleInstallment) {
|
||||
return new ProgressiveLoanInterestRepaymentModel(repaymentScheduleInstallment.getFromDate(),
|
||||
repaymentScheduleInstallment.getDueDate(), Money.zero(repaymentScheduleInstallment.getLoan().getCurrency()));
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -61,10 +61,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
@@ -90,7 +90,7 @@ 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 LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class);
|
||||
private final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
@@ -104,7 +104,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator);
|
||||
|
||||
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||
@@ -148,7 +148,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||
@@ -192,7 +192,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero));
|
||||
@@ -243,7 +243,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||
@@ -271,7 +271,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
installments.add(installment);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
@@ -328,7 +328,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
installments.add(installment);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify charges on installment
|
||||
@@ -380,7 +380,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
installments.add(installment2);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
|
||||
+524
-409
File diff suppressed because it is too large
Load Diff
-1
@@ -37,7 +37,6 @@ public class ApplyChargeToOverdueLoansBusinessStep implements LoanCOBBusinessSte
|
||||
public Loan execute(Loan loan) {
|
||||
final Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList = loanReadPlatformService
|
||||
.retrieveAllOverdueInstallmentsForLoan(loan);
|
||||
|
||||
loanChargeWritePlatformService.applyOverdueChargesForLoan(loan.getId(), overdueLoanScheduleDataList);
|
||||
return loan;
|
||||
}
|
||||
|
||||
+24
-6
@@ -27,6 +27,7 @@ import net.fortuna.ical4j.model.Recur;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
|
||||
import org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType;
|
||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
|
||||
@@ -36,6 +37,7 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarHistory;
|
||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -43,30 +45,46 @@ import org.springframework.stereotype.Component;
|
||||
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
|
||||
|
||||
@Override
|
||||
public List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(final LocalDate scheduledDueDate,
|
||||
public List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(final LocalDate scheduleStartDate,
|
||||
final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
|
||||
final Money zeroAmount = Money.zero(loanApplicationTerms.getCurrency());
|
||||
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
|
||||
final ArrayList<PreGeneratedLoanSchedulePeriod> repaymentPeriods = new ArrayList<>(numberOfRepayments);
|
||||
LocalDate lastRepaymentDate = scheduledDueDate;
|
||||
final ArrayList<LoanScheduleModelRepaymentPeriod> repaymentPeriods = new ArrayList<>(numberOfRepayments);
|
||||
LocalDate lastRepaymentDate = scheduleStartDate;
|
||||
LocalDate nextRepaymentDate;
|
||||
boolean isFirstRepayment = true;
|
||||
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
|
||||
for (int repaymentPeriodNumber = 1; repaymentPeriodNumber <= numberOfRepayments; repaymentPeriodNumber++) {
|
||||
nextRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment);
|
||||
|
||||
if (repaymentPeriod == numberOfRepayments) { // last period
|
||||
if (repaymentPeriodNumber == numberOfRepayments) { // last period
|
||||
if (loanApplicationTerms.getFixedLength() != null
|
||||
&& loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(nextRepaymentDate) != 0) {
|
||||
nextRepaymentDate = loanApplicationTerms.calculateMaxDateForFixedLength();
|
||||
}
|
||||
nextRepaymentDate = adjustRepaymentDate(nextRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
|
||||
}
|
||||
repaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(repaymentPeriod, lastRepaymentDate, nextRepaymentDate));
|
||||
nextRepaymentDate = applyLoanTermVariations(loanApplicationTerms, nextRepaymentDate);
|
||||
repaymentPeriods.add(LoanScheduleModelRepaymentPeriod.repayment(repaymentPeriodNumber, lastRepaymentDate, nextRepaymentDate,
|
||||
zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, false));
|
||||
lastRepaymentDate = nextRepaymentDate;
|
||||
isFirstRepayment = false;
|
||||
}
|
||||
return repaymentPeriods;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
return modifiedScheduledDueDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getLastRepaymentDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
|
||||
|
||||
|
||||
+2
-3
@@ -114,8 +114,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
@@ -757,7 +757,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
@Transactional
|
||||
@Override
|
||||
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
if (loan.isChargedOff()) {
|
||||
log.warn("Adding charge to Loan: {} is not allowed. Loan Account is Charged-off", loanId);
|
||||
@@ -844,7 +843,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
|
||||
loan.updateLoanSummaryAndStatus();
|
||||
|
||||
+3
-3
@@ -175,6 +175,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
@@ -1111,9 +1112,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (isTransactionChronologicallyLatest && (!reprocess || !loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled())
|
||||
&& !loan.isForeclosure()) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(newInterestPaymentWaiverTransaction,
|
||||
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
reprocess = false;
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
||||
|
||||
+3
-3
@@ -50,6 +50,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
@@ -87,9 +88,8 @@ public class LoanReAgingServiceImpl {
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
|
||||
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction, new TransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
persistNote(loan, command, changes);
|
||||
|
||||
+3
-3
@@ -47,6 +47,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepositor
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -75,9 +76,8 @@ public class LoanReAmortizationServiceImpl {
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction,
|
||||
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction, new TransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
|
||||
|
||||
|
||||
+3
-4
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.starter;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||
@@ -32,6 +31,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
@@ -104,9 +104,8 @@ public class LoanAccountAutoStarter {
|
||||
|
||||
@Bean
|
||||
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
||||
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(
|
||||
LoanReAgingParameterRepository reAgingParameterRepository) {
|
||||
return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
|
||||
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator) {
|
||||
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class DefaultScheduledDateGeneratorTest {
|
||||
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null);
|
||||
|
||||
// when
|
||||
List<PreGeneratedLoanSchedulePeriod> result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms,
|
||||
List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms,
|
||||
holidayDetailDTO);
|
||||
|
||||
// then
|
||||
|
||||
+172
-73
@@ -4257,79 +4257,6 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
});
|
||||
}
|
||||
|
||||
// UC139c: Advanced payment allocation, with DownPayment enabled in Loan Product and change the auto downpayment and
|
||||
// percentage amount
|
||||
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||
// 1. Create a Loan product with Adv. Pment. Alloc. and DownPayment
|
||||
// 2. Submit Loan with DownPayment and approve
|
||||
// 3. Modify Loan Application change the auto downpayment and percentage amount
|
||||
// 4. Disburse only 10
|
||||
// 5. Check the repayment schedule is correct, without DownPayment installment
|
||||
@Test
|
||||
public void uc139c() {
|
||||
final String processDate = "23 March 2024";
|
||||
final Double amount = 1000.0;
|
||||
runAt(processDate, () -> {
|
||||
BigDecimal percentageAmount = BigDecimal.valueOf(25);
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.installmentAmountInMultiplesOf(null).numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true)
|
||||
.enableAutoRepaymentForDownPayment(true).disbursedAmountPercentageForDownPayment(percentageAmount);
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(client.getClientId(), loanProductResponse.getResourceId(), processDate,
|
||||
amount, 4);
|
||||
|
||||
applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45)
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15)
|
||||
.enableDownPayment(true);
|
||||
|
||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
assertNotNull(loanDetails);
|
||||
assertEquals(true, loanDetails.getEnableDownPayment());
|
||||
assertEquals(true, loanDetails.getEnableAutoRepaymentForDownPayment());
|
||||
|
||||
// verify schedule (with DownPayment and AutoDownPayment to 25%)
|
||||
verifyRepaymentSchedule(loanResponse.getLoanId(), //
|
||||
installment(1000, null, "23 March 2024"), //
|
||||
installment(250.0, 0, 0, 0, 250.0, false, "23 March 2024", 750.0), //
|
||||
installment(250.0, 0, 0, 0, 250.0, false, "07 April 2024", 500.0), //
|
||||
installment(250.0, 0, 0, 0, 250.0, false, "22 April 2024", 250.0), //
|
||||
installment(250.0, 0, 0, 0, 250.0, false, "07 May 2024", 0.0) //
|
||||
);
|
||||
|
||||
percentageAmount = BigDecimal.valueOf(15);
|
||||
loanTransactionHelper.modifyApplicationForLoan(loanResponse.getLoanId(), "modify",
|
||||
new PutLoansLoanIdRequest().clientId(client.getClientId()).productId(loanProductResponse.getResourceId())
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||
.repaymentEvery(15).interestCalculationPeriodType(1).interestType(0).expectedDisbursementDate(processDate)
|
||||
.principal(amount.longValue()).repaymentFrequencyType(0).numberOfRepayments(3).loanTermFrequency(45)
|
||||
.loanTermFrequencyType(0).enableDownPayment(true).loanType("individual").dateFormat(DATETIME_PATTERN)
|
||||
.locale("en").enableAutoRepaymentForDownPayment(true)
|
||||
.disbursedAmountPercentageForDownPayment(percentageAmount));
|
||||
|
||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
|
||||
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(processDate).locale("en"));
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(processDate)
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
assertNotNull(loanDetails);
|
||||
assertEquals(true, loanDetails.getEnableDownPayment());
|
||||
assertEquals(true, loanDetails.getEnableAutoRepaymentForDownPayment());
|
||||
|
||||
// verify schedule (with DownPayment and AutoDownPayment to 15%)
|
||||
verifyRepaymentSchedule(loanResponse.getLoanId(), //
|
||||
installment(1000, null, "23 March 2024"), //
|
||||
installment(150, 0, 0, 0, 0.0, true, "23 March 2024", 850), //
|
||||
installment(283.33, 0, 0, 0, 283.33, false, "07 April 2024", 566.67), //
|
||||
installment(283.33, 0, 0, 0, 283.33, false, "22 April 2024", 283.34), //
|
||||
installment(283.34, 0, 0, 0, 283.34, false, "07 May 2024", 0.0) //
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// uc140: Negative Test: Advanced payment allocation, with DownPayment disabled in Loan Product but try to enable in
|
||||
// Loan application
|
||||
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||
@@ -4996,6 +4923,178 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
});
|
||||
}
|
||||
|
||||
// UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not
|
||||
// better than original, use original EMI and apply with multi disbursement
|
||||
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||
// 1. Create a Loan product with Adv. Payment. Alloc. and with 5% Interest, 360/30, 1 repayment per month
|
||||
// 2. Submit Loan and approve
|
||||
// 3. Disburse
|
||||
// 4. Validate Repayment Schedule
|
||||
@Test
|
||||
public void uc147a() {
|
||||
final String operationDate = "1 January 2024";
|
||||
runAt(operationDate, () -> {
|
||||
BigDecimal interestRatePerPeriod = BigDecimal.valueOf(9.4822);
|
||||
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(false)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.enableAutoRepaymentForDownPayment(null)//
|
||||
.multiDisburseLoan(true)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType("flat")//
|
||||
.overAppliedNumber(10000)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.installmentAmountInMultiplesOf(null)//
|
||||
;//
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6);
|
||||
|
||||
applicationRequest = applicationRequest.numberOfRepayments(6)//
|
||||
.loanTermFrequency(6)//
|
||||
.loanTermFrequencyType(2)//
|
||||
.interestRatePerPeriod(interestRatePerPeriod).interestCalculationPeriodType(DAYS)//
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2)//
|
||||
.maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))//
|
||||
;//
|
||||
|
||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
|
||||
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
validateLoanSummaryBalances(loanDetails, 102.78, 0.0, 100.0, 0.0, null);
|
||||
|
||||
String disbursementDate2nd = "8 January 2024";
|
||||
updateBusinessDate(disbursementDate2nd);
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||
new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDate2nd).dateFormat(DATETIME_PATTERN)
|
||||
.transactionAmount(BigDecimal.valueOf(200.0)).locale("en"));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
validateLoanSummaryBalances(loanDetails, 307.99, 0.0, 300.0, 0.0, null);
|
||||
|
||||
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 49.31, 0.0, 49.31, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.02, 0.0,
|
||||
2.02, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 49.35, 0.0, 49.35, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.98, 0.0,
|
||||
1.98, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 49.74, 0.0, 49.74, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.59, 0.0,
|
||||
1.59, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 50.13, 0.0, 50.13, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.20, 0.0,
|
||||
1.20, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 50.53, 0.0, 50.53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.80, 0.0,
|
||||
0.80, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 50.94, 0.0, 50.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.40, 0.0,
|
||||
0.40, 0.0, 0.0);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertEquals(loanDetails.getNumberOfRepayments(), 6);
|
||||
});
|
||||
}
|
||||
|
||||
// UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not
|
||||
// better than original, use original EMI and apply with multi disbursement in two different periods
|
||||
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||
// 1. Create a Loan product with Adv. Payment. Alloc. and with 5% Interest, 360/30, 1 repayment per month
|
||||
// 2. Submit Loan and approve
|
||||
// 3. Disburse
|
||||
// 4. Validate Repayment Schedule
|
||||
@Test
|
||||
public void uc147b() {
|
||||
final String operationDate = "1 January 2024";
|
||||
runAt(operationDate, () -> {
|
||||
BigDecimal interestRatePerPeriod = BigDecimal.valueOf(9.4822);
|
||||
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(false)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.enableAutoRepaymentForDownPayment(null)//
|
||||
.multiDisburseLoan(true)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType("flat")//
|
||||
.overAppliedNumber(10000)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.installmentAmountInMultiplesOf(null)//
|
||||
;//
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 5);
|
||||
|
||||
applicationRequest = applicationRequest.numberOfRepayments(6)//
|
||||
.loanTermFrequency(6)//
|
||||
.loanTermFrequencyType(2)//
|
||||
.interestRatePerPeriod(interestRatePerPeriod).interestCalculationPeriodType(DAYS)//
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2)//
|
||||
.maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))//
|
||||
;//
|
||||
|
||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
|
||||
.approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
validateLoanSummaryBalances(loanDetails, 102.78, 0.0, 100.0, 0.0, null);
|
||||
|
||||
final String repaymentDate = "1 February 2024";
|
||||
updateBusinessDate(repaymentDate);
|
||||
loanTransactionHelper.makeRepayment(repaymentDate, 17.13f, loanResponse.getLoanId().intValue());
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
|
||||
validateLoanSummaryBalances(loanDetails, 85.65, 17.13, 83.66, 16.34, null);
|
||||
|
||||
final String dateOf2ndDisbursement = "15 February 2024";
|
||||
updateBusinessDate(dateOf2ndDisbursement);
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||
new PostLoansLoanIdRequest().actualDisbursementDate(dateOf2ndDisbursement).dateFormat(DATETIME_PATTERN)
|
||||
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||
validateLoanSummaryBalances(loanDetails, 187.65, 17.13, 183.66, 16.34, null);
|
||||
|
||||
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.34, 16.34, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.79, 0.79,
|
||||
0.0, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 36.46, 0.0, 36.46, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.07, 0.0,
|
||||
1.07, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 36.37, 0.0, 36.37, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.16, 0.0,
|
||||
1.16, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 36.65, 0.0, 36.65, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.88, 0.0,
|
||||
0.88, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 36.94, 0.0, 36.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.59, 0.0,
|
||||
0.59, 0.0, 0.0);
|
||||
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 37.24, 0.0, 37.24, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.29, 0.0,
|
||||
0.29, 0.0, 0.0);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertEquals(loanDetails.getNumberOfRepayments(), 6);
|
||||
});
|
||||
}
|
||||
|
||||
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
|
||||
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
|
||||
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
|
||||
|
||||
Reference in New Issue
Block a user