FINERACT-2421: Run inline COB on newly created loans that are behind

This commit is contained in:
Adam Saghy
2026-02-06 09:34:32 +01:00
committed by Arnold Gálovics
parent 967e0ea306
commit f6d013c68d
5 changed files with 48 additions and 5 deletions
@@ -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"
@@ -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);
}
@@ -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);
@@ -40,4 +40,5 @@ public interface RetrieveLoanIdService {
List<COBIdAndExternalIdAndAccountNo> findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate);
List<COBIdAndLastClosedBusinessDate> retrieveLoanBehindOnDisbursementDate(LocalDate businessDateByType, List<Long> loanIds);
}
@@ -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