Skip to content

Commit d837f06

Browse files
FINERACT-2455: Add working capital breach schedule
1 parent 7f3d40d commit d837f06

18 files changed

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

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
import org.apache.fineract.test.factory.WorkingCapitalRequestFactory;
6464
import org.apache.fineract.test.helper.ErrorMessageHelper;
6565
import org.apache.fineract.test.helper.Utils;
66-
import org.apache.fineract.test.messaging.event.EventCheckHelper;
6766
import org.apache.fineract.test.stepdef.AbstractStepDef;
6867
import org.apache.fineract.test.support.TestContextKey;
6968

@@ -79,14 +78,42 @@ public class WorkingCapitalLoanAccountStepDef extends AbstractStepDef {
7978
private final WorkingCapitalLoanProductResolver workingCapitalLoanProductResolver;
8079
private final WorkingCapitalLoanRequestFactory workingCapitalLoanRequestFactory;
8180
private final WorkingCapitalRequestFactory workingCapitalProductRequestFactory;
82-
private final EventCheckHelper eventCheckHelper;
8381

8482
@When("Admin creates a working capital loan with the following data:")
8583
public void createWorkingCapitalLoan(final DataTable table) {
8684
final List<List<String>> data = table.asLists();
8785
createWorkingCapitalLoanAccount(data.get(1));
8886
}
8987

88+
@When("Admin creates a working capital loan using created product with the following data:")
89+
public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) {
90+
final List<List<String>> data = table.asLists();
91+
final List<String> rawData = data.get(1);
92+
final Long clientId = extractClientId();
93+
final PostWorkingCapitalLoanProductsResponse productResponse = testContext()
94+
.get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE);
95+
final Long loanProductId = productResponse.getResourceId();
96+
97+
final String submittedOnDate = rawData.get(0);
98+
final String expectedDisbursementDate = rawData.get(1);
99+
final String principal = rawData.get(2);
100+
final String totalPayment = rawData.get(3);
101+
final String periodPaymentRate = rawData.get(4);
102+
final String discount = rawData.get(5);
103+
104+
final PostWorkingCapitalLoansRequest loansRequest = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId)
105+
.productId(loanProductId).submittedOnDate(submittedOnDate).expectedDisbursementDate(expectedDisbursementDate)
106+
.principalAmount(new BigDecimal(principal)).totalPayment(new BigDecimal(totalPayment))
107+
.periodPaymentRate(new BigDecimal(periodPaymentRate))
108+
.discount(discount != null && !discount.isEmpty() ? new BigDecimal(discount) : null);
109+
testContext().set(TestContextKey.LOAN_CREATE_REQUEST, loansRequest);
110+
111+
final PostWorkingCapitalLoansResponse response = ok(
112+
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
113+
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
114+
log.info("Working Capital Loan created with dynamic product ID: {}, Loan ID: {}", loanProductId, response.getLoanId());
115+
}
116+
90117
@Then("Working capital loan creation was successful")
91118
public void verifyWorkingCapitalLoanCreationSuccess() {
92119
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest;
4747
import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdResponse;
4848
import org.apache.fineract.client.models.StringEnumOptionData;
49+
import org.apache.fineract.client.models.WorkingCapitalBreachRequest;
4950
import org.apache.fineract.test.data.workingcapitalproduct.DefaultWorkingCapitalLoanProduct;
5051
import org.apache.fineract.test.factory.LoanProductsRequestFactory;
5152
import org.apache.fineract.test.factory.WorkingCapitalRequestFactory;
@@ -125,6 +126,54 @@ public void createWorkingCapitalLoanProductWithBreachId() {
125126
checkWorkingCapitalLoanProductCreate();
126127
}
127128

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

0 commit comments

Comments
 (0)