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) {


Reply via email to