You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@fineract.apache.org by "Adam Saghy (Jira)" <ji...@apache.org> on 2026/07/08 14:41:00 UTC

[jira] [Resolved] (FINERACT-2656) Disbursing a loan with an account-transfer disbursement charge and no linked savings account throws NullPointerException (HTTP 500)

     [ https://issues.apache.org/jira/browse/FINERACT-2656?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Adam Saghy resolved FINERACT-2656.
----------------------------------
    Fix Version/s: 1.15.0
       Resolution: Fixed

> Disbursing a loan with an account-transfer disbursement charge and no linked savings account throws NullPointerException (HTTP 500)
> -----------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: FINERACT-2656
>                 URL: https://issues.apache.org/jira/browse/FINERACT-2656
>             Project: Apache Fineract
>          Issue Type: Bug
>          Components: Loan
>            Reporter: Farooq Ayoade
>            Priority: Critical
>             Fix For: 1.15.0
>
>
> h3. Observed behavior
> Disbursing a loan that has a charge configured as *due at disbursement* with {*}payment mode = "account transfer"{*}, when the loan has {*}no linked savings account{*}, returns HTTP 500 with a raw NPE:
> java.lang.NullPointerException: Cannot invoke
> "org.apache.fineract.portfolio.account.data.PortfolioAccountData.getId()"
> because "savingAccountData" is null
>     at org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl.disburseLoan(LoanWritePlatformServiceJpaRepositoryImpl.java:490)
> h3. Expected behavior
> The disbursement should fail with a clean domain validation error stating that a charge payable by account transfer requires a linked savings account — the same way the disburse-to-savings path already does ({{{}LinkedAccountRequiredException{}}}, HTTP 403/400 with {{{}error.msg.loan.<context>.requires.linked.account{}}}) — *not* an unhandled NullPointerException / HTTP 500.
> h3. Steps to reproduce
>  # Create a charge with *Charge time type = Disbursement* and {*}Charge payment mode = Account transfer{*}.
>  # Attach that charge to a loan product / loan application, and *do not* configure a linked savings account on the loan.
>  # Approve the loan, then {{POST /fineract-provider/api/v1/loans/\{loanId}?command=disburse}} with a normal disbursement body.
>  # Observe HTTP 500 and the NPE above. The charge collection ({{{}isDueAtDisbursement && getChargePaymentMode().isPaymentModeAccountTransfer() && isChargePending(){}}}) is non-empty, so the code enters the transfer loop and dereferences the null linked-account.
> h3. Root cause
> {{LoanWritePlatformServiceJpaRepositoryImpl}} retrieves the loan's linked savings account inside the disbursement-charge transfer loop and dereferences it with {*}no null check{*}:
>  
>  
> for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) {
>     final PortfolioAccountData savingAccountData =
>             this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
>     ...
>     final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(),
>             PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.getId(), loanId, "Loan Charge Payment",
>             ...);
>     this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
> }
>  
>  
> {{AccountAssociationsReadPlatformServiceImpl.retriveLoanLinkedAssociation(loanId)}} returns *null* when the loan has no {{LINKED_ACCOUNT_ASSOCIATION}} row (it catches {{EmptyResultDataAccessException}} and returns the null-initialised local). The code assumes a linked account always exists and calls {{savingAccountData.getId()}} → NPE.
> The correct guard *already exists* elsewhere in the same class (disburse-to-savings, ~line 1718):
> final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
>         .retriveLoanLinkedAssociation(loan.getId());
> if (portfolioAccountData == null) {
>     final String errorMessage = "Disburse Loan with id:" + loan.getId() + " requires linked savings account for payment";
>     throw new LinkedAccountRequiredException("loan.disburse.to.savings", errorMessage, loan.getId());
> }
> h3. Affected code (same unguarded {{retriveLoanLinkedAssociation(...).getId()}} pattern)
> All in {{{}org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl{}}}:
>  * *{{disburseLoan(...)}}* — disbursement-charge transfer loop (the reported NPE).
>  * *{{disburseLoan(...)}}* — down-payment auto-transfer standing-instruction block ({{{}if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()){}}}).
>  * *{{undoLoanDisbursal(...)}}* — undo-disbursal charge transfer loop (mirror of the disburse loop).
> And in {{org.apache.fineract.portfolio.loanaccount.jobs.transferfeechargeforloans.TransferFeeChargeForLoansTasklet}} — two {{retriveLoanLinkedAssociation(...)}} call sites in the charge-transfer loop, same missing null check.
> {{}}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)