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 7304af57b FINERACT-1981: pay-off transaction for progressive loans
7304af57b is described below
commit 7304af57bc673898fbe5a4a21cc2f184dcee1a88
Author: Kristof Jozsa <[email protected]>
AuthorDate: Wed Aug 14 11:14:48 2024 +0200
FINERACT-1981: pay-off transaction for progressive loans
---
.../loanaccount/data/LoanSummaryData.java | 2 +-
.../loanaccount/data/OutstandingAmountsDTO.java | 65 ++++++++
.../portfolio/loanaccount/domain/Loan.java | 22 +--
.../domain/LoanRepaymentScheduleInstallment.java | 12 +-
.../AbstractCumulativeLoanScheduleGenerator.java | 15 +-
.../loanschedule/domain/LoanScheduleGenerator.java | 6 +-
.../domain/ProgressiveLoanScheduleGenerator.java | 82 +++++++++-
.../ProgressiveLoanScheduleGeneratorTest.java | 176 +++++++++++++++++++++
.../service/LoanScheduleAssembler.java | 4 +-
...LoanScheduleCalculationPlatformServiceImpl.java | 9 +-
.../service/LoanReadPlatformServiceImpl.java | 19 +--
11 files changed, 370 insertions(+), 42 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
index 46567bf17..de2f68cb0 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
@@ -250,7 +250,7 @@ public class LoanSummaryData {
return BigDecimal.ZERO;
}
- private static BigDecimal computeAccruedInterestTillDay(final
LoanSchedulePeriodData period, final long untilDay,
+ public static BigDecimal computeAccruedInterestTillDay(final
LoanSchedulePeriodData period, final long untilDay,
final CurrencyData currency) {
Integer remainingDays = period.getDaysInPeriod();
BigDecimal totalAccruedInterest = BigDecimal.ZERO;
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java
new file mode 100644
index 000000000..aa1351880
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java
@@ -0,0 +1,65 @@
+/**
+ * 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.data;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+
+@Data
+@Accessors(chain = true, fluent = true)
+public class OutstandingAmountsDTO {
+
+ private Money principal;
+ private Money interest;
+ private Money feeCharges;
+ private Money penaltyCharges;
+
+ public OutstandingAmountsDTO(MonetaryCurrency currency) {
+ this.principal = Money.zero(currency);
+ this.interest = Money.zero(currency);
+ this.feeCharges = Money.zero(currency);
+ this.penaltyCharges = Money.zero(currency);
+ }
+
+ public Money getTotalOutstanding() {
+ return principal() //
+ .plus(interest()) //
+ .plus(feeCharges()) //
+ .plus(penaltyCharges());
+ }
+
+ public void plusPrincipal(Money principal) {
+ this.principal = this.principal.plus(principal);
+ }
+
+ public void plusInterest(Money interest) {
+ this.interest = this.interest.plus(interest);
+ }
+
+ public void plusFeeCharges(Money feeCharges) {
+ this.feeCharges = this.feeCharges.plus(feeCharges);
+ }
+
+ public void plusPenaltyCharges(Money penaltyCharges) {
+ this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges);
+ }
+
+}
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 008f9537e..21f69236d 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
@@ -109,6 +109,7 @@ import
org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
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.data.OutstandingAmountsDTO;
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.MoneyHolder;
@@ -4368,8 +4369,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom<Long> {
loanRepaymentScheduleTransactionProcessor,
generatorDTO.getRecalculateFrom());
}
- public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final
ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) {
- LoanRepaymentScheduleInstallment installment;
+ public OutstandingAmountsDTO fetchPrepaymentDetail(final
ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) {
+ OutstandingAmountsDTO outstandingAmounts;
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled())
{
final MathContext mc = MoneyHelper.getMathContext();
@@ -4381,12 +4382,12 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom<Long> {
.create(loanApplicationTerms.getLoanScheduleType(),
interestMethod);
final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
- installment =
loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate,
loanApplicationTerms, mc, this,
+ outstandingAmounts =
loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate,
loanApplicationTerms, mc, this,
scheduleGeneratorDTO.getHolidayDetailDTO(),
loanRepaymentScheduleTransactionProcessor);
} else {
- installment = this.getTotalOutstandingOnLoan();
+ outstandingAmounts = this.getTotalOutstandingOnLoan();
}
- return installment;
+ return outstandingAmounts;
}
public LoanApplicationTerms constructLoanApplicationTerms(final
ScheduleGeneratorDTO scheduleGeneratorDTO) {
@@ -4460,11 +4461,11 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom<Long> {
return annualNominalInterestRate;
}
- private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() {
- Money feeCharges = Money.zero(loanCurrency());
- Money penaltyCharges = Money.zero(loanCurrency());
+ private OutstandingAmountsDTO getTotalOutstandingOnLoan() {
Money totalPrincipal = Money.zero(loanCurrency());
Money totalInterest = Money.zero(loanCurrency());
+ Money feeCharges = Money.zero(loanCurrency());
+ Money penaltyCharges = Money.zero(loanCurrency());
final Set<LoanInterestRecalcualtionAdditionalDetails>
compoundingDetails = null;
List<LoanRepaymentScheduleInstallment> repaymentSchedule =
getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment :
repaymentSchedule) {
@@ -4473,9 +4474,8 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom<Long> {
feeCharges =
feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency()));
penaltyCharges =
penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency()));
}
- LocalDate businessDate = DateUtils.getBusinessLocalDate();
- return new LoanRepaymentScheduleInstallment(null, 0, businessDate,
businessDate, totalPrincipal.getAmount(),
- totalInterest.getAmount(), feeCharges.getAmount(),
penaltyCharges.getAmount(), false, compoundingDetails);
+ return new
OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest)
+ .feeCharges(feeCharges).penaltyCharges(penaltyCharges);
}
public LocalDate fetchInterestRecalculateFromDate() {
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 7c072bf1b..34d8baf6b 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
@@ -583,20 +583,20 @@ public class LoanRepaymentScheduleInstallment extends
AbstractAuditableWithUTCDa
return interestPortionOfTransaction;
}
- public Money payPrincipalComponent(final LocalDate transactionDate, final
Money transactionAmountRemaining) {
+ public Money payPrincipalComponent(final LocalDate transactionDate, final
Money transactionAmount) {
- final MonetaryCurrency currency =
transactionAmountRemaining.getCurrency();
+ final MonetaryCurrency currency = transactionAmount.getCurrency();
Money principalPortionOfTransaction = Money.zero(currency);
- if (transactionAmountRemaining.isZero()) {
+ if (transactionAmount.isZero()) {
return principalPortionOfTransaction;
}
final Money principalDue = getPrincipalOutstanding(currency);
- if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) {
+ if (transactionAmount.isGreaterThanOrEqualTo(principalDue)) {
this.principalCompleted =
getPrincipalCompleted(currency).plus(principalDue).getAmount();
principalPortionOfTransaction =
principalPortionOfTransaction.plus(principalDue);
} else {
- this.principalCompleted =
getPrincipalCompleted(currency).plus(transactionAmountRemaining).getAmount();
- principalPortionOfTransaction =
principalPortionOfTransaction.plus(transactionAmountRemaining);
+ this.principalCompleted =
getPrincipalCompleted(currency).plus(transactionAmount).getAmount();
+ principalPortionOfTransaction =
principalPortionOfTransaction.plus(transactionAmount);
}
this.principalCompleted = defaultToNullIfZero(this.principalCompleted);
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 c4d4b8847..b891ace31 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
@@ -45,6 +45,7 @@ import
org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
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.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
@@ -2775,7 +2776,7 @@ public abstract class
AbstractCumulativeLoanScheduleGenerator implements LoanSch
* Method returns the amount payable to close the loan account as of today.
*/
@Override
- public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final
MonetaryCurrency currency, final LocalDate onDate,
+ public OutstandingAmountsDTO calculatePrepaymentAmount(final
MonetaryCurrency currency, final LocalDate onDate,
final LoanApplicationTerms loanApplicationTerms, final MathContext
mc, Loan loan, final HolidayDetailDTO holidayDetailDTO,
final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor) {
@@ -2790,10 +2791,10 @@ public abstract class
AbstractCumulativeLoanScheduleGenerator implements LoanSch
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanTransactions, currency, loanScheduleDTO.getInstallments(),
loan.getActiveCharges());
- Money feeCharges = Money.zero(currency);
- Money penaltyCharges = Money.zero(currency);
Money totalPrincipal = Money.zero(currency);
Money totalInterest = Money.zero(currency);
+ Money feeCharges = Money.zero(currency);
+ Money penaltyCharges = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment currentInstallment :
loanScheduleDTO.getInstallments()) {
if (currentInstallment.isNotFullyPaidOff()) {
totalPrincipal =
totalPrincipal.plus(currentInstallment.getPrincipalOutstanding(currency));
@@ -2802,8 +2803,10 @@ public abstract class
AbstractCumulativeLoanScheduleGenerator implements LoanSch
penaltyCharges =
penaltyCharges.plus(currentInstallment.getPenaltyChargesOutstanding(currency));
}
}
- final Set<LoanInterestRecalcualtionAdditionalDetails>
compoundingDetails = null;
- return new LoanRepaymentScheduleInstallment(null, 0, onDate, onDate,
totalPrincipal.getAmount(), totalInterest.getAmount(),
- feeCharges.getAmount(), penaltyCharges.getAmount(), false,
compoundingDetails);
+ return new OutstandingAmountsDTO(currency) //
+ .principal(totalPrincipal) //
+ .interest(totalInterest) //
+ .feeCharges(feeCharges) //
+ .penaltyCharges(penaltyCharges);
}
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java
index def9a53b4..9cb841f5f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java
@@ -23,9 +23,9 @@ import java.time.LocalDate;
import java.util.Set;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
+import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
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;
@@ -38,8 +38,8 @@ public interface LoanScheduleGenerator {
HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom);
- LoanRepaymentScheduleInstallment
calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
- LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan
loan, HolidayDetailDTO holidayDetailDTO,
+ OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency,
LocalDate onDate, LoanApplicationTerms loanApplicationTerms,
+ MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor);
}
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 a53e0b4b7..e28da6996 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,22 +18,29 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
+import static java.time.temporal.ChronoUnit.DAYS;
+
import java.math.BigDecimal;
import java.math.MathContext;
+import java.math.RoundingMode;
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 java.util.concurrent.atomic.AtomicBoolean;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
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.monetary.domain.MoneyHelper;
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.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -48,6 +55,7 @@ import
org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.springframework.stereotype.Component;
+@Slf4j
@Component
@RequiredArgsConstructor
public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator
{
@@ -222,10 +230,80 @@ public class ProgressiveLoanScheduleGenerator implements
LoanScheduleGenerator {
}
@Override
- public LoanRepaymentScheduleInstallment
calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
+ public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency
currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan
loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor) {
- return null;
+ return switch
(loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
+ case TILL_PRE_CLOSURE_DATE -> {
+ log.debug("calculating prepayment amount till pre closure date
(Strategy A)");
+ OutstandingAmountsDTO outstandingAmounts = new
OutstandingAmountsDTO(currency);
+ AtomicBoolean firstAfterPayoff = new AtomicBoolean(true);
+ loan.getRepaymentScheduleInstallments().forEach(installment ->
{
+ boolean isInstallmentAfterPayoff =
installment.getDueDate().isAfter(onDate);
+
+
outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
+ if (isInstallmentAfterPayoff) {
+ if (firstAfterPayoff.getAndSet(false)) {
+
outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment,
onDate));
+ } else {
+ log.debug("Installment {} - {} is after payoff,
not counting interest", installment.getFromDate(),
+ installment.getDueDate());
+ }
+ } else {
+ log.debug("adding interest for {} - {}: {}",
installment.getFromDate(), installment.getDueDate(),
+ installment.getInterestOutstanding(currency));
+
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
+ }
+
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
+
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
+ });
+ yield outstandingAmounts;
+ }
+
+ case TILL_REST_FREQUENCY_DATE -> {
+ log.debug("calculating prepayment amount till rest frequency
date (Strategy B)");
+ OutstandingAmountsDTO outstandingAmounts = new
OutstandingAmountsDTO(currency);
+ loan.getRepaymentScheduleInstallments().forEach(installment ->
{
+ boolean isPayoffAfterInstallmentFrom =
installment.getFromDate().isAfter(onDate);
+
+
outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
+ if (!isPayoffAfterInstallmentFrom) {
+
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
+ } else {
+ log.debug("Payoff after installment {}, not counting
interest", installment.getDueDate());
+ }
+
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
+
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
+ });
+
+ yield outstandingAmounts;
+ }
+ case NONE -> throw new UnsupportedOperationException("Pre-closure
interest calculation strategy not supported");
+ };
+ }
+
+ private Money calculatePayableInterest(Loan loan,
LoanRepaymentScheduleInstallment installment, LocalDate onDate) {
+ RoundingMode roundingMode = MoneyHelper.getRoundingMode();
+ MonetaryCurrency currency = loan.getCurrency();
+ Money originalInterest = installment.getInterestCharged(currency);
+ log.debug("calculating interest for {} from {} to {}",
originalInterest, installment.getFromDate(), installment.getDueDate());
+
+ LocalDate start = installment.getFromDate();
+ Money payableInterest = Money.zero(currency);
+
+ while (!start.isEqual(onDate)) {
+ long between = DAYS.between(start, installment.getDueDate());
+ Money dailyInterest =
originalInterest.minus(payableInterest).dividedBy(between, roundingMode);
+ log.debug("Daily interest is {}: {} / {}, total: {}",
dailyInterest, originalInterest.minus(payableInterest), between,
+ payableInterest.add(dailyInterest));
+ payableInterest = payableInterest.add(dailyInterest);
+ start = start.plusDays(1);
+ }
+
+ payableInterest =
payableInterest.minus(installment.getInterestPaid(currency).minus(installment.getInterestWaived(currency)));
+
+ log.debug("Payable interest is {}", payableInterest);
+ return payableInterest;
}
// Private, internal methods
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java
new file mode 100644
index 000000000..9da9156b1
--- /dev/null
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java
@@ -0,0 +1,176 @@
+/**
+ * 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 static java.math.BigDecimal.ZERO;
+import static
org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;
+import static
org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
+import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+import org.springframework.test.util.ReflectionTestUtils;
+
+class ProgressiveLoanScheduleGeneratorTest {
+
+ static class TestRow {
+
+ LocalDate fromDate;
+ LocalDate dueDate;
+ BigDecimal balance;
+ BigDecimal principal;
+ BigDecimal interest;
+ BigDecimal fee;
+ BigDecimal penalty;
+ boolean paid;
+
+ TestRow(LocalDate fromDate, LocalDate dueDate, BigDecimal balance,
BigDecimal principal, BigDecimal interest, BigDecimal fee,
+ BigDecimal penalty, boolean paid) {
+ this.fromDate = fromDate;
+ this.dueDate = dueDate;
+ this.balance = balance;
+ this.principal = principal;
+ this.interest = interest;
+ this.fee = fee;
+ this.penalty = penalty;
+ this.paid = paid;
+ }
+ }
+
+ private ProgressiveLoanScheduleGenerator generator = new
ProgressiveLoanScheduleGenerator(null, null);
+ private MonetaryCurrency usd = new MonetaryCurrency("USD", 2, null);
+ private HolidayDetailDTO holidays = new HolidayDetailDTO(false, null,
null);
+ LoanRepaymentScheduleTransactionProcessor processor =
mock(LoanRepaymentScheduleTransactionProcessor.class);
+
+ static {
+ ConfigurationDomainService domainService =
mock(ConfigurationDomainService.class);
+
when(domainService.getRoundingMode()).thenReturn(RoundingMode.HALF_UP.ordinal());
+ ReflectionTestUtils.setField(MoneyHelper.class,
"staticConfigurationDomainService", domainService);
+ }
+
+ @BeforeAll
+ public static void beforeAll() {
+ ((LoggerContext)
LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(ch.qos.logback.classic.Level.DEBUG);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ ((LoggerContext)
LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(Level.INFO);
+ }
+
+ public List<TestRow> testRows() {
+ return List.of(
+ new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2,
1), BigDecimal.valueOf(83.57), BigDecimal.valueOf(16.43),
+ BigDecimal.valueOf(0.58), ZERO, ZERO, true),
+ new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3,
1), BigDecimal.valueOf(67.05), BigDecimal.valueOf(16.52),
+ BigDecimal.valueOf(0.49), ZERO, ZERO, false),
+ new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4,
1), BigDecimal.valueOf(50.43), BigDecimal.valueOf(16.62),
+ BigDecimal.valueOf(0.39), ZERO, ZERO, false),
+ new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5,
1), BigDecimal.valueOf(33.71), BigDecimal.valueOf(16.72),
+ BigDecimal.valueOf(0.29), ZERO, ZERO, false),
+ new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6,
1), BigDecimal.valueOf(16.90), BigDecimal.valueOf(16.81),
+ BigDecimal.valueOf(0.20), ZERO, ZERO, false),
+ new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7,
1), BigDecimal.valueOf(00.90), BigDecimal.valueOf(16.90),
+ BigDecimal.valueOf(0.10), ZERO, ZERO, false));
+ }
+
+ @Test
+ public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() {
+ LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
+
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE);
+ Loan loan = prepareLoanWithInstallments(testRows());
+
+ OutstandingAmountsDTO amounts =
generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms,
MathContext.DECIMAL32,
+ loan, holidays, processor);
+ assertEquals(BigDecimal.valueOf(83.84),
amounts.getTotalOutstanding().getAmount());
+ }
+
+ @Test
+ public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() {
+ LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
+
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE);
+ Loan loan = prepareLoanWithInstallments(testRows());
+
+ OutstandingAmountsDTO amounts =
generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms,
MathContext.DECIMAL32,
+ loan, holidays, processor);
+ assertEquals(BigDecimal.valueOf(84.06),
amounts.getTotalOutstanding().getAmount());
+ }
+
+ @Test
+ public void calculateSameDayPayoff() {
+ LoanApplicationTerms terms = mock(LoanApplicationTerms.class);
+
when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE);
+
+ Loan loan = prepareLoanWithInstallments(List.of(
+ new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2,
1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
+ BigDecimal.valueOf(2), ZERO, ZERO, false),
+ new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3,
1), BigDecimal.valueOf(102), BigDecimal.valueOf(100),
+ BigDecimal.valueOf(2), ZERO, ZERO, false)));
+
+ OutstandingAmountsDTO amounts =
generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms,
MathContext.DECIMAL32,
+ loan, holidays, processor);
+ assertEquals(BigDecimal.valueOf(200.0).longValue(),
amounts.getTotalOutstanding().getAmount().longValue());
+ }
+
+ @NotNull
+ private Loan prepareLoanWithInstallments(List<TestRow> rows) {
+ Loan loan = mock(Loan.class);
+ List<LoanRepaymentScheduleInstallment> installments =
createInstallments(rows, loan, usd);
+ when(loan.getRepaymentScheduleInstallments()).thenReturn(installments);
+ when(loan.getCurrency()).thenReturn(usd);
+ return loan;
+ }
+
+ private List<LoanRepaymentScheduleInstallment>
createInstallments(List<TestRow> rows, Loan loan, MonetaryCurrency usd) {
+ AtomicInteger count = new AtomicInteger(1);
+ return rows.stream().map(row -> {
+ LoanRepaymentScheduleInstallment installment = new
LoanRepaymentScheduleInstallment(loan, count.incrementAndGet(), row.fromDate,
+ row.dueDate, row.principal, row.interest, row.fee,
row.penalty, true, null, null, row.paid);
+ if (row.paid) {
+ installment.payPrincipalComponent(row.fromDate, Money.of(usd,
row.principal));
+ installment.payInterestComponent(row.fromDate, Money.of(usd,
row.interest));
+ installment.updateObligationMet(true);
+ }
+ return installment;
+ }).toList();
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
index 6e8b22d9c..7ae511925 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
@@ -91,6 +91,7 @@ import
org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
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.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -732,7 +733,7 @@ public class LoanScheduleAssembler {
loanRepaymentScheduleTransactionProcessor,
rescheduleFrom).getLoanScheduleModel();
}
- public LoanRepaymentScheduleInstallment
calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
+ public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency
currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, Loan loan, final Long
officeId,
final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor) {
final LoanScheduleGenerator loanScheduleGenerator =
this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(),
@@ -748,7 +749,6 @@ public class LoanScheduleAssembler {
return loanScheduleGenerator.calculatePrepaymentAmount(currency,
onDate, loanApplicationTerms, mc, loan, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor);
-
}
public void assempleVariableScheduleFrom(final Loan loan, final String
json) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java
index 0f4dd818f..1edb542bc 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java
@@ -31,6 +31,7 @@ import
org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import
org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
+import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
@@ -115,8 +116,12 @@ public class LoanScheduleCalculationPlatformServiceImpl
implements LoanScheduleC
}
}
LoanApplicationTerms loanApplicationTerms =
constructLoanApplicationTerms(loan);
- LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment =
this.loanScheduleAssembler.calculatePrepaymentAmount(currency,
- today, loanApplicationTerms, loan, loan.getOfficeId(),
loanRepaymentScheduleTransactionProcessor);
+ OutstandingAmountsDTO outstandingAmountsDTO =
this.loanScheduleAssembler.calculatePrepaymentAmount(currency, today,
+ loanApplicationTerms, loan, loan.getOfficeId(),
loanRepaymentScheduleTransactionProcessor);
+ LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment =
new LoanRepaymentScheduleInstallment(null, 0, today, today,
+ outstandingAmountsDTO.principal().getAmount(),
outstandingAmountsDTO.interest().getAmount(),
+ outstandingAmountsDTO.feeCharges().getAmount(),
outstandingAmountsDTO.penaltyCharges().getAmount(), false, null);
+
Money totalAmount =
totalPrincipal.plus(loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency))
.plus(loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency));
Money interestDue = Money.zero(currency);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index d662fd3db..cda0e6f40 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -104,6 +104,7 @@ import
org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import
org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData;
+import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
import
org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
@@ -473,20 +474,20 @@ public class LoanReadPlatformServiceImpl implements
LoanReadPlatformService, Loa
final LocalDate earliestUnpaidInstallmentDate =
DateUtils.getBusinessLocalDate();
final LocalDate recalculateFrom = null;
final ScheduleGeneratorDTO scheduleGeneratorDTO =
loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
- final LoanRepaymentScheduleInstallment
loanRepaymentScheduleInstallment =
loan.fetchPrepaymentDetail(scheduleGeneratorDTO, onDate);
+ final OutstandingAmountsDTO outstandingAmounts =
loan.fetchPrepaymentDetail(scheduleGeneratorDTO, onDate);
final LoanTransactionEnumData transactionType =
LoanEnumerations.transactionType(repaymentTransactionType);
final Collection<PaymentTypeData> paymentOptions =
this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
- final BigDecimal outstandingLoanBalance =
loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
+ final BigDecimal outstandingLoanBalance =
outstandingAmounts.principal().getAmount();
final BigDecimal unrecognizedIncomePortion = null;
+
BigDecimal adjustedChargeAmount = adjustPrepayInstallmentCharge(loan,
onDate);
+ BigDecimal totalAdjusted =
outstandingAmounts.getTotalOutstanding().getAmount().subtract(adjustedChargeAmount);
- return new LoanTransactionData(null, null, null, transactionType,
null, currencyData, earliestUnpaidInstallmentDate,
-
loanRepaymentScheduleInstallment.getTotalOutstanding(currency).getAmount().subtract(adjustedChargeAmount),
- loan.getNetDisbursalAmount(),
loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(),
-
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
-
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount().subtract(adjustedChargeAmount),
-
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(),
null, unrecognizedIncomePortion,
- paymentOptions, ExternalId.empty(), null, null,
outstandingLoanBalance, false, loanId, loan.getExternalId());
+ return new LoanTransactionData(null, null, null, transactionType,
null, currencyData, earliestUnpaidInstallmentDate, totalAdjusted,
+ loan.getNetDisbursalAmount(),
outstandingAmounts.principal().getAmount(),
outstandingAmounts.interest().getAmount(),
+
outstandingAmounts.feeCharges().getAmount().subtract(adjustedChargeAmount),
outstandingAmounts.penaltyCharges().getAmount(),
+ null, unrecognizedIncomePortion, paymentOptions,
ExternalId.empty(), null, null, outstandingLoanBalance, false, loanId,
+ loan.getExternalId());
}
private BigDecimal adjustPrepayInstallmentCharge(Loan loan, final
LocalDate onDate) {