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 46567bf17bb..de2f68cb061 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 @@ private static BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualP 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 00000000000..aa1351880b3 --- /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 80fb57c0452..ea7f61b823f 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.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 @@ private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO gener 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 LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGene .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 BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, B 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 compoundingDetails = null; List repaymentSchedule = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) { @@ -4473,9 +4474,11 @@ private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() { 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 7c072bf1b16..34d8baf6b5c 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 Money payInterestComponent(final LocalDate transactionDate, final Money t 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 c4d4b884797..43037ac796a 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.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,9 +2776,9 @@ private LocalDate getNextCompoundScheduleDate(LocalDate startDate, LoanApplicati * Method returns the amount payable to close the loan account as of today. */ @Override - public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final MonetaryCurrency currency, final LocalDate onDate, - final LoanApplicationTerms loanApplicationTerms, final MathContext mc, Loan loan, final HolidayDetailDTO holidayDetailDTO, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency currency, final LocalDate onDate, + final LoanApplicationTerms loanApplicationTerms, final MathContext mc, Loan loan, final HolidayDetailDTO holidayDetailDTO, + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { LocalDate calculateTill = onDate; if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { @@ -2790,10 +2791,10 @@ public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final Monetary 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 LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final Monetary penaltyCharges = penaltyCharges.plus(currentInstallment.getPenaltyChargesOutstanding(currency)); } } - final Set 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 def9a53b460..b4ffd2ac1ae 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,6 +23,7 @@ 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; @@ -38,8 +39,8 @@ LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom); - LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + 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 a53e0b4b79e..2114a1ae7d1 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 @@ -27,16 +27,18 @@ import java.util.List; import java.util.Set; 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.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; 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; 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; @@ -48,6 +50,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { @@ -222,10 +225,50 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio } @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 -> { + // Strategy A + log.info("calculating prepayment amount till pre closure date (Strategy A)"); + OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); + loan.getRepaymentScheduleInstallments().forEach(installment -> { + boolean isPayoffAfterInstallment = installment.getFromDate().isAfter(onDate); + + outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); + if (!isPayoffAfterInstallment) { + outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); + } else { + log.info("Payoff after installment {}, not counting interest", installment.getDueDate()); + } + outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); + outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); + }); + + yield outstandingAmounts; + } + case TILL_REST_FREQUENCY_DATE -> { + // Strategy B + log.info("calculating prepayment amount till rest frequency date (Strategy B)"); + OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); + loan.getRepaymentScheduleInstallments().forEach(installment -> { + boolean isPayoffAfterInstallment = installment.getDueDate().isAfter(onDate); + + outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); + if (!isPayoffAfterInstallment) { + outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); + } else { + log.info("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, 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 00000000000..fabc8383f24 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java @@ -0,0 +1,126 @@ +/** + * 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 java.math.BigDecimal.valueOf; +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 java.math.BigDecimal; +import java.math.MathContext; +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.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +public class ProgressiveLoanScheduleGeneratorTest { + + static class TestRow { + + LocalDate fromDate; + LocalDate dueDate; + BigDecimal balance; + BigDecimal principal; + BigDecimal interest; + BigDecimal fee; + BigDecimal penalty; + boolean paid; + + public 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; + } + } + + List rows = List.of( + new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), valueOf(83.57), valueOf(16.43), valueOf(0.58), ZERO, ZERO, + true), + new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), valueOf(67.05), valueOf(16.52), valueOf(0.49), ZERO, ZERO, + false), + new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), valueOf(50.43), valueOf(16.62), valueOf(0.39), ZERO, ZERO, + false), + new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), valueOf(33.71), valueOf(16.72), valueOf(0.29), ZERO, ZERO, + false), + new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), valueOf(16.90), valueOf(16.81), valueOf(0.20), ZERO, ZERO, + false), + new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), valueOf(00.90), valueOf(16.90), valueOf(0.10), ZERO, ZERO, + false)); + + LocalDate onDate = LocalDate.of(2024, 2, 15); + + 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 { + ReflectionTestUtils.setField(MoneyHelper.class, "staticConfigurationDomainService", mock(ConfigurationDomainService.class)); + } + + @Test + public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() { + LoanApplicationTerms terms = mock(LoanApplicationTerms.class); + when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); + + Loan loan = mock(Loan.class); + List installments = createInstallments(rows, loan, usd); + when(loan.getRepaymentScheduleInstallments()).thenReturn(installments); + + // start calculation + OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, onDate, terms, MathContext.DECIMAL32, loan, holidays, + processor); + System.out.println(amounts); + System.out.println("total outstanding: " + amounts.getTotalOutstanding()); + assertEquals(valueOf(84.06), amounts.getTotalOutstanding().getAmount()); + } + + private List createInstallments(List 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.fromDate, 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 6e8b22d9c3b..7ae511925dd 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.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 LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationT 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 LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurren 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 0f4dd818f60..c57570225b7 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.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,11 @@ public void updateFutureSchedule(LoanScheduleData loanScheduleData, final Long l } } LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(loan); - LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = this.loanScheduleAssembler.calculatePrepaymentAmount(currency, + 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 b30c2e0a453..43ed78e6e8d 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.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; @@ -472,19 +473,21 @@ public LoanTransactionData retrieveLoanPrePaymentTemplate(final LoanTransactionT 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 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, + 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()); }