Skip to content

Commit e489d55

Browse files
authored
Merge pull request #5743 from openMF/FINERACT-2455/wc-breach-management-schedule
FINERACT-2455: Add working capital breach schedule
2 parents 80dee22 + a119483 commit e489d55

21 files changed

Lines changed: 1332 additions & 4 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
import org.apache.fineract.client.feign.services.UserGeneratedDocumentsApi;
156156
import org.apache.fineract.client.feign.services.UsersApi;
157157
import org.apache.fineract.client.feign.services.WorkingCapitalBreachApi;
158+
import org.apache.fineract.client.feign.services.WorkingCapitalLoanBreachScheduleApi;
158159
import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi;
159160
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyActionsApi;
160161
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyRangeScheduleApi;
@@ -766,6 +767,10 @@ public WorkingCapitalLoanDelinquencyRangeScheduleApi workingCapitalLoanDelinquen
766767
return create(WorkingCapitalLoanDelinquencyRangeScheduleApi.class);
767768
}
768769

770+
public WorkingCapitalLoanBreachScheduleApi workingCapitalLoanBreachSchedule() {
771+
return create(WorkingCapitalLoanBreachScheduleApi.class);
772+
}
773+
769774
public InternalWorkingCapitalLoansApi internalWorkingCapitalLoans() {
770775
return create(InternalWorkingCapitalLoansApi.class);
771776
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,12 @@ public static String wrongErrorCode(Integer actual, Integer expected) {
662662
actual.toString(), expected.toString());
663663
}
664664

665+
public static String wrongStatusCodeInBreachScheduleRetrieval(Integer actual, Integer expected, Long loanId) {
666+
return String.format(
667+
"Not the expected HTTP status code for GET breach-schedule on loanId %d: Actual code is: %s. Expected code is: %s", loanId,
668+
actual.toString(), expected.toString());
669+
}
670+
665671
public static String idNull() {
666672
return "The requested ID is null";
667673
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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.test.stepdef.loan;
20+
21+
import static org.apache.fineract.client.feign.util.FeignCalls.fail;
22+
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
import io.cucumber.datatable.DataTable;
26+
import io.cucumber.java.en.Then;
27+
import java.math.BigDecimal;
28+
import java.time.LocalDate;
29+
import java.util.List;
30+
import lombok.RequiredArgsConstructor;
31+
import lombok.extern.slf4j.Slf4j;
32+
import org.apache.fineract.client.feign.FineractFeignClient;
33+
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
34+
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
35+
import org.apache.fineract.client.models.WorkingCapitalLoanBreachScheduleData;
36+
import org.apache.fineract.test.helper.ErrorMessageHelper;
37+
import org.apache.fineract.test.stepdef.AbstractStepDef;
38+
import org.apache.fineract.test.support.TestContextKey;
39+
40+
@Slf4j
41+
@RequiredArgsConstructor
42+
public class WorkingCapitalBreachScheduleStepDef extends AbstractStepDef {
43+
44+
private final FineractFeignClient fineractClient;
45+
46+
@Then("Working Capital loan breach schedule has no data")
47+
public void verifyBreachScheduleIsEmpty() {
48+
final Long loanId = extractLoanId();
49+
final List<WorkingCapitalLoanBreachScheduleData> schedule = retrieveBreachSchedule(loanId);
50+
51+
assertThat(schedule).as("Breach schedule should be empty").isEmpty();
52+
53+
log.info("Verified that loan {} has no breach schedule", loanId);
54+
}
55+
56+
@Then("Working Capital loan breach schedule has {int} period(s)")
57+
public void verifyBreachScheduleSize(final int expectedSize) {
58+
final Long loanId = extractLoanId();
59+
final List<WorkingCapitalLoanBreachScheduleData> schedule = retrieveBreachSchedule(loanId);
60+
61+
assertThat(schedule).as("Breach schedule size for loan %d", loanId).hasSize(expectedSize);
62+
63+
log.info("Verified that loan {} has {} breach schedule period(s)", loanId, expectedSize);
64+
}
65+
66+
@Then("Retrieving Working Capital loan breach schedule for non-existent loanId {long} fails with status code {int}")
67+
public void verifyBreachScheduleNotFound(final long loanId, final int expectedStatus) {
68+
final CallFailedRuntimeException exception = fail(
69+
() -> fineractClient.workingCapitalLoanBreachSchedule().retrieveBreachSchedule(loanId));
70+
71+
assertThat(exception.getStatus())
72+
.as(ErrorMessageHelper.wrongStatusCodeInBreachScheduleRetrieval(exception.getStatus(), expectedStatus, loanId))
73+
.isEqualTo(expectedStatus);
74+
75+
log.info("Verified that GET breach schedule for loanId {} fails with status {}", loanId, expectedStatus);
76+
}
77+
78+
@Then("Working Capital loan breach schedule has the following data:")
79+
public void verifyBreachSchedule(final DataTable dataTable) {
80+
final Long loanId = extractLoanId();
81+
final List<WorkingCapitalLoanBreachScheduleData> schedule = retrieveBreachSchedule(loanId);
82+
83+
final List<List<String>> rows = dataTable.asLists();
84+
final List<String> headers = rows.getFirst();
85+
final List<List<String>> expectedData = rows.subList(1, rows.size());
86+
87+
assertThat(schedule).as("Breach schedule size should match expected data").hasSize(expectedData.size());
88+
89+
for (int i = 0; i < expectedData.size(); i++) {
90+
final List<String> expectedRow = expectedData.get(i);
91+
final WorkingCapitalLoanBreachScheduleData actualRow = schedule.get(i);
92+
93+
for (int j = 0; j < headers.size(); j++) {
94+
final String header = headers.get(j);
95+
final String expectedValue = expectedRow.get(j);
96+
verifyField(actualRow, header, expectedValue, i + 1);
97+
}
98+
}
99+
100+
log.info("Successfully verified {} breach schedule entries for loan {}", schedule.size(), loanId);
101+
}
102+
103+
private Long extractLoanId() {
104+
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
105+
return loanResponse.getLoanId();
106+
}
107+
108+
private List<WorkingCapitalLoanBreachScheduleData> retrieveBreachSchedule(final Long loanId) {
109+
final List<WorkingCapitalLoanBreachScheduleData> schedule = ok(
110+
() -> fineractClient.workingCapitalLoanBreachSchedule().retrieveBreachSchedule(loanId));
111+
log.debug("Breach schedule for loan {}: {}", loanId, schedule);
112+
return schedule;
113+
}
114+
115+
private void verifyField(final WorkingCapitalLoanBreachScheduleData actual, final String fieldName, final String expectedValue,
116+
final int rowNumber) {
117+
switch (fieldName) {
118+
case "periodNumber" ->
119+
assertThat(actual.getPeriodNumber()).as("Period number for row %d", rowNumber).isEqualTo(Integer.parseInt(expectedValue));
120+
case "fromDate" ->
121+
assertThat(actual.getFromDate()).as("From date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
122+
case "toDate" -> assertThat(actual.getToDate()).as("To date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
123+
case "numberOfDays" -> verifyNullableInteger(actual.getNumberOfDays(), expectedValue, "Number of days", rowNumber);
124+
case "minPaymentAmount" ->
125+
verifyNullableBigDecimal(actual.getMinPaymentAmount(), expectedValue, "Min payment amount", rowNumber);
126+
case "outstandingAmount" ->
127+
verifyNullableBigDecimal(actual.getOutstandingAmount(), expectedValue, "Outstanding amount", rowNumber);
128+
case "nearBreach" -> verifyNullableBoolean(actual.getNearBreach(), expectedValue, "Near breach", rowNumber);
129+
case "breach" -> verifyNullableBoolean(actual.getBreach(), expectedValue, "Breach", rowNumber);
130+
default -> throw new IllegalArgumentException("Unknown field name: " + fieldName);
131+
}
132+
}
133+
134+
private void verifyNullableBoolean(final Boolean actualValue, final String expectedValue, final String fieldDescription,
135+
final int rowNumber) {
136+
if ("null".equals(expectedValue)) {
137+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isNull();
138+
} else {
139+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isEqualTo(Boolean.parseBoolean(expectedValue));
140+
}
141+
}
142+
143+
private void verifyNullableBigDecimal(final BigDecimal actualValue, final String expectedValue, final String fieldDescription,
144+
final int rowNumber) {
145+
if ("null".equals(expectedValue)) {
146+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isNull();
147+
} else {
148+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isEqualByComparingTo(new BigDecimal(expectedValue));
149+
}
150+
}
151+
152+
private void verifyNullableInteger(final Integer actualValue, final String expectedValue, final String fieldDescription,
153+
final int rowNumber) {
154+
if ("null".equals(expectedValue)) {
155+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isNull();
156+
} else {
157+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isEqualTo(Integer.parseInt(expectedValue));
158+
}
159+
}
160+
161+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@ public void createWorkingCapitalLoan(final DataTable table) {
9494
createWorkingCapitalLoanAccount(data.get(1));
9595
}
9696

97+
@When("Admin creates a working capital loan using created product with the following data:")
98+
public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) {
99+
final List<List<String>> data = table.asLists();
100+
final List<String> rawData = data.get(1);
101+
final Long clientId = extractClientId();
102+
final PostWorkingCapitalLoanProductsResponse productResponse = testContext()
103+
.get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE);
104+
final Long loanProductId = productResponse.getResourceId();
105+
106+
final String submittedOnDate = rawData.get(0);
107+
final String expectedDisbursementDate = rawData.get(1);
108+
final String principal = rawData.get(2);
109+
final String totalPayment = rawData.get(3);
110+
final String periodPaymentRate = rawData.get(4);
111+
final String discount = rawData.get(5);
112+
113+
final PostWorkingCapitalLoansRequest loansRequest = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId)
114+
.productId(loanProductId).submittedOnDate(submittedOnDate).expectedDisbursementDate(expectedDisbursementDate)
115+
.principalAmount(new BigDecimal(principal)).totalPayment(new BigDecimal(totalPayment))
116+
.periodPaymentRate(new BigDecimal(periodPaymentRate))
117+
.discount(discount != null && !discount.isEmpty() ? new BigDecimal(discount) : null);
118+
testContext().set(TestContextKey.LOAN_CREATE_REQUEST, loansRequest);
119+
120+
final PostWorkingCapitalLoansResponse response = ok(
121+
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
122+
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
123+
log.info("Working Capital Loan created with dynamic product ID: {}, Loan ID: {}", loanProductId, response.getLoanId());
124+
}
125+
97126
@Then("Working capital loan creation was successful")
98127
public void verifyWorkingCapitalLoanCreationSuccess() {
99128
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest;
5353
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdResponse;
5454
import org.apache.fineract.client.models.StringEnumOptionData;
55+
import org.apache.fineract.client.models.WorkingCapitalBreachRequest;
5556
import org.apache.fineract.test.data.accounttype.AccountTypeResolver;
5657
import org.apache.fineract.test.data.accounttype.DefaultAccountType;
5758
import org.apache.fineract.test.data.workingcapitalproduct.DefaultWorkingCapitalLoanProduct;
@@ -135,6 +136,55 @@ public void createWorkingCapitalLoanProductWithBreachId() {
135136
checkWorkingCapitalLoanProductCreate();
136137
}
137138

139+
@When("Admin creates a new Working Capital Loan Product with breachId and overrides enabled")
140+
public void createWorkingCapitalLoanProductWithBreachIdAndOverrides() {
141+
final CommandProcessingResult breachCreateResponse = ok(() -> fineractFeignClient.workingCapitalBreaches()
142+
.createWorkingCapitalBreach(workingCapitalRequestFactory.defaultWorkingCapitalBreachRequest()));
143+
final Long breachId = breachCreateResponse.getResourceId();
144+
testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachId);
145+
146+
final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10);
147+
final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory
148+
.defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() //
149+
.name(name) //
150+
.breachId(breachId);
151+
152+
final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request);
153+
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response);
154+
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request);
155+
checkWorkingCapitalLoanProductCreate();
156+
}
157+
158+
@When("Admin creates a Working Capital Loan Product with custom breach config and overrides enabled:")
159+
public void createWorkingCapitalLoanProductWithCustomBreachConfig(final DataTable table) {
160+
final Map<String, String> data = table.asMaps().get(0);
161+
162+
final String breachName = "WC Breach " + Utils.randomStringGenerator("", 10);
163+
final WorkingCapitalBreachRequest breachRequest = new WorkingCapitalBreachRequest().name(breachName)
164+
.breachFrequency(Integer.valueOf(data.get("breachFrequency"))).breachFrequencyType(data.get("breachFrequencyType"))
165+
.breachAmountCalculationType(data.get("breachAmountCalculationType"))
166+
.breachAmount(new BigDecimal(data.get("breachAmount")));
167+
final CommandProcessingResult breachCreateResponse = ok(
168+
() -> fineractFeignClient.workingCapitalBreaches().createWorkingCapitalBreach(breachRequest));
169+
final Long breachId = breachCreateResponse.getResourceId();
170+
testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachId);
171+
172+
final String graceDaysStr = data.get("delinquencyGraceDays");
173+
final Integer graceDays = graceDaysStr != null && !graceDaysStr.isEmpty() ? Integer.valueOf(graceDaysStr) : null;
174+
175+
final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10);
176+
final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory
177+
.defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() //
178+
.name(name) //
179+
.breachId(breachId) //
180+
.delinquencyGraceDays(graceDays);
181+
182+
final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request);
183+
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response);
184+
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request);
185+
checkWorkingCapitalLoanProductCreate();
186+
}
187+
138188
@When("Admin creates a new Working Capital Loan Product with external-id")
139189
public void createWorkingCapitalLoanProductWithExternalId() {
140190
final String workingCapitalProductDefaultName = DefaultWorkingCapitalLoanProduct.WCLP.getName()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Feature: Working Capital Breach Configuration
2222
| breachFrequency | "null" | The parameter `breachFrequency` is mandatory. |
2323
| breachFrequency | "0" | The parameter `breachFrequency` must be greater than 0. |
2424
| breachFrequencyType | "null" | The parameter `breachFrequencyType` is mandatory. |
25-
| breachFrequencyType | "INVALID" | The parameter `breachFrequencyType` must be one of [ DAYS, MONTHS, YEARS ] . |
25+
| breachFrequencyType | "INVALID" | The parameter `breachFrequencyType` must be one of [ DAYS, WEEKS, MONTHS, YEARS ] . |
2626
| breachAmountCalculationType | "null" | The parameter `breachAmountCalculationType` is mandatory. |
2727
| breachAmountCalculationType | "INVALID" | The parameter `breachAmountCalculationType` must be one of [ PERCENTAGE, FLAT ] . |
2828
| breachAmount | "null" | The parameter `breachAmount` is mandatory. |
@@ -42,7 +42,7 @@ Feature: Working Capital Breach Configuration
4242
| name | "null" | The parameter `name` is mandatory. |
4343
| name | "" | The parameter `name` is mandatory. |
4444
| breachFrequency | "0" | The parameter `breachFrequency` must be greater than 0. |
45-
| breachFrequencyType | "INVALID" | The parameter `breachFrequencyType` must be one of [ DAYS, MONTHS, YEARS ] . |
45+
| breachFrequencyType | "INVALID" | The parameter `breachFrequencyType` must be one of [ DAYS, WEEKS, MONTHS, YEARS ] . |
4646
| breachAmountCalculationType | "INVALID" | The parameter `breachAmountCalculationType` must be one of [ PERCENTAGE, FLAT ] . |
4747
| breachAmount | "-1" | The parameter `breachAmount` must be greater than or equal to 0. |
4848

0 commit comments

Comments
 (0)