From 99352c66b25279de256a058f8ac4cd76ab318c65 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 5 Feb 2024 16:45:00 +0100 Subject: [PATCH] FINERACT-1981: Disbursement on overpaid loan --- .../portfolio/loanaccount/domain/Loan.java | 11 +-- ...edPaymentScheduleTransactionProcessor.java | 52 ++++++++---- .../service/AccountingProcessorHelper.java | 2 +- ...ccrualBasedAccountingProcessorForLoan.java | 36 +++++---- .../CashBasedAccountingProcessorForLoan.java | 35 ++++---- ...WritePlatformServiceJpaRepositoryImpl.java | 18 ++--- ...ntAllocationLoanRepaymentScheduleTest.java | 81 +++++++++++++++++++ ...ncedPaymentAllocationWaiveLoanCharges.java | 18 ++--- .../BaseLoanIntegrationTest.java | 5 +- 9 files changed, 186 insertions(+), 72 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 3e1ab126f..400c769ee 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 @@ -2483,7 +2483,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map actualChanges, - final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail, boolean downPaymentEnabled) { + final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) { final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); @@ -2501,7 +2501,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(this.repaymentScheduleDetail().isInterestRecalculationEnabled(), - isDisbursementMissed(), scheduleGeneratorDTO, downPaymentEnabled); + isDisbursementMissed(), scheduleGeneratorDTO); updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); @@ -2533,7 +2533,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam, - boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean downPaymentEnabled) { + boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO) { LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate(); if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) { @@ -3837,7 +3837,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); - } else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback()) { + } else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback() || loanTransaction.isDisbursement()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); } } @@ -5940,7 +5940,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { List loanTransactions = retrieveListOfTransactionsExcludeAccruals(); for (LoanTransaction loanTransaction : loanTransactions) { if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) { - outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())); + outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())) + .minus(loanTransaction.getOverPaymentPortion(getCurrency())); loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { Money transactionOutstanding = loanTransaction.getAmount(getCurrency()); 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 c63291b5a..37d43be4d 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 @@ -154,7 +154,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Set charges, MoneyHolder overpaymentHolder) { switch (loanTransaction.getTypeOf()) { - case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments); + case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments, overpaymentHolder); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder); @@ -281,12 +281,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments) { - updateLoanSchedule(loanTransaction, currency, installments); + List installments, MoneyHolder overpaymentHolder) { + updateLoanSchedule(loanTransaction, currency, installments, overpaymentHolder); } private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency, - List installments) { + List installments, MoneyHolder overpaymentHolder) { final MathContext mc = MoneyHelper.getMathContext(); List candidateRepaymentInstallments = installments.stream().filter( i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) @@ -304,23 +304,43 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); } downPaymentAmount = Money.of(currency, downPaymentAmt); + Money autoPayFromOverpayment = overpaymentHolder.getMoneyObject(); downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); - + Money paidAmount = downPaymentInstallment.payPrincipalComponent(disbursementTransaction.getTransactionDate(), + autoPayFromOverpayment); + disbursementTransaction.setOverPayments(paidAmount); + overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(paidAmount)); } Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); - Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode()); - if (installmentAmountInMultiplesOf != null) { - increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf); + Money overpaidAmount = overpaymentHolder.getMoneyObject(); + if (overpaidAmount.isGreaterThanZero()) { + if (amortizableAmount.isGreaterThan(overpaidAmount)) { + overpaymentHolder.setMoneyObject(Money.zero(currency)); + amortizableAmount = amortizableAmount.minus(overpaidAmount); + disbursementTransaction.setOverPayments(disbursementTransaction.getOverPaymentPortion(currency).add(overpaidAmount)); + } else { + overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(amortizableAmount)); + amortizableAmount = Money.zero(currency); + disbursementTransaction.setOverPayments(disbursementTransaction.getOverPaymentPortion(currency).add(amortizableAmount)); + } } - Money remainingAmount = amortizableAmount - .minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode())); - Money finalIncreasePrincipalBy = increasePrincipalBy; - candidateRepaymentInstallments - .forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy)); - // Hence the rounding, we might need to amend the last installment amount - candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) - .addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount); + if (amortizableAmount.isGreaterThanZero()) { + Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode()); + if (installmentAmountInMultiplesOf != null) { + increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf); + } + + Money remainingAmount = amortizableAmount + .minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode())); + + Money finalIncreasePrincipalBy = increasePrincipalBy; + candidateRepaymentInstallments + .forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy)); + // Hence the rounding, we might need to amend the last installment amount + candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) + .addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount); + } } private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency, diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 0ca95a995..6c637e9a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -784,7 +784,7 @@ public class AccountingProcessorHelper { return this.officeRepository.getReferenceById(officeId); } - private void createCreditJournalEntryOrReversalForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, + public void createCreditJournalEntryOrReversalForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); 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 fba0cbd25..0b4cd0672 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 @@ -33,6 +33,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.office.domain.Office; import org.springframework.stereotype.Component; @@ -467,30 +468,34 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); - final BigDecimal disbursalAmount = loanTransactionDTO.getAmount(); + final BigDecimal overpaymentPortion = loanTransactionDTO.getOverPayment() != null ? loanTransactionDTO.getOverPayment() + : BigDecimal.ZERO; + final BigDecimal principalPortion = loanTransactionDTO.getAmount().subtract(overpaymentPortion); final boolean isReversed = loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); // create journal entries for the disbursement (or disbursement // reversal) - if (loanTransactionDTO.isLoanToLoanTransfer()) { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversed); - } else if (loanTransactionDTO.isAccountTransfer()) { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversed); - } else { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversed); - } + 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); + } + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed); + } else { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed); + } } /** - * * Handles repayments using the following posting rules
*
*
@@ -520,7 +525,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess * @param loanTransactionDTO * @param loanDTO * @param office - * */ private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office, final boolean writeOff, final boolean isIncomeFromFee) { 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 88f5f39c5..5c261725f 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 @@ -32,6 +32,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.office.domain.Office; import org.springframework.stereotype.Component; @@ -468,23 +469,29 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); - final BigDecimal disbursalAmount = loanTransactionDTO.getAmount(); + final BigDecimal overpaymentPortion = loanTransactionDTO.getOverPayment() != null ? loanTransactionDTO.getOverPayment() + : BigDecimal.ZERO; + final BigDecimal principalPortion = loanTransactionDTO.getAmount().subtract(overpaymentPortion); final boolean isReversal = loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); - if (loanTransactionDTO.isLoanToLoanTransfer()) { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversal); - } else if (loanTransactionDTO.isAccountTransfer()) { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversal); - } else { - this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, - disbursalAmount, isReversal); - } + 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(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, overpaymentPortion, isReversal); + } + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal); + } else { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal); + } } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index b3a9798ca..565812c8e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -427,7 +427,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf disburseLoanToLoan(loan, command, loanOutstanding); } - + LoanTransaction disbursementTransaction = null; if (isAccountTransfer) { disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail); existingTransactionIds.addAll(loan.findExistingTransactionIds()); @@ -435,11 +435,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } else { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, + disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, actualDisbursementDate, txnExternalId); disbursementTransaction.updateLoan(loan); loan.addLoanTransaction(disbursementTransaction); - LoanTransaction savedLoanTransaction = loanTransactionRepository.saveAndFlush(disbursementTransaction); } if (loan.getRepaymentScheduleInstallments().isEmpty()) { /* @@ -454,12 +453,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); } if (isPaymentTypeApplicableForDisbursementCharge) { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, - downPaymentEnabled); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); } else { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); } loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); + if (disbursementTransaction != null) { + loanTransactionRepository.saveAndFlush(disbursementTransaction); + } if (loan.isAutoRepaymentForDownPaymentEnabled()) { // updating linked savings account for auto down payment transaction for disbursement to savings account if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { @@ -764,10 +765,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); } if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, - downPaymentEnabled); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); } else { - changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); + changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); } } if (!changes.isEmpty()) { 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 97dc0fb0d..b682c2250 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 @@ -46,6 +46,8 @@ import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; 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.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; @@ -3124,6 +3126,85 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC122: 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 (110) + // 5. Disburse again 100 + @Test + public void uc122() { + runAt("24 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("23 November 2023").locale("en").transactionAmount(110.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 100.0, 0.0, 100.0, 10.0); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 25.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("24 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 90.0, 110.0, 90.0, 110.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 25.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 11, 24), 25.0, 10.0, 15.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 7), 50.0, 25.0, 25.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 12, 22), 50.0, 25.0, 25.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 1, 6), 50.0, 25.0, 25.0, 25.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(110, "Repayment", "23 November 2023", 0.0, 100.0, 0.0, 0.0, 0.0, 0.0, 10.0), // + transaction(100, "Disbursement", "24 November 2023", 90.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0) // + ); + // verify journal entries + verifyJournalEntries(loanResponse.getLoanId(), journalEntry(100.0, loansReceivableAccount, "DEBIT"), // + journalEntry(100.0, suspenseClearingAccount, "CREDIT"), // + journalEntry(100.0, loansReceivableAccount, "CREDIT"), // + journalEntry(10.0, overpaymentAccount, "CREDIT"), // + journalEntry(110.0, suspenseClearingAccount, "DEBIT"), // + journalEntry(90.0, loansReceivableAccount, "DEBIT"), // + journalEntry(10.0, overpaymentAccount, "DEBIT"), // + journalEntry(100.0, suspenseClearingAccount, "CREDIT") // + ); + }); + } + private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, Double principalOutstanding, Double principalPaid, Double totalOverpaid) { assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationWaiveLoanCharges.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationWaiveLoanCharges.java index 250dacc7e..6a0ee5ac5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationWaiveLoanCharges.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationWaiveLoanCharges.java @@ -66,8 +66,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati // Then verify verifyTransactions(loanId, // - transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // - transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) // ); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); @@ -99,8 +99,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati // Then verify verifyTransactions(loanId, // - transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // - transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) // ); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); @@ -137,8 +137,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati // Then verify verifyTransactions(loanId, // - transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // - transaction(50, "Waive loan charges", "05 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(50, "Waive loan charges", "05 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) // ); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); @@ -151,9 +151,9 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati addRepaymentForLoan(loanId, 200.0, "03 January 2023"); verifyTransactions(loanId, // - transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // - transaction(200, "Repayment", "03 January 2023", 800.0, 200.0, 0.0, 0.0, 0.0, 0.0), // - transaction(50, "Waive loan charges", "05 January 2023", 800.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(200, "Repayment", "03 January 2023", 800.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(50, "Waive loan charges", "05 January 2023", 800.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) // ); }); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index dcc4c1d8a..b04dd36eb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -489,9 +489,9 @@ public abstract class BaseLoanIntegrationTest { } protected TransactionExt transaction(double amount, String type, String date, double outstandingAmount, double principalPortion, - double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion) { + double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion, double overpaymentPortion) { return new TransactionExt(amount, type, date, outstandingAmount, principalPortion, interestPortion, feePortion, penaltyPortion, - unrecognizedIncomePortion); + unrecognizedIncomePortion, overpaymentPortion); } protected Installment installment(double principalAmount, Boolean completed, String dueDate) { @@ -530,6 +530,7 @@ public abstract class BaseLoanIntegrationTest { Double feePortion; Double penaltyPortion; Double unrecognizedPortion; + Double overpaymentPortion; } @ToString