This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new f6d013c68d FINERACT-2421: Run inline COB on newly created loans that
are behind
f6d013c68d is described below
commit f6d013c68d939acbf67d854a481a748f57a70577
Author: Adam Saghy <[email protected]>
AuthorDate: Thu Feb 5 12:40:55 2026 +0100
FINERACT-2421: Run inline COB on newly created loans that are behind
---
.../features/LoanAccrualTransaction.feature | 30 ++++++++++++++++++++++
.../loanaccount/domain/LoanRepository.java | 4 +++
.../RetrieveAllNonClosedLoanIdServiceImpl.java | 5 ++++
.../fineract/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 27dce9c3cd..3c831869dd 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 24515f20ec..0cb93c1038 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<Loan,
Long>, 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<Loan,
Long>, 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<COBIdAndLastClosedBusinessDate>
findAllLoansBehindOnDisbursementDate(@Param("cobBusinessDate") LocalDate
cobBusinessDate,
+ @Param("loanIds") List<Long> loanIds, @Param("loanStatuses")
Collection<LoanStatus> 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 7c81cc43da..cdc09d38d5 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<COBIdAndLastClosedBusinessDate>
retrieveLoanBehindOnDisbursementDate(LocalDate businessDate, List<Long>
loanIds) {
+ return
loanRepository.findAllLoansBehindOnDisbursementDate(businessDate, loanIds,
NON_CLOSED_LOAN_STATUSES);
+ }
+
@Override
public List<COBIdAndLastClosedBusinessDate>
retrieveLoanIdsBehindDateOrNull(LocalDate businessDate, List<Long> 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 8604b01baa..590757fc74 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<COBIdAndExternalIdAndAccountNo>
findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate
cobBusinessDate);
+ List<COBIdAndLastClosedBusinessDate>
retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List<Long>
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 5066277a08..d1d7948421 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<String> 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<Long> loanIds) {
List<COBIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates
= new ArrayList<>();
List<List<Long>> 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<Long> loanIds) {
- inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
+ inlineLoanCOBExecutorService.execute(loanIds,
LoanCOBConstant.INLINE_LOAN_COB_JOB_NAME);
}
@Override