Skip to content

Commit 35e6f04

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
1 parent cb92bb4 commit 35e6f04

17 files changed

Lines changed: 618 additions & 9 deletions

File tree

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/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: 11 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;
@@ -350,6 +352,15 @@ public void checkWorkingCapitalLoanProductIsDeletedViaExternalId() {
350352
.contains(ErrorMessageHelper.workingCapitalLoanProductIdentifiedDoesNotExistFailure(String.valueOf(externalId)));
351353
}
352354

355+
@Then("Delinquency Tag History for WC Loan has lines:")
356+
public void checkDelinquencyHistory(final DataTable table) {
357+
PostWorkingCapitalLoansResponse workingCapitalLoanProductsResponse = testContext()
358+
.get(TestContextKey.LOAN_CREATE_RESPONSE);
359+
Long resourceId = workingCapitalLoanProductsResponse.getResourceId();
360+
List<GetDelinquencyTagHistoryResponse> responses = ok(() -> fineractFeignClient.workingCapitalLoans().getDelinquencyTagHistoryById(resourceId));
361+
log.info("Loan {}", responses);
362+
}
363+
353364
public PostWorkingCapitalLoanProductsResponse createWorkingCapitalLoanProduct(
354365
PostWorkingCapitalLoanProductsRequest workingCapitalProductRequest) {
355366
String workingCapitalProductName = workingCapitalProductRequest.getName();

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,3 +851,29 @@ 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+
Then Delinquency Tag History for WC Loan has lines:
876+
| a | b |
877+
878+
When Admin sets the business date to "01 April 2026"
879+
And Admin runs inline COB job for Working Capital Loan

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-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>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fineract.cob.workingcapitalloan.businessstep;
21+
22+
import lombok.RequiredArgsConstructor;
23+
import lombok.extern.slf4j.Slf4j;
24+
import org.apache.fineract.infrastructure.core.domain.ActionContext;
25+
import org.apache.fineract.infrastructure.core.domain.ExternalId;
26+
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
27+
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
28+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
29+
import org.springframework.stereotype.Component;
30+
31+
import java.util.Optional;
32+
33+
import static org.apache.fineract.infrastructure.core.diagnostics.performance.MeasuringUtil.measure;
34+
35+
@Slf4j
36+
@RequiredArgsConstructor
37+
@Component
38+
public class WorkingCapitalLoanDelinquencyClassificationBusinessStep extends WorkingCapitalLoanCOBBusinessStep {
39+
40+
@Override
41+
public WorkingCapitalLoan execute(WorkingCapitalLoan loan) {
42+
if (loan == null) {
43+
log.debug("Ignoring Working Capital delinquency tag processing for null loan.");
44+
return null;
45+
}
46+
String externalId = Optional.ofNullable(loan.getExternalId()).map(ExternalId::getValue).orElse(null);
47+
measure( ()-> setDelinquencyBucketTags(loan, externalId), duration -> {
48+
log.debug("Ending Working Capital delinquency tag processing for loan with Id [{}], account number [{}], external Id [{}], finished in [{}]ms",
49+
loan.getId(), loan.getAccountNumber(), externalId, duration.toMillis());
50+
});
51+
return loan;
52+
}
53+
54+
public void setDelinquencyBucketTags(WorkingCapitalLoan loan, String externalId) {
55+
try {
56+
log.debug("Starting Working Capital delinquency tag processing for loan with Id [{}], account number [{}], external Id [{}]",
57+
loan.getId(), loan.getAccountNumber(), externalId);
58+
59+
// Change the Action Context to DEFAULT for Business Date so that we can compare the loan due date
60+
// to the current date and not the previous (COB) date.
61+
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
62+
if (loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDelinquencyBucket() != null) {
63+
log.info("Evaluate {} Working Capital Delinquency bucket", loan.getLoanProductRelatedDetails().getDelinquencyBucket());
64+
evaluateDelinquencyCalculation(loan, loan.getLoanProductRelatedDetails().getDelinquencyBucket());
65+
} else {
66+
log.info("Skipping... Delinquency bucket is not configured for Working Capital Loan {}.", loan.getId());
67+
}
68+
} catch (RuntimeException re) {
69+
log.error(
70+
"Received [{}] exception while processing delinquency tag for Working Capital Loan with Id [{}], account number [{}], external Id [{}]",
71+
re.getMessage(), loan.getId(), loan.getAccountNumber(), externalId, re);
72+
73+
throw re;
74+
} finally {
75+
// Change the Action Context back to COB to resume COB steps.
76+
ThreadLocalContextUtil.setActionContext(ActionContext.COB);
77+
}
78+
}
79+
80+
private void evaluateDelinquencyCalculation(WorkingCapitalLoan loan, DelinquencyBucket delinquencyBucket) {
81+
//TODO should be this the service call???
82+
83+
}
84+
85+
@Override
86+
public String getEnumStyledName() {
87+
return "WC_LOAN_DELINQUENCY_CLASSIFICATION";
88+
}
89+
90+
@Override
91+
public String getHumanReadableName() {
92+
return "Working Capital Loan Delinquency Classification Business Step";
93+
}
94+
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.swagger.v3.oas.annotations.Operation;
2222
import io.swagger.v3.oas.annotations.Parameter;
23+
import io.swagger.v3.oas.annotations.media.ArraySchema;
2324
import io.swagger.v3.oas.annotations.media.Content;
2425
import io.swagger.v3.oas.annotations.media.Schema;
2526
import io.swagger.v3.oas.annotations.parameters.RequestBody;
@@ -35,8 +36,11 @@
3536
import jakarta.ws.rs.PathParam;
3637
import jakarta.ws.rs.Produces;
3738
import jakarta.ws.rs.QueryParam;
39+
import jakarta.ws.rs.core.Context;
3840
import jakarta.ws.rs.core.MediaType;
41+
import jakarta.ws.rs.core.UriInfo;
3942
import lombok.RequiredArgsConstructor;
43+
import org.apache.commons.lang3.NotImplementedException;
4044
import org.apache.fineract.commands.domain.CommandWrapper;
4145
import org.apache.fineract.commands.service.CommandWrapperBuilder;
4246
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
@@ -47,15 +51,21 @@
4751
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
4852
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
4953
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
54+
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger;
55+
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
56+
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
5057
import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
5158
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData;
5259
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData;
5360
import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException;
5461
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService;
62+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyReadPlatformService;
5563
import org.springframework.data.domain.Page;
5664
import org.springframework.data.domain.Pageable;
5765
import org.springframework.stereotype.Component;
5866

67+
import java.util.Collection;
68+
5969
@Component
6070
@Path("/v1/working-capital-loans")
6171
@Tag(name = "Working Capital Loans", description = "Working Capital Loan applications")
@@ -67,6 +77,7 @@ public class WorkingCapitalLoanApiResource {
6777
private final PlatformSecurityContext context;
6878
private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService;
6979
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
80+
private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService;
7081

7182
@GET
7283
@Path("template")
@@ -192,6 +203,48 @@ public CommandProcessingResult deleteLoanApplication(
192203
return deleteLoanApplication(null, loanExternalId);
193204
}
194205

206+
@GET
207+
@Path("{loanId}/delinquencytags")
208+
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
209+
@Produces(MediaType.APPLICATION_JSON)
210+
@Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", description = "", operationId = "getDelinquencyTagHistoryById")
211+
@ApiResponses({
212+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyTagHistoryResponse.class)))) })
213+
public Collection<LoanDelinquencyTagHistoryData> getDelinquencyTagHistory(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
214+
@Context final UriInfo uriInfo) {
215+
return getDelinquencyTagHistory(loanId, null, uriInfo);
216+
}
217+
private Collection<LoanDelinquencyTagHistoryData> getDelinquencyTagHistory(final Long loanId, final String loanExternalIdStr, final UriInfo uriInfo) {
218+
context.authenticatedUser().validateHasReadPermission("DELINQUENCY_TAGS");
219+
ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr);
220+
221+
final Collection<LoanDelinquencyTagHistoryData> loanDelinquencyTagHistoryData = workingCapitalLoanDelinquencyReadPlatformService
222+
.retrieveDelinquencyRangeHistory(resolveLoanIdBy(loanExternalId, loanId));
223+
return loanDelinquencyTagHistoryData;
224+
}
225+
226+
private Long resolveLoanIdBy(ExternalId externalId, Long loanId) {
227+
if (loanId == null) {
228+
throw new NotImplementedException("External Id is not supported yet.");
229+
}
230+
// TODO
231+
//return loanId == null ? loanReadPlatformService.getResolvedLoanId(loanExternalId) : loanId;
232+
return loanId;
233+
}
234+
235+
@GET
236+
@Path("external-id/{loanExternalId}/delinquencytags")
237+
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
238+
@Produces(MediaType.APPLICATION_JSON)
239+
@Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", operationId = "getDelinquencyTagHistoryByExternalId", description = "")
240+
@ApiResponses({
241+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyTagHistoryResponse.class)))) })
242+
public Collection<LoanDelinquencyTagHistoryData> getDelinquencyTagHistory(
243+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
244+
@Context final UriInfo uriInfo) {
245+
return getDelinquencyTagHistory(null, loanExternalId, uriInfo);
246+
}
247+
195248
@POST
196249
@Path("{loanId}")
197250
@Consumes({ MediaType.APPLICATION_JSON })

0 commit comments

Comments
 (0)