You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2024/08/07 21:04:34 UTC
(fineract) branch develop updated: FINERACT-1981: Repayment Schedule Interest Model with multi disbursement support, AdvPymnTxProcessor fix
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 69dd19c74 FINERACT-1981: Repayment Schedule Interest Model with multi disbursement support, AdvPymnTxProcessor fix
69dd19c74 is described below
commit 69dd19c74b374136d82ee9049c4cc7fb17b4388c
Author: Janos Meszaros <ja...@outlook.hu>
AuthorDate: Wed Jul 31 14:34:05 2024 +0200
FINERACT-1981: Repayment Schedule Interest Model with multi disbursement support, AdvPymnTxProcessor fix
---
.../src/test/resources/features/Loan.feature | 8 +-
.../resources/features/LoanDownPayment.feature | 4 +-
.../portfolio/loanaccount/domain/Loan.java | 18 +-
.../domain/LoanRepaymentScheduleInstallment.java | 21 +-
...tLoanRepaymentScheduleTransactionProcessor.java | 7 +-
.../LoanRepaymentScheduleTransactionProcessor.java | 18 -
.../transactionprocessor/TransactionCtx.java | 26 +-
.../AbstractCumulativeLoanScheduleGenerator.java | 5 +-
.../LoanScheduleModelDisbursementPeriod.java | 7 -
.../domain/LoanScheduleModelRepaymentPeriod.java | 12 +-
.../domain/PreGeneratedLoanSchedulePeriod.java | 96 ---
.../domain/ScheduledDateGenerator.java | 2 +-
...dvancedPaymentScheduleTransactionProcessor.java | 261 ++++---
.../impl/ProgressiveTransactionCtx.java | 45 ++
...ressiveLoanInterestRepaymentInterestPeriod.java | 52 ++
.../ProgressiveLoanInterestRepaymentModel.java | 82 ++
.../ProgressiveLoanInterestScheduleModel.java} | 22 +-
.../AbstractProgressiveLoanScheduleGenerator.java | 620 ---------------
.../domain/ProgressiveLoanScheduleGenerator.java | 344 ++++++++-
.../portfolio/loanproduct/calc/EMICalculator.java | 28 +-
.../loanproduct/calc/ProgressiveEMICalculator.java | 527 ++++++++-----
.../loanproduct/calc/RepaymentPeriodModel.java | 24 -
...rogressiveLoanInterestRepaymentModelMapper.java | 37 +
...cedPaymentScheduleTransactionProcessorTest.java | 20 +-
.../calc/ProgressiveEMICalculatorTest.java | 845 ++++++++++++---------
.../ApplyChargeToOverdueLoansBusinessStep.java | 1 -
.../domain/DefaultScheduledDateGenerator.java | 30 +-
.../LoanChargeWritePlatformServiceImpl.java | 5 +-
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 6 +-
.../service/reaging/LoanReAgingServiceImpl.java | 6 +-
.../LoanReAmortizationServiceImpl.java | 6 +-
.../starter/LoanAccountAutoStarter.java | 7 +-
.../domain/DefaultScheduledDateGeneratorTest.java | 2 +-
...PaymentAllocationLoanRepaymentScheduleTest.java | 245 ++++--
34 files changed, 1844 insertions(+), 1595 deletions(-)
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
index 60555c9b0..07e2df118 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
@@ -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 |
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature
index c66222764..a2f316a2b 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanDownPayment.feature
@@ -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 |
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index e6c59a5d7..17c7281e6 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -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)) {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
index 97c2ea015..7c072bf1b 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
@@ -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() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index 742f2bb51..e5978412e 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -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
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
index 7e862ebe1..f49635f62 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java
@@ -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();
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/TransactionCtx.java
similarity index 50%
copy from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java
copy to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/TransactionCtx.java
index 14d224921..bda760734 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/TransactionCtx.java
@@ -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;
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
index 7e46ce5cd..c4d4b8847 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java
@@ -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,
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java
index 3f9894ea9..e5e31e91d 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java
@@ -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);
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java
index a3db61f20..fd848751e 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java
@@ -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;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
deleted file mode 100644
index 636cf0046..000000000
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
index 88271f95c..fd5e824e8 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
@@ -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);
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 0dfbfb7a8..a9b5773b9 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -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 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();
+ LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
+ Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
+ Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency());
+ if (loanProductRelatedDetail.isEnableDownPayment()) {
+ 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(transactionCtx.getCurrency(), downPaymentAmt);
+ downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), 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 updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
+ private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
disbursementTransaction.resetDerivedComponents();
final MathContext mc = MoneyHelper.getMathContext();
- List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
+ 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(currency);
+ Money downPaymentAmount = Money.zero(transactionCtx.getCurrency());
if (loanProductRelatedDetail.isEnableDownPayment()) {
- LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
- .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
+ 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(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);
if (amortizableAmount.isGreaterThanZero()) {
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
- if (installmentAmountInMultiplesOf != null) {
- increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf);
- }
-
- Money remainingAmount = amortizableAmount
- .minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()));
+ MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
- 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);
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
new file mode 100644
index 000000000..c1348f004
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java
@@ -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;
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java
new file mode 100644
index 000000000..2a5a04ecb
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java
@@ -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);
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java
new file mode 100644
index 000000000..f39f808f3
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java
@@ -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);
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java
similarity index 50%
rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java
rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java
index 14d224921..6755f230d 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentScheduleModel.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java
@@ -16,18 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanproduct.calc;
+package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
-import java.util.ArrayList;
+import java.math.MathContext;
+import java.time.temporal.ChronoUnit;
import java.util.List;
-import lombok.Getter;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
-@Getter
-public class RepaymentScheduleModel {
+public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments,
+ LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
- List<RepaymentPeriodModel> scheduleList = new ArrayList<>();
-
- public void addRepaymentPeriodModel(final RepaymentPeriodModel repaymentPeriodModel) {
- scheduleList.add(repaymentPeriodModel);
+ 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()));
}
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
deleted file mode 100644
index 54761f4ea..000000000
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
index 4bafc7561..a53e0b4b7 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
@@ -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;
+ }
+
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
index c59e1ef07..f21fe4def 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
@@ -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);
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
index 564cdfe2e..abdf61a7e 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -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();
+ }
+
/**
- * Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
+ * Add disbursement to Interest Period
*/
@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());
+ 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);
- final List<BigDecimal> rateFactorList = getRateFactorList(expectedRepaymentPeriods, nominalInterestRatePerPeriod, daysInYearType,
- daysInMonthType, repaymentFrequency, repaymentEvery, mc);
+ repaymentPeriod.getInterestPeriods().add(interestPeriod);
+ Collections.sort(repaymentPeriod.getInterestPeriods());
+ }
+
+ @Override
+ public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel) {
+ return makeScheduleModelDeepCopy(scheduleModel, scheduleModel.loanProductRelatedDetail(),
+ scheduleModel.installmentAmountInMultiplesOf(), scheduleModel.mc());
+ }
- return calculateEMI(rateFactorList, actualPeriodNumber, numberOfRepayments, outstandingBalanceAsPerRest, 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
+ */
+ 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);
+ }
+
+ private static List<ProgressiveLoanInterestRepaymentModel> getRelatedRepaymentPeriods(LocalDate calculateFromRepaymentPeriodDueDate,
+ ProgressiveLoanInterestScheduleModel scheduleModel) {
+ return calculateFromRepaymentPeriodDueDate == null ? scheduleModel.repayments()
+ : scheduleModel.repayments().stream().filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))
+ .toList();
+ }
+
+ 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));
-
- final Money emiValue = Money.of(outstandingBalanceForRest.getCurrency(),
- calculateEMIValue(rateFactorN, outstandingBalanceForRest.getAmount(), fnResult, mc));
- final List<BigDecimal> rateFactorMinus1List = getRateFactorMinus1List(rateFactorList, mc);
-
- final Money adjustedEqualMonthlyInstallmentValue = adjustEMIForMoreStreamlinedRepaymentSchedule(actualPeriodNumber,
- numberOfRepayments, outstandingBalanceForRest, emiValue, rateFactorMinus1List, mc);
-
- return new EMICalculationResult(adjustedEqualMonthlyInstallmentValue, rateFactorMinus1List);
- }
-
- /**
- * 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;
- if (numberOfUpcomingPeriods < 2) {
- return equalMonthlyInstallmentValue;
+ 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 equalMonthlyInstallment = Money.of(outstandingBalance.getCurrency(),
+ calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc));
+ final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
+
+ repaymentPeriods.forEach(period -> period.setEqualMonthlyInstallment(finalEqualMonthlyInstallment));
+ }
- 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;
- }
+ 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()));
}
- 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;
+ Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
+ final Money equalMonthlyInstallment) {
+ return scheduleModel.installmentAmountInMultiplesOf() != null
+ ? Money.roundToMultiplesOf(equalMonthlyInstallment, scheduleModel.installmentAmountInMultiplesOf())
+ : equalMonthlyInstallment;
}
- /**
- * 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();
+ Money getDifferenceBetweenLastTwoPeriod(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
+ final ProgressiveLoanInterestScheduleModel scheduleModel) {
+ int numberOfUpcomingPeriods = repaymentPeriods.size();
+ if (numberOfUpcomingPeriods < 2) {
+ return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
+ }
+ 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()));
- }
-
- 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);
+ 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 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);
+ }
+
+ final Money cumulatedInterest = repaymentPeriod.getInterestDue();
+ final Money calculatedPrincipal = isLastPeriod ? outstandingBalance
+ : repaymentPeriod.getEqualMonthlyInstallment().minus(cumulatedInterest);
+
+ if (isLastPeriod) {
+ repaymentPeriod.setEqualMonthlyInstallment(calculatedPrincipal.add(cumulatedInterest));
}
- return calculatedPrincipal;
+
+ 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);
}
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentPeriodModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentPeriodModel.java
deleted file mode 100644
index de99d69c0..000000000
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/RepaymentPeriodModel.java
+++ /dev/null
@@ -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) {
-}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java
new file mode 100644
index 000000000..a234197a3
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java
@@ -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()));
+ }
+}
diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
index 551ae9cbd..96afc9934 100644
--- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
+++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
@@ -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
diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
index aebfdcf88..a837ef533 100644
--- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
+++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -33,10 +33,8 @@ 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.domain.LoanRepaymentScheduleInstallment;
-import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
-import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
-import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
-import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
+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.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
@@ -50,7 +48,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ProgressiveEMICalculatorTest {
- private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
+ private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
@@ -82,7 +80,7 @@ class ProgressiveEMICalculatorTest {
final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()));
final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate()));
final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate()));
- return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod,
+ return emiCalculator.rateFactorMinus1ByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod,
daysInPeriod, MoneyHelper.getMathContext());
}
@@ -91,12 +89,13 @@ class ProgressiveEMICalculatorTest {
// Given
final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+ final MathContext mc = MoneyHelper.getMathContext();
final String[] expectedValues = new String[] { "1.00805337534", "1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534",
"1.00779358904" };
// Then
for (LoanRepaymentScheduleInstallment period : periods) {
- BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period);
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc);
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString());
}
}
@@ -106,12 +105,13 @@ class ProgressiveEMICalculatorTest {
// Given
final DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+ final MathContext mc = MoneyHelper.getMathContext();
final String[] expectedValues = new String[] { "1.00803137158", "1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158",
"1.00777229508" };
// Then
for (LoanRepaymentScheduleInstallment period : periods) {
- BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period);
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc);
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString());
}
}
@@ -128,7 +128,7 @@ class ProgressiveEMICalculatorTest {
final List<BigDecimal> fnValuesCalculated = new ArrayList<>();
BigDecimal previousFnValue = BigDecimal.ZERO;
for (LoanRepaymentScheduleInstallment period : periods) {
- BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period);
+ BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc);
final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactor, mc);
fnValuesCalculated.add(currentFnValue);
@@ -143,457 +143,535 @@ class ProgressiveEMICalculatorTest {
}
@Test
- public void testEMICalculation_principal100_dayInYearsActual_daysInMonthActual_repayEvery1Month() {
+ public void testEMICalculator_generateInterestScheduleModel() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
-
- Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
-
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
-
- // 17.13
- Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
-
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00751321858), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.valueOf(0), result.getNextRepaymentPeriodRateFactorMinus1());
-
- result.reset();
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ final Integer installmentAmountInMultiplesOf = null;
+
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+
+ final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator
+ .generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
+
+ Assertions.assertTrue(interestScheduleModel != null);
+ Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null);
+ Assertions.assertTrue(interestScheduleModel.mc() != null);
+ Assertions.assertTrue(interestScheduleModel.installmentAmountInMultiplesOf() == null);
+ Assertions.assertTrue(interestScheduleModel.repayments() != null);
+ Assertions.assertEquals(4, interestScheduleModel.repayments().size());
+ Assertions.assertEquals(121, interestScheduleModel.getLoanTermInDays());
}
@Test
- public void testEMICalculation_principal10000_dayInYearsActual_daysInMonthActual_repayEvery1Month_DisbursementInPreviousYear() {
+ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 12)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(10_000);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
-
- Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
- // 1713.12
- Assertions.assertEquals(BigDecimal.valueOf(1713.12), result.getEqualMonthlyInstallmentValue().getAmount());
-
- Assertions.assertEquals(BigDecimal.valueOf(0.00804485776), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00751321858), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
- }
-
- @Test
- public void testEMICalculation_principal100_dayInYears365_daysInMonthActual_repayEvery1Month() {
- final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
-
- // 17.13
- Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
-
- Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00753380274), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), result.getNextRepaymentPeriodRateFactorMinus1());
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
- result.reset();
- // check reset
- Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), result.getNextRepaymentPeriodRateFactorMinus1());
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66);
+ checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19);
+ checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59);
+ checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86);
+ checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
+ checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
}
@Test
- public void testEMICalculation_principal100_dayInYears360_daysInMonth30_repayEvery1Month() {
+ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
+
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 15)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 15), LocalDate.of(2024, 4, 15)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 15), LocalDate.of(2024, 5, 15)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15)));
+
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
-
- // 17.13
- Assertions.assertEquals(BigDecimal.valueOf(17.13), result.getEqualMonthlyInstallmentValue().getAmount());
-
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
- result.reset();
- // check reset
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66);
+ checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19);
+ checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59);
+ checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86);
+ checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
+ checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
}
+ // @Test
+ // public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month_reschedule() {
+ // final MathContext mc = MoneyHelper.getMathContext();
+ // final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ //
+ // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ // expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 15)));
+ // expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 15), LocalDate.of(2024, 4, 15)));
+ // expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 15), LocalDate.of(2024, 5, 15)));
+ // expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15)));
+ // expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15)));
+ //
+ // final BigDecimal interestRate = new BigDecimal("9.4822");
+ // final Integer installmentAmountInMultiplesOf = null;
+ //
+ // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+ // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+ // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ // Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+ //
+ // final ProgressiveLoanInterestScheduleModel interestSchedule =
+ // emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ // loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
+ //
+ // final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ // emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+ //
+ // checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ // checkPeriod(interestSchedule, 0, 0, 17.18, 0.008053375342, 0.81, 16.37, 83.66);
+ // checkPeriod(interestSchedule, 1, 0, 17.18, 0.011170810959, 0.93, 16.25, 67.38);
+ // checkPeriod(interestSchedule, 2, 0, 17.18, 0.008053375342, 0.54, 16.64, 50.74);
+ // checkPeriod(interestSchedule, 3, 0, 17.18, 0.007793589041, 0.40, 16.78, 33.96);
+ // checkPeriod(interestSchedule, 4, 0, 17.18, 0.008053375342, 0.27, 16.91, 17.05);
+ // checkPeriod(interestSchedule, 5, 0, 17.18, 0.007793589041, 0.13, 17.05, 0.0);
+ // }
+
@Test
- public void testEMICalculation_principal100_dayInYears364_daysInMonthDoesntMatter_repayEvery1Week() {
+ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 1, 8), LocalDate.of(2024, 1, 15)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 22)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 1, 22), LocalDate.of(2024, 1, 29)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 1, 5)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
-
- Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
-
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
-
- // 16.77
- Assertions.assertEquals(BigDecimal.valueOf(16.77), result.getEqualMonthlyInstallmentValue().getAmount());
-
- final BigDecimal fixValue = new BigDecimal("0.0018235");
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
- result.reset();
- // check reset
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- }
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
- @Test
- public void testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery2Week() {
- final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 2, 26)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 2, 26), LocalDate.of(2024, 3, 11)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 3, 11), LocalDate.of(2024, 3, 25)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
-
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
-
- // 16.88
- Assertions.assertEquals(new BigDecimal("16.88"), result.getEqualMonthlyInstallmentValue().getAmount());
-
- final BigDecimal fixValue = new BigDecimal("0.00368752222");
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
-
- result.reset();
- // check reset
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
+
+ Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66);
+ checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19);
+ checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59);
+ checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86);
+ checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
+ checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
+
+ disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(200));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, 300);
+ checkPeriod(interestSchedule, 0, 0, 51.33, 0.001784284946, 0.18, 2.02, 49.31, 250.69);
+ checkPeriod(interestSchedule, 0, 1, 51.33, 0.006117548387, 1.84, 2.02, 49.31, 250.69);
+ checkPeriod(interestSchedule, 1, 0, 51.33, 0.007901833333, 1.98, 49.35, 201.34);
+ checkPeriod(interestSchedule, 2, 0, 51.33, 0.007901833333, 1.59, 49.74, 151.60);
+ checkPeriod(interestSchedule, 3, 0, 51.33, 0.007901833333, 1.20, 50.13, 101.47);
+ checkPeriod(interestSchedule, 4, 0, 51.33, 0.007901833333, 0.80, 50.53, 50.94);
+ checkPeriod(interestSchedule, 5, 0, 51.34, 0.007901833333, 0.40, 50.94, 0.0);
}
@Test
- public void testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
+ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 1, 16), LocalDate.of(2024, 1, 31)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 15)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 2, 15), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31)));
-
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
- Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
- // 16.90
- Assertions.assertEquals(new BigDecimal("16.90"), result.getEqualMonthlyInstallmentValue().getAmount());
-
- final BigDecimal fixValue = new BigDecimal("0.00395091667");
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
-
- result.reset();
- // check reset
- Assertions.assertEquals(fixValue, result.getNextRepaymentPeriodRateFactorMinus1());
+ Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
+
+ Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66);
+ checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19);
+ checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59);
+ checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86);
+ checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0);
+ checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0);
+
+ disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 15), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 1, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66);
+ checkPeriod(interestSchedule, 1, 0, 37.53, 0.003814678161, 0.32, 1.07, 36.46, 147.20);
+ checkPeriod(interestSchedule, 1, 1, 37.53, 0.004087155172, 0.75, 1.07, 36.46, 147.20);
+ checkPeriod(interestSchedule, 2, 0, 37.53, 0.007901833333, 1.16, 36.37, 110.83);
+ checkPeriod(interestSchedule, 3, 0, 37.53, 0.007901833333, 0.88, 36.65, 74.18);
+ checkPeriod(interestSchedule, 4, 0, 37.53, 0.007901833333, 0.59, 36.94, 37.24);
+ checkPeriod(interestSchedule, 5, 0, 37.53, 0.007901833333, 0.29, 37.24, 0.0);
}
@Test
- public void testEMICalculation_Disbursement_DownPayment_Principal100_dayInYears360_daysInMonth30_repayEvery1Month() {
+ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() {
final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
+ repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)),
+ repayment(2, LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)),
+ repayment(3, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 1)),
+ repayment(4, LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 1)),
+ repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 1)),
+ repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 1)));
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final Money principal = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
- final Money downPaymentValue = Money.of(monetaryCurrency, BigDecimal.valueOf(25));
- final Money outstandingBalance = principal.minus(downPaymentValue);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods
- .add(LoanScheduleModelDisbursementPeriod.disbursement(LocalDate.of(2024, 1, 1), principal, BigDecimal.ZERO));
- expectedRepaymentPeriods
- .add(LoanScheduleModelDownPaymentPeriod.downPayment(1, LocalDate.of(2024, 1, 1), downPaymentValue, outstandingBalance));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
-
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 5, mc);
-
- // 15.36
- Assertions.assertEquals(new BigDecimal("15.36"), result.getEqualMonthlyInstallmentValue().getAmount());
-
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
-
- result.reset();
- // check reset
- Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), result.getNextRepaymentPeriodRateFactorMinus1());
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 17.13, 0.008044857759, 0.80, 16.33, 83.67);
+ checkPeriod(interestSchedule, 1, 0, 17.13, 0.008031371585, 0.67, 16.46, 67.21);
+ checkPeriod(interestSchedule, 2, 0, 17.13, 0.007513218579, 0.50, 16.63, 50.58);
+ checkPeriod(interestSchedule, 3, 0, 17.13, 0.008031371585, 0.41, 16.72, 33.86);
+ checkPeriod(interestSchedule, 4, 0, 17.13, 0.007772295082, 0.26, 16.87, 16.99);
+ checkPeriod(interestSchedule, 5, 0, 17.13, 0.008031371585, 0.14, 16.99, 0.0);
}
@Test
- public void testEMICalculation_Principal1000_NoInterest_repayEvery1Month() {
+ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() {
final MathContext mc = MoneyHelper.getMathContext();
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
+ repayment(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
+ repayment(3, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
+ repayment(4, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
+ repayment(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
- final BigDecimal interestRate = BigDecimal.valueOf(0);
- final Money outstandingBalance = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
+ final BigDecimal interestRate = new BigDecimal("0");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
-
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 4, mc);
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- // 250.00
- Assertions.assertEquals(new BigDecimal("250.00"), result.getEqualMonthlyInstallmentValue().getAmount());
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
-
- // no more period, no more interest
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
-
- result.reset();
- // check reset
- Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 250.0, 0.0, 0.0, 250.0, 750.0);
+ checkPeriod(interestSchedule, 1, 0, 250.0, 0.0, 0.0, 250.0, 500.0);
+ checkPeriod(interestSchedule, 2, 0, 250.0, 0.0, 0.0, 250.0, 250.0);
+ checkPeriod(interestSchedule, 3, 0, 250.0, 0.0, 0.0, 250.0, 0.0);
}
@Test
- public void testUnsupportedRepaymentEveryYear() {
+ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
+ repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)),
+ repayment(2, LocalDate.of(2024, 1, 8), LocalDate.of(2024, 1, 15)),
+ repayment(3, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 22)),
+ repayment(4, LocalDate.of(2024, 1, 22), LocalDate.of(2024, 1, 29)),
+ repayment(5, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 5)),
+ repayment(6, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12)));
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.YEARS);
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- try {
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
- Assertions.fail();
- } catch (Exception e) {
- Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
- }
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 16.77, 0.001823500000, 0.18, 16.59, 83.41);
+ checkPeriod(interestSchedule, 1, 0, 16.77, 0.001823500000, 0.15, 16.62, 66.79);
+ checkPeriod(interestSchedule, 2, 0, 16.77, 0.001823500000, 0.12, 16.65, 50.14);
+ checkPeriod(interestSchedule, 3, 0, 16.77, 0.001823500000, 0.09, 16.68, 33.46);
+ checkPeriod(interestSchedule, 4, 0, 16.77, 0.001823500000, 0.06, 16.71, 16.75);
+ checkPeriod(interestSchedule, 5, 0, 16.78, 0.001823500000, 0.03, 16.75, 0.0);
}
@Test
- public void testUnsupportedRepaymentEveryWholeTerm() {
+ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
+ repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)),
+ repayment(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)),
+ repayment(3, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12)));
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
- Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
- Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WHOLE_TERM);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
+ Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- try {
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
- Assertions.fail();
- } catch (Exception e) {
- Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
- }
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 33.57, 0.003647000000, 0.36, 33.21, 66.79);
+ checkPeriod(interestSchedule, 1, 0, 33.57, 0.003647000000, 0.24, 33.33, 33.46);
+ checkPeriod(interestSchedule, 2, 0, 33.58, 0.003647000000, 0.12, 33.46, 0.0);
}
@Test
- public void testInvalidRepaymentEveryValue() {
+ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
final MathContext mc = MoneyHelper.getMathContext();
- final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
+ repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)),
+ repayment(2, LocalDate.of(2024, 1, 16), LocalDate.of(2024, 1, 31)),
+ repayment(3, LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 15)),
+ repayment(4, LocalDate.of(2024, 2, 15), LocalDate.of(2024, 3, 1)),
+ repayment(5, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)),
+ repayment(6, LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31)));
- final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
- final BigDecimal principal = BigDecimal.valueOf(100);
- final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ final BigDecimal interestRate = new BigDecimal("9.4822");
+ final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
- Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.INVALID);
- Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
+ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
+ Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
- expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+ loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
- try {
- final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, loanProductRelatedDetail,
- expectedRepaymentPeriods, 1, 6, mc);
- Assertions.fail();
- } catch (Exception e) {
- Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
- }
+ final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100));
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
+
+ checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount);
+ checkPeriod(interestSchedule, 0, 0, 16.90, 0.003950916667, 0.40, 16.50, 83.50);
+ checkPeriod(interestSchedule, 1, 0, 16.90, 0.003950916667, 0.33, 16.57, 66.93);
+ checkPeriod(interestSchedule, 2, 0, 16.90, 0.003950916667, 0.26, 16.64, 50.29);
+ checkPeriod(interestSchedule, 3, 0, 16.90, 0.003950916667, 0.20, 16.70, 33.59);
+ checkPeriod(interestSchedule, 4, 0, 16.90, 0.003950916667, 0.13, 16.77, 16.82);
+ checkPeriod(interestSchedule, 5, 0, 16.89, 0.003950916667, 0.07, 16.82, 0.0);
+ }
+
+ //
+ // @Test
+ // public void testEMICalculation_Principal1000_NoInterest_repayEvery1Month() {
+ // final MathContext mc = MoneyHelper.getMathContext();
+ //
+ // final BigDecimal interestRate = BigDecimal.valueOf(0);
+ // final Money outstandingBalance = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
+ //
+ // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+ // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+ // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ //
+ // final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+ // expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+ // expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+ // expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+ //
+ // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance,
+ // loanProductRelatedDetail,
+ // expectedRepaymentPeriods, 1, 4, mc);
+ //
+ // // 250.00
+ // Assertions.assertEquals(new BigDecimal("250.00"), result.getEqualMonthlyInstallmentValue().getAmount());
+ //
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ //
+ // // no more period, no more interest
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ //
+ // result.reset();
+ // // check reset
+ // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1());
+ // }
+ //
+ // @Test
+ // public void testUnsupportedRepaymentEveryYear() {
+ // final MathContext mc = MoneyHelper.getMathContext();
+ // final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ //
+ // final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ // final BigDecimal principal = BigDecimal.valueOf(100);
+ // final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ //
+ // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+ // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.YEARS);
+ // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ //
+ // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ //
+ // try {
+ // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance,
+ // loanProductRelatedDetail,
+ // expectedRepaymentPeriods, 1, 6, mc);
+ // Assertions.fail();
+ // } catch (Exception e) {
+ // Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
+ // }
+ // }
+ //
+ // @Test
+ // public void testUnsupportedRepaymentEveryWholeTerm() {
+ // final MathContext mc = MoneyHelper.getMathContext();
+ // final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ //
+ // final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ // final BigDecimal principal = BigDecimal.valueOf(100);
+ // final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ //
+ // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+ // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WHOLE_TERM);
+ // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ //
+ // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ //
+ // try {
+ // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance,
+ // loanProductRelatedDetail,
+ // expectedRepaymentPeriods, 1, 6, mc);
+ // Assertions.fail();
+ // } catch (Exception e) {
+ // Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
+ // }
+ // }
+ //
+ // @Test
+ // public void testInvalidRepaymentEveryValue() {
+ // final MathContext mc = MoneyHelper.getMathContext();
+ // final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new ArrayList<>();
+ //
+ // final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+ // final BigDecimal principal = BigDecimal.valueOf(100);
+ // final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+ //
+ // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+ // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+ // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+ // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.INVALID);
+ // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+ //
+ // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+ //
+ // try {
+ // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance,
+ // loanProductRelatedDetail,
+ // expectedRepaymentPeriods, 1, 6, mc);
+ // Assertions.fail();
+ // } catch (Exception e) {
+ // Assertions.assertInstanceOf(UnsupportedOperationException.class, e);
+ // }
+ // }
+
+ private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
+ final Money zeroAmount = Money.zero(monetaryCurrency);
+ return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
+ zeroAmount, zeroAmount, false);
}
@NotNull
@@ -605,4 +683,41 @@ class ProgressiveEMICalculatorTest {
return period;
}
+
+ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
+ final int interestIdx, final double emiValue, final double rateFactorMinus1, final double interestDue,
+ final double principalDue, final double remaingBalance) {
+ checkPeriod(interestScheduleModel, repaymentIdx, interestIdx, emiValue, rateFactorMinus1, interestDue, interestDue, principalDue,
+ remaingBalance);
+ }
+
+ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
+ final int interestIdx, final double emiValue, final double rateFactorMinus1, final double interestDue,
+ final double interestDueCumulated, final double principalDue, final double remaingBalance) {
+ final var repaymentPeriod = interestScheduleModel.repayments().get(repaymentIdx);
+ final var interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx);
+
+ Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEqualMonthlyInstallment().getAmount()));
+ Assertions.assertEquals(rateFactorMinus1, toDouble(interestPeriod.getRateFactorMinus1()));
+ Assertions.assertEquals(interestDue, toDouble(interestPeriod.getInterestDue().getAmount()));
+ Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getInterestDue().getAmount()));
+ Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getPrincipalDue().getAmount()));
+ Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getRemainingBalance().getAmount()));
+ }
+
+ private static void checkDisbursementOnPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
+ final Money disbursedAmount) {
+ checkDisbursementOnPeriod(interestScheduleModel, repaymentIdx, disbursedAmount.getAmount().doubleValue());
+ }
+
+ private static void checkDisbursementOnPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
+ final double disbursedAmount) {
+ final var repaymentPeriod = interestScheduleModel.repayments().get(repaymentIdx);
+ Assertions.assertEquals(disbursedAmount, toDouble(repaymentPeriod.getDisbursedAmountInPeriod().getAmount()));
+ }
+
+ private static double toDouble(final BigDecimal value) {
+ return value == null ? 0 : value.doubleValue();
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java
index 3b89896bc..86a26eceb 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyChargeToOverdueLoansBusinessStep.java
@@ -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;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
index 83245dfbb..eb077751e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
@@ -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) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index 4f14bd250..12295fd05 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -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();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 823a99201..8e02a793c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -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()) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
index 33909fa7d..5fccb83f9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
@@ -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);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
index 880b85663..1546f6c93 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
@@ -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);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
index 905f4f4bd..47b820906 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java
@@ -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);
}
}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
index 442e4cd33..3dc81175d 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
@@ -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
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
index be43cc313..9f5a5f150 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
@@ -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 ---------------------------------------");