diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index 87fe5f81ecc..a938039a8cf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -79,6 +79,14 @@ private GetLoansTotal() {} @Schema(example = "200.000000") public Double amount; + @Schema(example = "100.000000") + public Double principalPortion; + @Schema(example = "80.000000") + public Double interestPortion; + @Schema(example = "20.000000") + public Double feeChargesPortion; + @Schema(example = "20.000000") + public Double penaltyChargesPortion; public GetLoanCurrency currency; } 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 e50e6822e42..3a91044ea73 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 @@ -2722,7 +2722,7 @@ public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO genera public OutstandingAmountsDTO fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) { OutstandingAmountsDTO outstandingAmounts; - if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { + if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled() && !isChargeOffOnDate(onDate)) { final MathContext mc = MoneyHelper.getMathContext(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); @@ -3571,4 +3571,10 @@ public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor() { public boolean isProgressiveSchedule() { return getLoanProductRelatedDetail().getLoanScheduleType() == PROGRESSIVE; } + + public boolean isChargeOffOnDate(final LocalDate onDate) { + final LoanTransaction chargeOffTransaction = findChargedOffTransaction(); + return (chargeOffTransaction == null) ? false : (chargeOffTransaction.getDateOf().compareTo(onDate) <= 0); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java index a5bf24e662d..86cb820a7db 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java @@ -54,7 +54,7 @@ public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGenerator } public void recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO) { - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) { regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); } else { regenerateRepaymentSchedule(loan, generatorDTO); @@ -73,7 +73,7 @@ public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loa * loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom = * loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom); */ - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) { regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); } else { regenerateRepaymentSchedule(loan, generatorDTO); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 7751d18aa25..d0e8495bc9b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -1154,7 +1154,7 @@ private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate } public Loan runScheduleRecalculation(Loan loan, final LocalDate recalculateFrom) { - if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) { final List existingTransactionIds = loan.findExistingTransactionIds(); ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ChangedTransactionDetail changedTransactionDetail = loanScheduleService diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 03002d86bd4..671eed4afd5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -52,6 +52,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; import org.apache.fineract.client.models.LoanProduct; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; @@ -82,6 +83,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.organisation.StaffHelper; +import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.fineract.integrationtests.useradministration.roles.RolesHelper; import org.apache.fineract.integrationtests.useradministration.users.UserHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; @@ -5802,6 +5804,92 @@ public void uc152() { }); } + // UC153: Validate Stop recalculating interest if the loan is charged-off + // 1. Create a Loan product with Adv. Pment. Alloc. and with Interest Recalculation enabled + // 2. Submit Loan, approve and Disburse + // 3. Apply Charge-Off + // 4. Prepay the loan to get the same interest amount after Charge-Off + @Test + public void uc153() { + AtomicLong createdLoanId = new AtomicLong(); + runAt("01 January 2024", () -> { + String operationDate = "01 January 2024"; + Long clientId = client.getClientId(); + BigDecimal interestRatePerPeriod = BigDecimal.valueOf(7.0); + + final Integer rescheduleStrategyMethod = 4; // Adjust last, unpaid period + PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation( + (double) 80.0, rescheduleStrategyMethod); + final PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProduct); + assertNotNull(loanProductResponse); + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6); + + applicationRequest = applicationRequest.numberOfRepayments(6)// + .loanTermFrequency(6)// + .loanTermFrequencyType(2)// + .interestRatePerPeriod(interestRatePerPeriod)// + .interestCalculationPeriodType(DAYS)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(1)// + .repaymentFrequencyType(2)// + .maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))// + ;// + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .approvedLoanAmount(BigDecimal.valueOf(100))// + .approvedOnDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .transactionAmount(BigDecimal.valueOf(100.0))// + .actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.67, 0.0, 100.0, 0.0, null); + createdLoanId.set(loanResponse.getLoanId()); + }); + + runAt("01 February 2024", () -> { + + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate("01 February 2024").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(21.0)); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + validateLoanSummaryBalances(loanDetails, 104.67, 21.0, 86.11, 13.89, null); + }); + + runAt("01 March 2024", () -> { + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + + Utils.randomStringGenerator("is", 5); + String transactionExternalId = UUID.randomUUID().toString(); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + + loanTransactionHelper.chargeOffLoan(createdLoanId.get(), + new PostLoansLoanIdTransactionsRequest().transactionDate("01 March 2024").locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + + // Loan Prepayment (before) Charge-Off transaction - With Interest Recalculation + GetLoansLoanIdTransactionsTemplateResponse transactionBefore = loanTransactionHelper + .retrieveTransactionTemplate(createdLoanId.get(), "prepayLoan", "dd MMMM yyyy", "15 February 2024", "en"); + assertEquals(88.88, transactionBefore.getAmount()); + assertEquals(86.11, transactionBefore.getPrincipalPortion()); + assertEquals(2.77, transactionBefore.getInterestPortion()); + assertEquals(0.00, transactionBefore.getFeeChargesPortion()); + assertEquals(0.00, transactionBefore.getPenaltyChargesPortion()); + + // Loan Prepayment (after) Charge-Off transaction - WithOut Interest Recalculation + GetLoansLoanIdTransactionsTemplateResponse transactionAfter = loanTransactionHelper + .retrieveTransactionTemplate(createdLoanId.get(), "prepayLoan", "dd MMMM yyyy", "01 March 2024", "en"); + assertEquals(104.67, transactionAfter.getAmount()); + assertEquals(86.11, transactionAfter.getPrincipalPortion()); + assertEquals(18.56, transactionAfter.getInterestPortion()); + assertEquals(0.00, transactionAfter.getFeeChargesPortion()); + assertEquals(0.00, transactionAfter.getPenaltyChargesPortion()); + }); + + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 1748863999d..8785ec626af 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -1920,6 +1920,11 @@ private String createChargebackPayload(final String transactionAmount, final Lon return chargebackPayload; } + public GetLoansLoanIdTransactionsTemplateResponse retrieveTransactionTemplate(Long loanId, String command, String dateFormat, + String transactionDate, String locale) { + return ok(fineract().loanTransactions.retrieveTransactionTemplate(loanId, command, dateFormat, transactionDate, locale)); + } + public GetLoansLoanIdTransactionsTemplateResponse retrieveTransactionTemplate(String loanExternalIdStr, String command, String dateFormat, String transactionDate, String locale) { return ok(