diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index 3b7649d6a..722bf3f82 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3350,6 +3350,7 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | + | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | Then Loan Repayment schedule has 2 periods, with the following data for periods: @@ -3369,9 +3370,10 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | + | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | - | 25 August 2024 | Accrual | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | + | 25 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | Then Loan Repayment schedule has 2 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index ece6bcdd8..266d52c66 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -1264,6 +1264,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing updateLoanChargesAndInstallmentChargesPaidBy(loan, accrualTransaction); // TODO check if this is required // saveLoanTransactionWithDataIntegrityViolationChecks(accrualTransaction); + accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction); loan.addLoanTransaction(accrualTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); @@ -1357,11 +1358,17 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } BigDecimal accruedAmount = BigDecimal.ZERO; BigDecimal waivedAmount = BigDecimal.ZERO; - for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { - if (loanChargePaidBy.getLoanTransaction().isAccrual()) { - accruedAmount = accruedAmount.add(loanChargePaidBy.getLoanTransaction().getAmount()); - } else if (loanChargePaidBy.getLoanTransaction().isChargesWaiver()) { - waivedAmount = waivedAmount.add(loanChargePaidBy.getLoanTransaction().getAmount()); + for (LoanTransaction loanTransaction : loan.getLoanTransactions()) { + if (loanTransaction.isAccrual() || loanTransaction.isChargesWaiver()) { + for (LoanChargePaidBy loanChargePaidBy : loanTransaction.getLoanChargesPaid()) { + if (loanChargePaidBy.getLoanCharge().getId().equals(loanCharge.getId())) { + if (loanTransaction.isAccrual()) { + accruedAmount = accruedAmount.add(loanTransaction.getAmount()); + } else if (loanTransaction.isChargesWaiver()) { + waivedAmount = waivedAmount.add(loanTransaction.getAmount()); + } + } + } } } Money needToAccrueAmount = MathUtil.negativeToZero(loanCharge.getAmount(currency).minus(accruedAmount).minus(waivedAmount)); 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 488c7fc55..75f86c50a 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 @@ -1031,8 +1031,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge); // we want to apply charge transactions only for those loans charges that are applied when a loan is active and - // the loan product uses Upfront Accruals - if (loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + // the loan product uses Upfront Accruals, or only when the loan are closed too, + if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) + || loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()) { final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); } 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 b91140e8e..5091ade69 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 @@ -48,6 +48,7 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; 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.LoanProduct; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; @@ -55,6 +56,7 @@ import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; @@ -5274,6 +5276,84 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC149: Validate TotalUnpaidAccruedDueInterest in Zero when Interest paid is higher than Accrued Interest + // 1. Create a Loan product with Adv. Pment. Alloc. and with Declining Balance, Accrual accounting and Daily Accrual + // Activity + // 2. Submit Loan, approve and Disburse + // 3. Add a Loan Specific Due date charge + // 4. Add a Repayment higher than accrued interest to validate TotalUnpaidAccruedDueInterest equal to Zero + @Test + public void uc149() { + String operationDate = "22 April 2024"; + AtomicLong createdLoanId = new AtomicLong(); + AtomicLong createdLoanChargeId = new AtomicLong(); + AtomicLong createdLoanAccrualId = new AtomicLong(); + runAt(operationDate, () -> { + Long clientId = client.getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRateFrequencyType(YEARS).numberOfRepayments(4)// + .maxInterestRatePerPeriod((double) 0)// + .repaymentEvery(1)// + .repaymentFrequencyType(1L)// + .allowPartialPeriodInterestCalcualtion(false)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(null)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .installmentAmountInMultiplesOf(null)// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 400.0, 6); + + applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS).interestRatePerPeriod(BigDecimal.ZERO) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(400.0)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(400.0)).locale("en")); + + addRepaymentForLoan(createdLoanId.get(), 600.00, operationDate); + + createdLoanChargeId.set(addCharge(createdLoanId.get(), true, 30, operationDate)); + + executeInlineCOB(createdLoanId.get()); + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + // Validate Loan Accrual transaction + final GetLoansLoanIdTransactions loanTransaction = loanDetails.getTransactions().stream() + .filter(t -> Boolean.TRUE.equals(t.getType().getAccrual())).toList().get(0); + assertNotNull(loanTransaction); + createdLoanAccrualId.set(loanTransaction.getId()); + }); + + runAt("10 October 2024", () -> { + PostLoansLoanIdChargesChargeIdRequest request = new PostLoansLoanIdChargesChargeIdRequest().amount(15.0).locale("en"); + loanTransactionHelper.chargeAdjustment(createdLoanId.get(), createdLoanChargeId.get(), request); + + loanTransactionHelper.makeGoodwillCredit(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate(operationDate).locale("en").transactionAmount(15.0)); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + // Get the Repayment transaction + GetLoansLoanIdTransactions loanTransaction = loanDetails.getTransactions().stream() + .filter(t -> Boolean.TRUE.equals(t.getType().getRepayment())).toList().get(0); + loanTransactionHelper.reverseRepayment(Math.toIntExact(createdLoanId.get()), Math.toIntExact(loanTransaction.getId()), + operationDate); + + // Validate Loan Accrual transaction + loanTransaction = loanDetails.getTransactions().stream().filter(t -> Boolean.TRUE.equals(t.getType().getAccrual())).toList() + .get(0); + assertNotNull(loanTransaction); + assertEquals(loanTransaction.getId(), createdLoanAccrualId.get()); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");