From 6910051674b1a0cbb3bfbe60d30bccfe9a172f17 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 19 Feb 2024 17:18:47 +0100 Subject: [PATCH] FINERACT-1981: Fix principal due during disbursement on overpaid loan --- .../portfolio/loanaccount/domain/Loan.java | 2 +- ...edPaymentScheduleTransactionProcessor.java | 9 ++- ...ccrualBasedAccountingProcessorForLoan.java | 6 +- .../CashBasedAccountingProcessorForLoan.java | 6 +- ...ntAllocationLoanRepaymentScheduleTest.java | 78 +++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) 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 8d936944335..efa3a3ce6cc 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 @@ -3838,7 +3838,7 @@ private Money calculateTotalOverpayment() { } if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); - } else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback() || loanTransaction.isDisbursement()) { + } else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 297f4efcaa7..1afa4f6172f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -522,7 +522,7 @@ private void updateLoanSchedule(LoanTransaction disbursementTransaction, Monetar downPaymentAmount = Money.of(currency, downPaymentAmt); downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); } - disbursementTransaction.setOverPayments(overpaymentHolder.getMoneyObject()); + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); if (amortizableAmount.isGreaterThanZero()) { @@ -548,6 +548,11 @@ private void updateLoanSchedule(LoanTransaction disbursementTransaction, Monetar private void allocateOverpayment(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, MoneyHolder overpaymentHolder) { if (overpaymentHolder.getMoneyObject().isGreaterThanZero()) { + if (overpaymentHolder.getMoneyObject().isGreaterThan(loanTransaction.getAmount(currency))) { + loanTransaction.setOverPayments(loanTransaction.getAmount(currency)); + } else { + loanTransaction.setOverPayments(overpaymentHolder.getMoneyObject()); + } List transactionMappings = new ArrayList<>(); List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() @@ -653,7 +658,7 @@ private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping l private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, MoneyHolder overpaymentHolder) { if (overpaymentPortion.isGreaterThanZero()) { onLoanOverpayment(loanTransaction, overpaymentPortion); - overpaymentHolder.setMoneyObject(overpaymentPortion); + overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().add(overpaymentPortion)); loanTransaction.setOverPayments(overpaymentPortion); } else { overpaymentHolder.setMoneyObject(overpaymentPortion.zero()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index 8ba14d78f52..f0ed03edb45 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -477,9 +477,11 @@ private void createJournalEntriesForDisbursements(final LoanDTO loanDTO, final L // create journal entries for the disbursement (or disbursement // reversal) - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversed); + if (MathUtil.isGreaterThanZero(principalPortion)) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversed); + } if (MathUtil.isGreaterThanZero(overpaymentPortion)) { this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.OVERPAYMENT.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, overpaymentPortion, isReversed); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index 5c261725f75..4a2f9009c02 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -475,8 +475,10 @@ private void createJournalEntriesForDisbursements(final LoanDTO loanDTO, final L final boolean isReversal = loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversal); + if (MathUtil.isGreaterThanZero(principalPortion)) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversal); + } if (MathUtil.isGreaterThanZero(overpaymentPortion)) { this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.OVERPAYMENT.getValue(), 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 603f100159a..f6723cf7707 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 @@ -3205,6 +3205,84 @@ public void uc122() { }); } + // UC123: Advanced payment allocation, 2nd disbursement on overpaid loan + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. + // 2. Submit Loan and approve + // 3. Disburse only 100 from 1000 + // 4. Overpay the loan (150) + // 5. Disburse again 25 + @Test + public void uc123() { + runAt("22 November 2023", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true) + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)).enableAutoRepaymentForDownPayment(false); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023", + 1000.0, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("22 November 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 0.0, 25.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 7), 25.0, 0.0, 25.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 22), 25.0, 0.0, 25.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 1, 6), 25.0, 0.0, 25.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("22 November 2023").locale("en").transactionAmount(150.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 100.0, 0.0, 100.0, 50.0); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 7), 25.0, 25.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 22), 25.0, 25.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 1, 6), 25.0, 25.0, 0.0, 25.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(28.0)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 128.0, 0.0, 128.0, 22.0); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 11, 22), 7.0, 7.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 7), 32.0, 32.0, 0.0, 32.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 12, 22), 32.0, 32.0, 0.0, 32.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 1, 6), 32.0, 32.0, 0.0, 32.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + verifyTransactions(loanResponse.getLoanId(), // + transaction(100, "Disbursement", "22 November 2023", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(150, "Repayment", "22 November 2023", 0.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(28, "Disbursement", "22 November 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 28.0) // + ); + // verify journal entries + verifyJournalEntries(loanResponse.getLoanId(), journalEntry(100.0, loansReceivableAccount, "DEBIT"), // + journalEntry(100.0, suspenseClearingAccount, "CREDIT"), // + journalEntry(100.0, loansReceivableAccount, "CREDIT"), // + journalEntry(50.0, overpaymentAccount, "CREDIT"), // + journalEntry(150.0, suspenseClearingAccount, "DEBIT"), // + journalEntry(28.0, overpaymentAccount, "DEBIT"), // + journalEntry(28.0, suspenseClearingAccount, "CREDIT") // + ); + }); + } + private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, Double principalOutstanding, Double principalPaid, Double totalOverpaid) { assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());