This is an automated email from the ASF dual-hosted git repository. arnold pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit cb82af119b3005a2f3f7cb26cd1d8b3c674454ce Author: CsengeSóti <[email protected]> AuthorDate: Sat Jun 14 01:46:09 2025 +0200 FINERACT-2181: fix prepayment functionality when prepayment happens in the first month of loan --- .../loanaccount/data/ScheduleGeneratorDTO.java | 10 +- .../AbstractCumulativeLoanScheduleGenerator.java | 3 +- .../loanschedule/domain/LoanScheduleGenerator.java | 4 + .../domain/ProgressiveLoanScheduleGenerator.java | 8 + .../domain/LoanAccountDomainServiceJpa.java | 17 ++- .../LoanTransactionProcessingServiceImpl.java | 2 +- .../loanaccount/service/LoanUtilService.java | 10 +- .../loan/repayment/LoanRepaymentTest.java | 167 +++++++++++++++++++++ 8 files changed, 213 insertions(+), 8 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java index 6d85063bc9..3bea4792bf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java @@ -35,6 +35,7 @@ public class ScheduleGeneratorDTO { final CalendarInstance calendarInstanceForInterestRecalculation; final CalendarInstance compoundingCalendarInstance; LocalDate recalculateFrom; + LocalDate recalculateTill; final Long overdurPenaltyWaitPeriod; final FloatingRateDTO floatingRateDTO; final Calendar calendar; @@ -50,8 +51,8 @@ public class ScheduleGeneratorDTO { public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final CurrencyData currency, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, final CalendarInstance calendarInstanceForInterestRecalculation, final CalendarInstance compoundingCalendarInstance, - final LocalDate recalculateFrom, final Long overdurPenaltyWaitPeriod, final FloatingRateDTO floatingRateDTO, - final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, + final LocalDate recalculateFrom, final LocalDate recalculateTill, final Long overdurPenaltyWaitPeriod, + final FloatingRateDTO floatingRateDTO, final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays, final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, @@ -63,6 +64,7 @@ public class ScheduleGeneratorDTO { this.calendarInstanceForInterestRecalculation = calendarInstanceForInterestRecalculation; this.compoundingCalendarInstance = compoundingCalendarInstance; this.recalculateFrom = recalculateFrom; + this.recalculateTill = recalculateTill; this.overdurPenaltyWaitPeriod = overdurPenaltyWaitPeriod; this.holidayDetailDTO = holidayDetailDTO; this.floatingRateDTO = floatingRateDTO; @@ -97,6 +99,10 @@ public class ScheduleGeneratorDTO { return this.recalculateFrom; } + public LocalDate getRecalculateTill() { + return this.recalculateTill; + } + public Long getOverdurPenaltyWaitPeriod() { return this.overdurPenaltyWaitPeriod; } 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 f92112531d..7ebfb54db9 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 @@ -2186,7 +2186,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch } - private LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan, + @Override + public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan, final HolidayDetailDTO holidayDetailDTO, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom, final LocalDate scheduleTillDate) { 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 33666a7976..abe62c20e2 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 @@ -41,6 +41,10 @@ public interface LoanScheduleGenerator { HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom); + LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom, LocalDate rescheduleTill); + 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 5e3f85ffd9..4ce430ccf8 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 @@ -175,6 +175,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { return LoanScheduleDTO.from(null, model); } + @Override + public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom, LocalDate rescheduleTill) { + return rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, loanRepaymentScheduleTransactionProcessor, + rescheduleFrom); + } + @Override public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 8cf7b662e4..e3e25ce8cc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -242,11 +242,24 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } LocalDate recalculateFrom = null; - if (loan.isInterestBearingAndInterestRecalculationEnabled()) { + if (loan.isInterestBearingAndInterestRecalculationEnabled() && loan.getLoanProduct().getProductInterestRecalculationDetails() + .getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) { recalculateFrom = transactionDate; } + final ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, + transactionDate, holidayDetailDto); + + Money outstanding = loanTransactionProcessingService.fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay, transactionDate, loan) + .getTotalOutstanding(); + LocalDate recalculateTill = null; + if (repaymentAmount.isGreaterThanOrEqualTo(outstanding) && loan.isInterestBearingAndInterestRecalculationEnabled() + && loan.getLoanProduct().getProductInterestRecalculationDetails().getPreCloseInterestCalculationStrategy() + .calculateTillPreClosureDateEnabled()) { + recalculateTill = transactionDate; + } + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, - holidayDetailDto); + recalculateTill, holidayDetailDto); if (!isHolidayValidationDone) { final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java index d9d53a6ca7..a8a358a3f8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java @@ -200,7 +200,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc loan.getTransactionProcessingStrategyCode()); return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loan, generatorDTO.getHolidayDetailDTO(), - loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom()); + loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom(), generatorDTO.getRecalculateTill()); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java index 123d717987..d924132b43 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java @@ -76,10 +76,16 @@ public class LoanUtilService { public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom) { final HolidayDetailDTO holidayDetailDTO = null; - return buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDTO); + return buildScheduleGeneratorDTO(loan, recalculateFrom, null, holidayDetailDTO); } public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom, + final LocalDate rescheduleTill) { + final HolidayDetailDTO holidayDetailDTO = null; + return buildScheduleGeneratorDTO(loan, recalculateFrom, rescheduleTill, holidayDetailDTO); + } + + public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom, final LocalDate recalculateTill, final HolidayDetailDTO holidayDetailDTO) { HolidayDetailDTO holidayDetails = holidayDetailDTO; if (holidayDetailDTO == null) { @@ -133,7 +139,7 @@ public class LoanUtilService { ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency.toData(), calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, - overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, + recalculateTill, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday, isInterestToBeRecoveredFirstWhenGreaterThanEMI, isPrincipalCompoundingDisabledForOverdueLoans); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/repayment/LoanRepaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/repayment/LoanRepaymentTest.java index ea4d852b1b..3bcde40ee5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/repayment/LoanRepaymentTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/repayment/LoanRepaymentTest.java @@ -134,4 +134,171 @@ public class LoanRepaymentTest extends BaseLoanIntegrationTest { ); }); } + + @Test + public void test_LoanRepaymentWorks_WhenOnlyOneInstallment_AndAccrualAccounting_AndMonthlyRecalculateInterest_AndMonthlyInterestCalculationPeriod_AllowPartial() { + + runAt("31 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 1; + int repaymentEvery = 1; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) // + .interestType(InterestType.DECLINING_BALANCE)// + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)// + .interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)// + .isInterestRecalculationEnabled(true)// + .recalculationRestFrequencyInterval(1)// + .recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)// + .allowPartialPeriodInterestCalcualtion(true)// + .preClosureInterestCalculationStrategy(1).disallowExpectedDisbursements(false)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedNumber(null)// + .overAppliedCalculationType(null)// + .interestRatePerPeriod(10.0)// + .multiDisburseLoan(null);// + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1000.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .interestType(InterestType.DECLINING_BALANCE)// + .interestRatePerPeriod(BigDecimal.valueOf(10.0))// + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD);// + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2023")// + ); + + verifyPrepayAmountByRepayment(loanId, "15 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2023"), // + transaction(1045.16, "Repayment", "15 January 2023"), // + transaction(45.16, "Accrual", "15 January 2023") // + ); + + // verify journal entries + verifyJournalEntries(loanId, + + journalEntry(1000.0, loansReceivableAccount, "DEBIT"), journalEntry(1000.0, fundSource, "CREDIT"), + journalEntry(1045.16, fundSource, "DEBIT"), journalEntry(45.16, interestReceivableAccount, "CREDIT"), + journalEntry(45.16, interestReceivableAccount, "DEBIT"), journalEntry(45.16, interestIncomeAccount, "CREDIT"), + journalEntry(1000.0, fundSource, "CREDIT") + + ); + }); + } + + @Test + public void test_LoanRepaymentWorks_WhenOnlyOneInstallment_AndAccrualAccounting_AndDailyRecalculateInterest_AndMonthlyInterestCalculationPeriod_NotAllowPartial() { + + runAt("31 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 1; + int repaymentEvery = 1; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) // + .interestType(InterestType.DECLINING_BALANCE)// + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)// + .interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)// + .isInterestRecalculationEnabled(true)// + .recalculationRestFrequencyInterval(1)// + .recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)// + .allowPartialPeriodInterestCalcualtion(true)// + .preClosureInterestCalculationStrategy(2).disallowExpectedDisbursements(false)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedNumber(null)// + .overAppliedCalculationType(null)// + .interestRatePerPeriod(10.0)// + .multiDisburseLoan(null);// + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1000.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .interestType(InterestType.DECLINING_BALANCE)// + .interestRatePerPeriod(BigDecimal.valueOf(10.0))// + .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD);// + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2023")// + ); + + verifyPrepayAmountByRepayment(loanId, "15 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2023"), // + transaction(1100.0, "Repayment", "15 January 2023"), // + transaction(100.0, "Accrual", "15 January 2023") // + ); + + // verify journal entries + verifyJournalEntries(loanId, + + journalEntry(1000.0, loansReceivableAccount, "DEBIT"), journalEntry(1000.0, fundSource, "CREDIT"), + journalEntry(1100.0, fundSource, "DEBIT"), journalEntry(100.0, interestReceivableAccount, "CREDIT"), + journalEntry(100.0, interestReceivableAccount, "DEBIT"), journalEntry(100.0, interestIncomeAccount, "CREDIT"), + journalEntry(1000.0, fundSource, "CREDIT") + + ); + }); + } + }
