Skip to content

Commit 2f3cfb3

Browse files
committed
FINERACT-2455: WC - Delinquency Management - Delinquency days & Delinquency Bucket handling
- create SetWorkingCapitalLoanDelinquencyTagsBusinessStep - QA test for business step order - create ENABLE_INSTANT_DELINQUENCY_CALCULATION + integration tests - Delinquency History for Working Capital Loans - DTOs Entity & Repository - Get API - introduce WorkingCapitalLoanDelinquencyReadPlatformService - for read operations. - Cucumber StepDef + basic tests - WIP - WorkingCapitalLoanDelinquencyClassificationService - Handle loan level delinquency + grace day handling
1 parent 44617c9 commit 2f3cfb3

File tree

22 files changed

+854
-26
lines changed

22 files changed

+854
-26
lines changed

fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public final class GlobalConfigurationConstants {
8181
public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
8282
public static final String MAX_LOGIN_RETRY_ATTEMPTS = "max-login-retry-attempts";
8383
public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application";
84+
public static final String ENABLE_INSTANT_DELINQUENCY_CALCULATION = "enable-instant-delinquency-calculation";
8485
public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT = "password-reuse-check-history-count";
8586
public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT = "allow-force-withdrawal-on-savings-account";
8687
public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT = "force-withdrawal-on-savings-account-limit";

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
public enum DefaultWorkingCapitalLoanProduct implements WorkingCapitalLoanProduct {
2222

2323
WCLP, //
24-
WCLP_FOR_UPDATE; //
24+
WCLP_FOR_UPDATE, WCLP_GRACE_5; //
2525

2626
@Override
2727
public String getName() {

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalProductLoanAccountStepDef.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,12 @@ private void createWorkingCapitalLoanAccount(final List<String> loanData) {
639639
final PostWorkingCapitalLoansResponse response = ok(
640640
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
641641
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
642+
List<Long> loanIds = testContext().get(TestContextKey.WC_LOAN_IDS);
643+
if (loanIds == null) {
644+
loanIds = new ArrayList<>();
645+
testContext().set(TestContextKey.WC_LOAN_IDS, loanIds);
646+
}
647+
loanIds.add(response.getLoanId());
642648
log.info("Working Capital Loan created with ID: {}", response.getLoanId());
643649
}
644650

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@
3636
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
3737
import org.apache.fineract.client.models.DeleteWorkingCapitalLoanProductsProductIdResponse;
3838
import org.apache.fineract.client.models.GetConfigurableAttributes;
39+
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
3940
import org.apache.fineract.client.models.GetPaymentAllocation;
4041
import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsProductIdResponse;
4142
import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsTemplateResponse;
4243
import org.apache.fineract.client.models.PostAllowAttributeOverrides;
4344
import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest;
4445
import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse;
46+
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
4547
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest;
4648
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdResponse;
4749
import org.apache.fineract.client.models.StringEnumOptionData;
@@ -53,6 +55,7 @@
5355
import org.apache.fineract.test.stepdef.AbstractStepDef;
5456
import org.apache.fineract.test.support.TestContextKey;
5557
import org.assertj.core.api.SoftAssertions;
58+
import org.junit.jupiter.api.Assertions;
5659
import org.springframework.beans.factory.annotation.Autowired;
5760

5861
@Slf4j
@@ -350,6 +353,30 @@ public void checkWorkingCapitalLoanProductIsDeletedViaExternalId() {
350353
.contains(ErrorMessageHelper.workingCapitalLoanProductIdentifiedDoesNotExistFailure(String.valueOf(externalId)));
351354
}
352355

356+
@Then("Delinquency Tag History for WC Loan has lines:")
357+
public void checkDelinquencyHistory(final DataTable table) {
358+
PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
359+
Long resourceId = workingCapitalLoanProductsResponse.getResourceId();
360+
List<GetDelinquencyTagHistoryResponse> actualLines = ok(
361+
() -> fineractFeignClient.workingCapitalLoans().getDelinquencyTagHistoryById(resourceId));
362+
log.info("Loan {}", actualLines);
363+
List<List<String>> rows = table.asLists();
364+
Assertions.assertEquals(rows.size() - 1, actualLines.size());
365+
for (int i = 0; i < rows.size() - 1; i++) {
366+
GetDelinquencyTagHistoryResponse actual = actualLines.get(i);
367+
List<String> expected = rows.get(i + 1);
368+
Assertions.assertEquals(expected.get(0), actual.getAddedOnDate() != null ? actual.getAddedOnDate().toString() : null);
369+
Assertions.assertEquals(expected.get(1), actual.getLiftedOnDate() != null ? actual.getLiftedOnDate().toString() : null);
370+
371+
Assertions.assertNotNull(actual.getDelinquencyRange());
372+
Assertions.assertEquals(expected.get(2), actual.getDelinquencyRange().getClassification());
373+
Assertions.assertEquals(expected.get(3), actual.getDelinquencyRange().getMinimumAgeDays() == null ? null
374+
: actual.getDelinquencyRange().getMinimumAgeDays().toString());
375+
Assertions.assertEquals(expected.get(4), actual.getDelinquencyRange().getMaximumAgeDays() == null ? null
376+
: actual.getDelinquencyRange().getMaximumAgeDays().toString());
377+
}
378+
}
379+
353380
public PostWorkingCapitalLoanProductsResponse createWorkingCapitalLoanProduct(
354381
PostWorkingCapitalLoanProductsRequest workingCapitalProductRequest) {
355382
String workingCapitalProductName = workingCapitalProductRequest.getName();

fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ public void initialize() throws Exception {
6161
TestContext.GLOBAL.set(TestContextKey.DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST_FOR_UPDATE_WCLP,
6262
defaultForUpdateWCPLRequest);
6363
TestContext.GLOBAL.set(TestContextKey.DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_FOR_UPDATE_WCLP, responseForUpdateWCPL);
64+
65+
// Delinquency grace period testing
66+
67+
final String WCLP_GRACE_5 = DefaultWorkingCapitalLoanProduct.WCLP_GRACE_5.getName();
68+
final PostWorkingCapitalLoanProductsRequest WCLP_GRACE_5Request = workingCapitalRequestFactory
69+
.defaultWorkingCapitalLoanProductRequest() //
70+
.name(WCLP_GRACE_5) //
71+
.delinquencyBucketId(2L) //
72+
.delinquencyGraceDays(5); //
73+
74+
createWorkingCapitalLoanProductIdempotent(WCLP_GRACE_5Request);
6475
}
6576

6677
private PostWorkingCapitalLoanProductsResponse createWorkingCapitalLoanProductIdempotent(

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,3 +851,92 @@ Feature: WorkingCapitalProduct
851851
And Working capital loan account has the correct data:
852852
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
853853
| WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 |
854+
855+
@TestRailId:CXXXXX1
856+
Scenario: Working Capital Loan COB + Delinquency UC1
857+
When Admin sets the business date to "01 January 2026"
858+
And Admin creates a client with random data
859+
And Admin creates a working capital loan with the following data:
860+
| LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount |
861+
| WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 |
862+
Then Working capital loan creation was successful
863+
And Working capital loan account has the correct data:
864+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
865+
| WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 |
866+
Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026"
867+
Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount
868+
Then Working Capital loan status will be "ACTIVE"
869+
Then Verify Working Capital loan disbursement was successful on "01 January 2026" with "100" EUR transaction amount
870+
And Working capital loan account has the correct data:
871+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
872+
| WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 |
873+
When Admin sets the business date to "02 January 2026"
874+
And Admin runs inline COB job for Working Capital Loan
875+
## Should be ok
876+
Then Delinquency Tag History for WC Loan has lines:
877+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
878+
879+
When Admin sets the business date to "30 January 2026"
880+
And Admin runs inline COB job for Working Capital Loan
881+
## Should be ok
882+
Then Delinquency Tag History for WC Loan has lines:
883+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
884+
885+
When Admin sets the business date to "31 January 2026"
886+
And Admin runs inline COB job for Working Capital Loan
887+
## Should be delinquent for 1 days and Delinquency range 1
888+
Then Delinquency Tag History for WC Loan has lines:
889+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
890+
| 2026-01-31 | | D00 | 1 | 30 |
891+
892+
When Admin sets the business date to "01 April 2026"
893+
And Admin runs inline COB job for Working Capital Loan
894+
Then Delinquency Tag History for WC Loan has lines:
895+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
896+
| 2026-04-01 | | D60 | 61 | 90 |
897+
| 2026-03-02 | 2026-04-01 | D30 | 31 | 60 |
898+
| 2026-01-31 | 2026-03-02 | D00 | 1 | 30 |
899+
900+
@TestRailId:CXXXXX2
901+
Scenario: Working Capital Loan COB + 5 grace days Delinquency UC2
902+
When Admin sets the business date to "01 January 2026"
903+
And Admin creates a client with random data
904+
And Admin creates a working capital loan with the following data:
905+
| LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount |
906+
| WCLP_GRACE_5 | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 |
907+
Then Working capital loan creation was successful
908+
And Working capital loan account has the correct data:
909+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
910+
| WCLP_GRACE_5 | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 |
911+
Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026"
912+
Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount
913+
Then Working Capital loan status will be "ACTIVE"
914+
Then Verify Working Capital loan disbursement was successful on "01 January 2026" with "100" EUR transaction amount
915+
And Working capital loan account has the correct data:
916+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
917+
| WCLP_GRACE_5 | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 |
918+
When Admin sets the business date to "02 January 2026"
919+
And Admin runs inline COB job for Working Capital Loan
920+
## Should be ok
921+
Then Delinquency Tag History for WC Loan has lines:
922+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
923+
When Admin sets the business date to "04 February 2026"
924+
And Admin runs inline COB job for Working Capital Loan
925+
## Should be ok
926+
Then Delinquency Tag History for WC Loan has lines:
927+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
928+
929+
When Admin sets the business date to "05 February 2026"
930+
And Admin runs inline COB job for Working Capital Loan
931+
## Should be delinquent for 1 days and Delinquency range 1
932+
Then Delinquency Tag History for WC Loan has lines:
933+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
934+
| 2026-02-05 | | D00 | 1 | 30 |
935+
936+
When Admin sets the business date to "06 April 2026"
937+
And Admin runs inline COB job for Working Capital Loan
938+
Then Delinquency Tag History for WC Loan has lines:
939+
| addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays |
940+
| 2026-04-06 | | D60 | 61 | 90 |
941+
| 2026-03-07 | 2026-04-06 | D30 | 31 | 60 |
942+
| 2026-02-05 | 2026-03-07 | D00 | 1 | 30 |

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ Feature: Working Capital COB Job
88
Scenario: Verify WC COB job registration, default business step, and scheduler metadata
99
Then Admin checks that configured business jobs contain "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS"
1010
Then Admin verifies configured business steps for "WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS" match:
11-
| stepName | order |
12-
| DUMMY_BUSINESS_STEP | 1 |
13-
| WC_DELINQUENCY_RANGE_SCHEDULE | 2 |
11+
| stepName | order |
12+
| DUMMY_BUSINESS_STEP | 1 |
13+
| WC_DELINQUENCY_RANGE_SCHEDULE | 2 |
14+
| WC_LOAN_DELINQUENCY_CLASSIFICATION | 3 |
1415
Then Admin verifies scheduler job "WC_COB" has display name "Working Capital Loan COB"
1516
Then Admin verifies scheduler job "WC_COB" has active status "false"
1617

fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,24 +145,26 @@ public CollectionData getOverdueCollectionData(final Loan loan, final List<LoanD
145145

146146
@Override
147147
public LoanDelinquencyData getLoanDelinquencyData(final Loan loan, List<LoanDelinquencyActionData> effectiveDelinquencyList) {
148+
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
149+
150+
final CollectionData collectionData = CollectionData.template();
151+
final Map<Long, CollectionData> loanInstallmentsCollectionData = new HashMap<>(); // If the Loan is not Active
152+
// yet, return template data
153+
// If the Loan is Rejected, Closed written-off, Withdrawn by Client, Closed with outstanding marked for
154+
// reschedule, Closed obligation met, Overpaid, return template data
155+
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isClosed() || loan.getStatus().isOverpaid()) {
156+
return new LoanDelinquencyData(collectionData, loanInstallmentsCollectionData);
157+
}
158+
148159
final List<LoanTransaction> chargebackTransactions = loanTransactionReadService.fetchLoanTransactionsByType(loan.getId(), null,
149160
LoanTransactionType.CHARGEBACK);
150161
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
151-
final CollectionData collectionData = CollectionData.template();
152-
final Map<Long, CollectionData> loanInstallmentsCollectionData = new HashMap<>();
162+
153163
LocalDate overdueSinceDate = null;
154164
BigDecimal outstandingAmount = BigDecimal.ZERO;
155165
boolean oldestOverdueInstallment = false;
156166
boolean overdueSinceDateWasSet = false;
157167
boolean firstNotYetDueInstallment = false;
158-
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
159-
160-
// If the Loan is not Active yet, return template data
161-
// If the Loan is Rejected, Closed written-off, Withdrawn by Client, Closed with outstanding marked for
162-
// reschedule, Closed obligation met, Overpaid, return template data
163-
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isClosed() || loan.getStatus().isOverpaid()) {
164-
return new LoanDelinquencyData(collectionData, loanInstallmentsCollectionData);
165-
}
166168

167169
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
168170
CollectionData installmentCollectionData = CollectionData.template();

fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
<class>org.apache.fineract.cob.domain.WorkingCapitalLoanAccountLock</class>
305305
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan</class>
306306
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBalance</class>
307+
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyTagHistory</class>
307308
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDisbursementDetails</class>
308309
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNote</class>
309310
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPaymentAllocationRule</class>

0 commit comments

Comments
 (0)