From f6d013c68d939acbf67d854a481a748f57a70577 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 5 Feb 2026 12:40:55 +0100 Subject: [PATCH] FINERACT-2421: Run inline COB on newly created loans that are behind --- .../features/LoanAccrualTransaction.feature | 30 +++++++++++++++++++ .../loanaccount/domain/LoanRepository.java | 4 +++ ...RetrieveAllNonClosedLoanIdServiceImpl.java | 5 ++++ .../cob/loan/RetrieveLoanIdService.java | 1 + .../jobs/filter/LoanCOBFilterHelper.java | 13 ++++---- 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index 27dce9c3c..3c831869d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -1961,3 +1961,33 @@ Feature: LoanAccrualTransaction When Loan Pay-off is made on "01 July 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_ACCRUAL_ACTIVITY_POSTING" loan product "MERCHANT_ISSUED_REFUND" transaction type to "REAMORTIZATION" future installment allocation rule + + @TestRailId:C4627 + Scenario: Verify accrual date matches charge creation date when repayment happens before COB run + When Admin sets the business date to "17 November 2025" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_ACCRUAL_ACTIVITY | 17 November 2025 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 30 | DAYS | 30 | DAYS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "17 November 2025" with "100" amount and expected disbursement date on "17 November 2025" + When Admin successfully disburse the loan on "17 November 2025" with "100" EUR transaction amount + When Admin adds "LOAN_SNOOZE_FEE" due date charge with "17 November 2025" due date and 10 EUR transaction amount + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | 17 November 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | + # --- Date changes to next day (post-midnight but before COB) --- + When Admin sets the business date to "18 November 2025" + # --- Full repayment made before COB runs --- + When Admin creates new user with "NO_BYPASS_AUTOTEST" username, "NO_BYPASS_AUTOTEST_ROLE" role name and given permissions: + | REPAYMENT_LOAN | + And Created user makes "AUTOPAY" repayment on "18 November 2025" with 110 EUR transaction amount + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + # --- Expected: Accrual transaction date should be 17 November 2025 (charge creation date) --- + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | 17 November 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | + | 18 November 2025 | Repayment | 110.0 | 100.0 | 0.0 | 10.0 | 0.0 | 0.0 | + | 17 November 2025 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | + | 18 November 2025 | Accrual Activity | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | + Then LoanAccrualTransactionCreatedBusinessEvent is raised on "17 November 2025" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "18 November 2025" diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 24515f20e..0cb93c103 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -100,6 +100,7 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ALL_LOANS_BY_LAST_CLOSED_BUSINESS_DATE_NOT_NULL_AND_MIN_AND_MAX_LOAN_ID_AND_STATUSES = "select loan.id from Loan loan where loan.id BETWEEN :minLoanId and :maxLoanId and loan.loanStatus in :loanStatuses and :cobBusinessDate = loan.lastClosedBusinessDate"; String FIND_ALL_LOANS_BEHIND_BY_LOAN_IDS_AND_STATUSES = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.id IN :loanIds and loan.loanStatus in :loanStatuses and loan.lastClosedBusinessDate < :cobBusinessDate"; + String FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.id IN :loanIds and loan.loanStatus in :loanStatuses and loan.lastClosedBusinessDate IS NULL and loan.actualDisbursementDate = :cobBusinessDate"; String FIND_ALL_STAYED_LOCKED_BY_COB_BUSINESS_DATE = "select loan.id, loan.externalId, loan.accountNumber from LoanAccountLock lock left join Loan loan on lock.loanId = loan.id where lock.lockPlacedOnCobBusinessDate = :cobBusinessDate"; @@ -275,4 +276,7 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query("select loan.loanRepaymentScheduleDetail.enableBuyDownFee from Loan loan where loan.id = :loanId") Boolean isEnabledBuyDownFee(@Param("loanId") Long loanId); + @Query(FIND_ALL_LOANS_BEHIND_ON_DISBURSEMENT_DATE) + List findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate cobBusinessDate, + @Param("loanIds") List loanIds, @Param("loanStatuses") Collection loanStatuses); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java index 7c81cc43d..cdc09d38d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java @@ -79,6 +79,11 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdServ return loanRepository.findAllLoansBehindByLoanIdsAndStatuses(businessDate, loanIds, NON_CLOSED_LOAN_STATUSES); } + @Override + public List retrieveLoanBehindOnDisbursementDate(LocalDate businessDate, List loanIds) { + return loanRepository.findAllLoansBehindOnDisbursementDate(businessDate, loanIds, NON_CLOSED_LOAN_STATUSES); + } + @Override public List retrieveLoanIdsBehindDateOrNull(LocalDate businessDate, List loanIds) { return loanRepository.findAllLoansBehindOrNullByLoanIdsAndStatuses(businessDate, loanIds, NON_CLOSED_LOAN_STATUSES); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java index 8604b01ba..590757fc7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java @@ -40,4 +40,5 @@ public interface RetrieveLoanIdService { List findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate); + List retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List loanIds); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java index 5066277a0..d1d794842 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java @@ -40,6 +40,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; import org.apache.fineract.cob.data.COBIdAndLastClosedBusinessDate; +import org.apache.fineract.cob.loan.LoanCOBConstant; import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl; import org.apache.fineract.cob.service.LoanAccountLockService; @@ -85,8 +86,6 @@ public class LoanCOBFilterHelper implements InitializingBean { private static final Predicate URL_FUNCTION = s -> LOAN_PATH_PATTERN.matcher(s).find() || LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find(); - private static final String JOB_NAME = "INLINE_LOAN_COB"; - private Long getLoanId(boolean isGlim, String pathInfo) { if (!isGlim) { String id = LOAN_PATH_PATTERN.matcher(pathInfo).replaceAll("$1"); @@ -197,8 +196,12 @@ public class LoanCOBFilterHelper implements InitializingBean { public boolean isLoanBehind(List loanIds) { List loanIdAndLastClosedBusinessDates = new ArrayList<>(); List> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit()); - partitions.forEach(partition -> loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService - .retrieveLoanIdsBehindDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition))); + partitions.forEach(partition -> { + loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService + .retrieveLoanIdsBehindDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)); + loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService.retrieveLoanBehindOnDisbursementDate( + ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)); + }); return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates); } @@ -269,7 +272,7 @@ public class LoanCOBFilterHelper implements InitializingBean { } public void executeInlineCob(List loanIds) { - inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME); + inlineLoanCOBExecutorService.execute(loanIds, LoanCOBConstant.INLINE_LOAN_COB_JOB_NAME); } @Override