Skip to content

Commit 43a8303

Browse files
authored
Merge pull request #5684
FINERACT-2455: Working Capital delinquency pause
2 parents 0b0153b + 325d873 commit 43a8303

28 files changed

Lines changed: 2089 additions & 11 deletions

File tree

fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
import org.apache.fineract.client.feign.services.InterestRateChartApi;
8484
import org.apache.fineract.client.feign.services.InterestRateSlabAKAInterestBandsApi;
8585
import org.apache.fineract.client.feign.services.InternalCobApi;
86+
import org.apache.fineract.client.feign.services.InternalWorkingCapitalLoansApi;
8687
import org.apache.fineract.client.feign.services.JournalEntriesApi;
8788
import org.apache.fineract.client.feign.services.LikelihoodApi;
8889
import org.apache.fineract.client.feign.services.ListReportMailingJobHistoryApi;
@@ -154,6 +155,7 @@
154155
import org.apache.fineract.client.feign.services.UserGeneratedDocumentsApi;
155156
import org.apache.fineract.client.feign.services.UsersApi;
156157
import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi;
158+
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyActionsApi;
157159
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyRangeScheduleApi;
158160
import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
159161
import org.apache.fineract.client.feign.services.WorkingCapitalLoanTransactionsApi;
@@ -755,10 +757,18 @@ public WorkingCapitalLoanCobCatchUpApi workingCapitalLoanCobCatchUpApi() {
755757
return create(WorkingCapitalLoanCobCatchUpApi.class);
756758
}
757759

760+
public WorkingCapitalLoanDelinquencyActionsApi workingCapitalLoanDelinquencyActions() {
761+
return create(WorkingCapitalLoanDelinquencyActionsApi.class);
762+
}
763+
758764
public WorkingCapitalLoanDelinquencyRangeScheduleApi workingCapitalLoanDelinquencyRangeSchedule() {
759765
return create(WorkingCapitalLoanDelinquencyRangeScheduleApi.class);
760766
}
761767

768+
public InternalWorkingCapitalLoansApi internalWorkingCapitalLoans() {
769+
return create(InternalWorkingCapitalLoansApi.class);
770+
}
771+
762772
public WorkingCapitalLoansApi workingCapitalLoans() {
763773
return create(WorkingCapitalLoansApi.class);
764774
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
package org.apache.fineract.client.feign.services;
20+
21+
import feign.Headers;
22+
import feign.Param;
23+
import feign.RequestLine;
24+
25+
/**
26+
* Internal testing API for Working Capital Loans. These endpoints are only available when the TEST profile is active.
27+
*/
28+
public interface InternalWorkingCapitalLoansApi {
29+
30+
@RequestLine("POST v1/internal/working-capital-loans/{loanId}/activate?disbursementDate={disbursementDate}")
31+
@Headers("Content-Type: application/json")
32+
void activateLoan(@Param("loanId") Long loanId, @Param("disbursementDate") String disbursementDate);
33+
34+
@RequestLine("POST v1/internal/working-capital-loans/{loanId}/generate-next-delinquency-period?businessDate={businessDate}")
35+
@Headers("Content-Type: application/json")
36+
void generateNextDelinquencyPeriod(@Param("loanId") Long loanId, @Param("businessDate") String businessDate);
37+
}

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,15 @@ public CommandWrapperBuilder undoWorkingCapitalLoanApplicationDisbursal(final Lo
605605
return this;
606606
}
607607

608+
public CommandWrapperBuilder createWorkingCapitalLoanDelinquencyAction(final Long workingCapitalLoanId) {
609+
this.actionName = "CREATE";
610+
this.entityName = "WC_DELINQUENCY_ACTION";
611+
this.entityId = workingCapitalLoanId;
612+
this.loanId = workingCapitalLoanId;
613+
this.href = "/working-capital-loans/" + workingCapitalLoanId + "/delinquency-actions";
614+
return this;
615+
}
616+
608617
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
609618
this.actionName = "CREATE";
610619
this.entityName = "CLIENTIDENTIFIER";

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.math.BigDecimal;
2222
import java.time.format.DateTimeFormatter;
2323
import lombok.RequiredArgsConstructor;
24+
import org.apache.fineract.client.models.PostWorkingCapitalLoansDelinquencyActionRequest;
2425
import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest;
2526
import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest;
2627
import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRequest;
@@ -102,4 +103,13 @@ public PostWorkingCapitalLoansLoanIdRequest defaultWorkingCapitalLoanUndoDisburs
102103
.dateFormat(DATE_FORMAT)//
103104
.locale(DEFAULT_LOCALE);//
104105
}
106+
107+
public PostWorkingCapitalLoansDelinquencyActionRequest defaultWorkingCapitalLoansDelinquencyActionRequest(String action) {
108+
return new PostWorkingCapitalLoansDelinquencyActionRequest()//
109+
.action(action)//
110+
.startDate(DATE_SUBMIT_STRING)//
111+
.endDate(DATE_SUBMIT_STRING)//
112+
.dateFormat(DATE_FORMAT)//
113+
.locale(DEFAULT_LOCALE);//
114+
}
105115
}

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

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,26 @@
1818
*/
1919
package org.apache.fineract.test.stepdef.loan;
2020

21+
import static org.apache.fineract.client.feign.util.FeignCalls.fail;
2122
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
2223
import static org.assertj.core.api.Assertions.assertThat;
2324

2425
import io.cucumber.datatable.DataTable;
2526
import io.cucumber.java.en.Then;
27+
import io.cucumber.java.en.When;
2628
import java.math.BigDecimal;
2729
import java.time.LocalDate;
2830
import java.util.List;
2931
import lombok.RequiredArgsConstructor;
3032
import lombok.extern.slf4j.Slf4j;
3133
import org.apache.fineract.client.feign.FineractFeignClient;
34+
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
35+
import org.apache.fineract.client.models.PostWorkingCapitalLoansDelinquencyActionRequest;
36+
import org.apache.fineract.client.models.PostWorkingCapitalLoansDelinquencyActionResponse;
3237
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
38+
import org.apache.fineract.client.models.WorkingCapitalLoanDelinquencyActionData;
3339
import org.apache.fineract.client.models.WorkingCapitalLoanDelinquencyRangeScheduleData;
40+
import org.apache.fineract.test.factory.WorkingCapitalLoanRequestFactory;
3441
import org.apache.fineract.test.stepdef.AbstractStepDef;
3542
import org.apache.fineract.test.support.TestContextKey;
3643

@@ -39,6 +46,92 @@
3946
public class WorkingCapitalDelinquencyStepDef extends AbstractStepDef {
4047

4148
private final FineractFeignClient fineractClient;
49+
private final WorkingCapitalLoanRequestFactory workingCapitalLoanRequestFactory;
50+
51+
@When("Admin initiate a Working Capital loan delinquency pause with startDate {string} and endDate {string}")
52+
public void initiateDelinquencyPause(String startDate, String endDate) {
53+
Long loanId = extractLoanId();
54+
PostWorkingCapitalLoansDelinquencyActionRequest request = buildDelinquencyActionRequest("pause", startDate, endDate);
55+
PostWorkingCapitalLoansDelinquencyActionResponse response = createDelinquencyActionById(loanId, request);
56+
57+
log.debug("Delinquency pause initiated for loan {} with startDate: {}, endDate: {}, response: {}", loanId, startDate, endDate,
58+
response);
59+
}
60+
61+
@When("Admin initiate a Working Capital loan delinquency pause by external ID with startDate {string} and endDate {string}")
62+
public void initiateDelinquencyPauseByExternalId(String startDate, String endDate) {
63+
String loanExternalId = extractLoanExternalId();
64+
PostWorkingCapitalLoansDelinquencyActionRequest request = buildDelinquencyActionRequest("pause", startDate, endDate);
65+
PostWorkingCapitalLoansDelinquencyActionResponse response = createDelinquencyActionByExternalId(loanExternalId, request);
66+
67+
log.debug("Delinquency pause initiated for loan externalId {} with startDate: {}, endDate: {}, response: {}", loanExternalId,
68+
startDate, endDate, response);
69+
}
70+
71+
@Then("Initiating a Working Capital loan delinquency pause with startDate {string} and endDate {string} results an error")
72+
public void initiateDelinquencyPauseResultsAnError(String startDate, String endDate) {
73+
initiateDelinquencyPauseResultsAnErrorWithDetails(startDate, endDate, null);
74+
}
75+
76+
@Then("Initiating a Working Capital loan delinquency pause with startDate {string} and endDate {string} results an error with the following data:")
77+
public void initiateDelinquencyPauseResultsAnErrorWithDetails(String startDate, String endDate, DataTable table) {
78+
Long loanId = extractLoanId();
79+
80+
PostWorkingCapitalLoansDelinquencyActionRequest request = workingCapitalLoanRequestFactory
81+
.defaultWorkingCapitalLoansDelinquencyActionRequest("pause").startDate(startDate).endDate(endDate);
82+
83+
CallFailedRuntimeException exception = fail(
84+
() -> fineractClient.workingCapitalLoanDelinquencyActions().createDelinquencyAction(loanId, request));
85+
86+
if (table != null) {
87+
verifyDelinquencyPauseErrorWithTable(exception, table);
88+
} else {
89+
// Default error for backward compatibility
90+
verifyDelinquencyPauseError(exception, 400,
91+
"Pause start date cannot fall within or before an already evaluated delinquency range period");
92+
}
93+
94+
log.info("Verified delinquency pause initiation failed with expected error for loan {}", loanId);
95+
}
96+
97+
@Then("Working Capital loan delinquency action has the following data:")
98+
public void verifyDelinquencyAction(DataTable dataTable) {
99+
Long loanId = extractLoanId();
100+
List<WorkingCapitalLoanDelinquencyActionData> actualActions = retrieveDelinquencyActions(loanId);
101+
verifyDelinquencyActionsWithTable(actualActions, dataTable);
102+
}
103+
104+
@Then("Working Capital loan delinquency action by external ID has the following data:")
105+
public void verifyDelinquencyActionByExternalId(DataTable dataTable) {
106+
String loanExternalId = extractLoanExternalId();
107+
List<WorkingCapitalLoanDelinquencyActionData> actualActions = retrieveDelinquencyActionsByExternalId(loanExternalId);
108+
verifyDelinquencyActionsWithTable(actualActions, dataTable);
109+
}
110+
111+
private void verifyDelinquencyActionsWithTable(List<WorkingCapitalLoanDelinquencyActionData> actualActions, DataTable dataTable) {
112+
assertThat(actualActions).as("Delinquency actions should not be empty").isNotEmpty();
113+
114+
List<List<String>> rows = dataTable.asLists();
115+
List<String> headers = rows.get(0);
116+
List<List<String>> expectedData = rows.subList(1, rows.size());
117+
118+
verifyDelinquencyActionsSize(actualActions, expectedData.size());
119+
verifyAllDelinquencyActionFields(actualActions, headers, expectedData);
120+
121+
log.info("Successfully verified {} delinquency action(s)", actualActions.size());
122+
}
123+
124+
private void verifyDelinquencyActionField(WorkingCapitalLoanDelinquencyActionData actual, String fieldName, String expectedValue,
125+
int rowNumber) {
126+
switch (fieldName) {
127+
case "action" -> assertThat(actual.getAction().name()).as("Action for row %d", rowNumber).isEqualTo(expectedValue);
128+
case "startDate" ->
129+
assertThat(actual.getStartDate()).as("Start date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
130+
case "endDate" ->
131+
assertThat(actual.getEndDate()).as("End date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
132+
default -> throw new IllegalArgumentException("Unknown field name: " + fieldName);
133+
}
134+
}
42135

43136
@Then("Working Capital loan delinquency range schedule has no data on a not yet disbursed loan")
44137
public void verifyRangeScheduleIsEmpty() {
@@ -76,6 +169,44 @@ private Long extractLoanId() {
76169
return loanResponse.getLoanId();
77170
}
78171

172+
private String extractLoanExternalId() {
173+
Long loanId = extractLoanId();
174+
return retrieveLoanExternalId(loanId);
175+
}
176+
177+
private String retrieveLoanExternalId(Long loanId) {
178+
return ok(() -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)).getExternalId();
179+
}
180+
181+
private PostWorkingCapitalLoansDelinquencyActionRequest buildDelinquencyActionRequest(String action, String startDate, String endDate) {
182+
return workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansDelinquencyActionRequest(action).startDate(startDate)
183+
.endDate(endDate);
184+
}
185+
186+
private PostWorkingCapitalLoansDelinquencyActionResponse createDelinquencyActionById(Long loanId,
187+
PostWorkingCapitalLoansDelinquencyActionRequest request) {
188+
return ok(() -> fineractClient.workingCapitalLoanDelinquencyActions().createDelinquencyAction(loanId, request));
189+
}
190+
191+
private PostWorkingCapitalLoansDelinquencyActionResponse createDelinquencyActionByExternalId(String loanExternalId,
192+
PostWorkingCapitalLoansDelinquencyActionRequest request) {
193+
return ok(() -> fineractClient.workingCapitalLoanDelinquencyActions().createDelinquencyActionByExternalId(loanExternalId, request));
194+
}
195+
196+
private List<WorkingCapitalLoanDelinquencyActionData> retrieveDelinquencyActions(Long loanId) {
197+
List<WorkingCapitalLoanDelinquencyActionData> actions = ok(
198+
() -> fineractClient.workingCapitalLoanDelinquencyActions().retrieveDelinquencyActions(loanId));
199+
log.debug("Delinquency actions for loan {}: {}", loanId, actions);
200+
return actions;
201+
}
202+
203+
private List<WorkingCapitalLoanDelinquencyActionData> retrieveDelinquencyActionsByExternalId(String loanExternalId) {
204+
List<WorkingCapitalLoanDelinquencyActionData> actions = ok(
205+
() -> fineractClient.workingCapitalLoanDelinquencyActions().retrieveDelinquencyActionsByExternalId(loanExternalId));
206+
log.debug("Delinquency actions for loan externalId {}: {}", loanExternalId, actions);
207+
return actions;
208+
}
209+
79210
private List<WorkingCapitalLoanDelinquencyRangeScheduleData> retrieveRangeSchedule(Long loanId) {
80211
List<WorkingCapitalLoanDelinquencyRangeScheduleData> rangeSchedule = ok(
81212
() -> fineractClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId));
@@ -87,6 +218,24 @@ private void verifyRangeScheduleSize(List<WorkingCapitalLoanDelinquencyRangeSche
87218
assertThat(actualRangeSchedule).as("Range schedule size should match expected data").hasSize(expectedSize);
88219
}
89220

221+
private void verifyDelinquencyActionsSize(List<WorkingCapitalLoanDelinquencyActionData> actualActions, int expectedSize) {
222+
assertThat(actualActions).as("Delinquency actions size should match expected data").hasSize(expectedSize);
223+
}
224+
225+
private void verifyAllDelinquencyActionFields(List<WorkingCapitalLoanDelinquencyActionData> actualActions, List<String> headers,
226+
List<List<String>> expectedData) {
227+
for (int i = 0; i < expectedData.size(); i++) {
228+
List<String> expectedRow = expectedData.get(i);
229+
WorkingCapitalLoanDelinquencyActionData actualAction = actualActions.get(i);
230+
231+
for (int j = 0; j < headers.size(); j++) {
232+
String header = headers.get(j);
233+
String expectedValue = expectedRow.get(j);
234+
verifyDelinquencyActionField(actualAction, header, expectedValue, i + 1);
235+
}
236+
}
237+
}
238+
90239
private void verifyAllRangeScheduleFields(List<WorkingCapitalLoanDelinquencyRangeScheduleData> actualRangeSchedule,
91240
List<String> headers, List<List<String>> expectedData) {
92241
for (int i = 0; i < expectedData.size(); i++) {
@@ -148,4 +297,23 @@ private void verifyNullableLong(Long actualValue, String expectedValue, String f
148297
}
149298
}
150299

300+
private void verifyDelinquencyPauseError(CallFailedRuntimeException exception, int expectedHttpCode, String expectedErrorMessage) {
301+
log.info("Checking for Http code: {} and error message: \"{}\"", expectedHttpCode, expectedErrorMessage);
302+
303+
assertThat(exception.getStatus()).as("HTTP status code should be " + expectedHttpCode).isEqualTo(expectedHttpCode);
304+
assertThat(exception.getMessage()).as("Should contain error message").contains(expectedErrorMessage);
305+
}
306+
307+
private void verifyDelinquencyPauseErrorWithTable(CallFailedRuntimeException exception, DataTable table) {
308+
List<List<String>> data = table.asLists();
309+
String expectedHttpCode = data.get(1).get(0);
310+
String expectedErrorMessage = data.get(1).get(1);
311+
312+
log.info("Checking for Http code: {} and error message: \"{}\"", expectedHttpCode, expectedErrorMessage);
313+
314+
assertThat(exception.getStatus()).as("HTTP status code should be " + expectedHttpCode)
315+
.isEqualTo(Integer.parseInt(expectedHttpCode));
316+
assertThat(exception.getMessage()).as("Should contain error message").contains(expectedErrorMessage);
317+
}
318+
151319
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767
@Slf4j
6868
@RequiredArgsConstructor
69-
public class WorkingCapitalProductLoanAccountStepDef extends AbstractStepDef {
69+
public class WorkingCapitalLoanAccountStepDef extends AbstractStepDef {
7070

7171
private static final String DATE_FORMAT = "dd MMMM yyyy";
7272
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);

0 commit comments

Comments
 (0)