Skip to content

Commit 8b82c61

Browse files
peter-kovacs-dpcbudaidev
authored andcommitted
FINERACT-2455: Working capital delinquency management range schedule - E2E tests
1 parent 58137df commit 8b82c61

6 files changed

Lines changed: 430 additions & 11 deletions

File tree

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DATE_FORMAT;
2525
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DAYS_IN_MONTH_TYPE_30;
2626
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DAYS_IN_YEAR_TYPE_360;
27-
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.DELINQUENCY_BUCKET_ID;
2827
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.FUND_ID;
2928
import static org.apache.fineract.test.factory.LoanProductsRequestFactory.LOCALE_EN;
3029

3130
import java.math.BigDecimal;
3231
import java.util.ArrayList;
3332
import java.util.List;
33+
import java.util.Map;
3434
import lombok.RequiredArgsConstructor;
35+
import org.apache.fineract.client.feign.FineractFeignClient;
3536
import org.apache.fineract.client.models.DelinquencyBucketRequest;
37+
import org.apache.fineract.client.models.DelinquencyBucketResponse;
3638
import org.apache.fineract.client.models.MinimumPaymentPeriodAndRule;
3739
import org.apache.fineract.client.models.PaymentAllocationOrder;
3840
import org.apache.fineract.client.models.PostAllowAttributeOverrides;
@@ -50,9 +52,11 @@
5052
public class WorkingCapitalRequestFactory {
5153

5254
private final LoanProductsRequestFactory loanProductsRequestFactory;
55+
private final FineractFeignClient fineractClient;
5356

5457
public static final String WCLP_NAME_PREFIX = "WCLP-";
5558
public static final String WCLP_DESCRIPTION = "Working Capital Loan Product";
59+
public static final String DEFAULT_WC_DELINQUENCY_BUCKET_NAME = "Default Working Capital delinquency bucket";
5660
public static final String PENALTY = "PENALTY";
5761
public static final String FEE = "FEE";
5862
public static final String PRINCIPAL = "PRINCIPAL";
@@ -79,7 +83,7 @@ public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductReq
7983
.maxPrincipal(new BigDecimal(100000))//
8084
.amortizationType(PostWorkingCapitalLoanProductsRequest.AmortizationTypeEnum.EIR)//
8185
.npvDayCount(DAYS_IN_YEAR_TYPE_360)//
82-
.delinquencyBucketId(DELINQUENCY_BUCKET_ID.longValue())//
86+
.delinquencyBucketId(getWCDelinquencyBucketIdByName(DEFAULT_WC_DELINQUENCY_BUCKET_NAME))//
8387
.dateFormat(DATE_FORMAT)//
8488
.locale(LOCALE_EN)//
8589
.paymentAllocation(List.of(//
@@ -173,4 +177,14 @@ public DelinquencyBucketRequest defaultWorkingCapitalDelinquencyBucketRequest()
173177
.minimumPayment(new BigDecimal("1.23")));
174178
}
175179

180+
private Long getWCDelinquencyBucketIdByName(String bucketName) {
181+
try {
182+
List<DelinquencyBucketResponse> buckets = fineractClient.delinquencyRangeAndBucketsManagement().getBuckets(Map.of());
183+
return buckets.stream().filter(b -> bucketName.equals(b.getName())).findFirst().map(DelinquencyBucketResponse::getId)
184+
.orElseThrow(() -> new RuntimeException("Working Capital delinquency bucket not found with name: " + bucketName));
185+
} catch (Exception e) {
186+
throw new RuntimeException("Failed to fetch Working Capital delinquency bucket by name: " + bucketName, e);
187+
}
188+
}
189+
176190
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/WorkingCapitalLoanCobStepDef.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@
4444
import org.apache.fineract.client.models.OldestCOBProcessedLoanDTO;
4545
import org.apache.fineract.client.models.PostClientsResponse;
4646
import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse;
47+
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
4748
import org.apache.fineract.test.data.LoanStatus;
4849
import org.apache.fineract.test.helper.BusinessDateHelper;
4950
import org.apache.fineract.test.helper.WorkingCapitalLoanTestHelper;
5051
import org.apache.fineract.test.messaging.config.JobPollingProperties;
5152
import org.apache.fineract.test.stepdef.AbstractStepDef;
5253
import org.apache.fineract.test.support.TestContextKey;
54+
import org.junit.jupiter.api.Assertions;
5355
import org.springframework.beans.factory.annotation.Autowired;
5456

5557
@Slf4j
@@ -93,6 +95,17 @@ public void runWorkingCapitalInlineCOB() throws IOException {
9395
ok(() -> fineractClient.inlineJob().executeInlineJob("WC_LOAN_COB", inlineJobRequest));
9496
}
9597

98+
@When("Admin runs inline COB job for Working Capital Loan by loanId")
99+
public void runWorkingCapitalInlineCOBByLoanId() throws IOException {
100+
PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
101+
Assertions.assertNotNull(loanResponse);
102+
long loanId = loanResponse.getLoanId();
103+
104+
InlineJobRequest inlineJobRequest = new InlineJobRequest().addLoanIdsItem(loanId);
105+
106+
ok(() -> fineractClient.inlineJob().executeInlineJob("WC_LOAN_COB", inlineJobRequest));
107+
}
108+
96109
@When("Admin runs inline COB job for all Working Capital Loans")
97110
public void runWorkingCapitalInlineCOBForAll() throws IOException {
98111
InlineJobRequest inlineJobRequest = new InlineJobRequest();
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.WorkingCapitalLoanDelinquencyRangeScheduleData;
34+
import org.apache.fineract.test.stepdef.AbstractStepDef;
35+
import org.apache.fineract.test.support.TestContextKey;
36+
37+
@Slf4j
38+
@RequiredArgsConstructor
39+
public class WorkingCapitalDelinquencyStepDef extends AbstractStepDef {
40+
41+
private final FineractFeignClient fineractClient;
42+
43+
@Then("Working Capital loan delinquency range schedule has no data on a not yet disbursed loan")
44+
public void verifyRangeScheduleIsEmpty() {
45+
Long loanId = extractLoanId();
46+
List<WorkingCapitalLoanDelinquencyRangeScheduleData> actualRangeSchedule = retrieveRangeSchedule(loanId);
47+
48+
assertThat(actualRangeSchedule).as("Range schedule should be empty when loan is not yet disbursed").isEmpty();
49+
50+
log.info("Verified that loan {} has no delinquency range schedule on a not yet disbursed loan", loanId);
51+
}
52+
53+
@Then("Working Capital loan delinquency range schedule has the following data:")
54+
public void verifyRangeSchedule(DataTable dataTable) {
55+
Long loanId = extractLoanId();
56+
List<WorkingCapitalLoanDelinquencyRangeScheduleData> actualRangeSchedule = retrieveRangeSchedule(loanId);
57+
58+
// If no data rows provided (only header), just log and return
59+
if (dataTable.height() <= 1) {
60+
log.info("No expected data provided for verification, skipping validation");
61+
return;
62+
}
63+
64+
List<List<String>> rows = dataTable.asLists();
65+
List<String> headers = rows.get(0);
66+
List<List<String>> expectedData = rows.subList(1, rows.size());
67+
68+
verifyRangeScheduleSize(actualRangeSchedule, expectedData.size());
69+
verifyAllRangeScheduleFields(actualRangeSchedule, headers, expectedData);
70+
71+
log.info("Successfully verified {} range schedule entries", actualRangeSchedule.size());
72+
}
73+
74+
private Long extractLoanId() {
75+
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
76+
return loanResponse.getLoanId();
77+
}
78+
79+
private List<WorkingCapitalLoanDelinquencyRangeScheduleData> retrieveRangeSchedule(Long loanId) {
80+
List<WorkingCapitalLoanDelinquencyRangeScheduleData> rangeSchedule = ok(
81+
() -> fineractClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId));
82+
log.debug("Delinquency Range Schedule for loan {}: {}", loanId, rangeSchedule);
83+
return rangeSchedule;
84+
}
85+
86+
private void verifyRangeScheduleSize(List<WorkingCapitalLoanDelinquencyRangeScheduleData> actualRangeSchedule, int expectedSize) {
87+
assertThat(actualRangeSchedule).as("Range schedule size should match expected data").hasSize(expectedSize);
88+
}
89+
90+
private void verifyAllRangeScheduleFields(List<WorkingCapitalLoanDelinquencyRangeScheduleData> actualRangeSchedule,
91+
List<String> headers, List<List<String>> expectedData) {
92+
for (int i = 0; i < expectedData.size(); i++) {
93+
List<String> expectedRow = expectedData.get(i);
94+
WorkingCapitalLoanDelinquencyRangeScheduleData actualRow = actualRangeSchedule.get(i);
95+
96+
for (int j = 0; j < headers.size(); j++) {
97+
String header = headers.get(j);
98+
String expectedValue = expectedRow.get(j);
99+
verifyRangeScheduleField(actualRow, header, expectedValue, i + 1);
100+
}
101+
}
102+
}
103+
104+
private void verifyRangeScheduleField(WorkingCapitalLoanDelinquencyRangeScheduleData actual, String fieldName, String expectedValue,
105+
int rowNumber) {
106+
switch (fieldName) {
107+
case "periodNumber" ->
108+
assertThat(actual.getPeriodNumber()).as("Period number for row %d", rowNumber).isEqualTo(Integer.parseInt(expectedValue));
109+
case "fromDate" ->
110+
assertThat(actual.getFromDate()).as("From date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
111+
case "toDate" -> assertThat(actual.getToDate()).as("To date for row %d", rowNumber).isEqualTo(LocalDate.parse(expectedValue));
112+
case "expectedAmount" -> assertThat(actual.getExpectedAmount()).as("Expected amount for row %d", rowNumber)
113+
.isEqualByComparingTo(new BigDecimal(expectedValue));
114+
case "paidAmount" -> assertThat(actual.getPaidAmount()).as("Paid amount for row %d", rowNumber)
115+
.isEqualByComparingTo(new BigDecimal(expectedValue));
116+
case "outstandingAmount" -> assertThat(actual.getOutstandingAmount()).as("Outstanding amount for row %d", rowNumber)
117+
.isEqualByComparingTo(new BigDecimal(expectedValue));
118+
case "minPaymentCriteriaMet" ->
119+
verifyNullableBoolean(actual.getMinPaymentCriteriaMet(), expectedValue, "Min payment criteria met", rowNumber);
120+
case "delinquentAmount" ->
121+
verifyNullableBigDecimal(actual.getDelinquentAmount(), expectedValue, "Delinquent amount", rowNumber);
122+
case "delinquentDays" -> verifyNullableLong(actual.getDelinquentDays(), expectedValue, "Delinquent days", rowNumber);
123+
default -> throw new IllegalArgumentException("Unknown field name: " + fieldName);
124+
}
125+
}
126+
127+
private void verifyNullableBoolean(Boolean actualValue, String expectedValue, String fieldDescription, int rowNumber) {
128+
if ("null".equals(expectedValue)) {
129+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isNull();
130+
} else {
131+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isEqualTo(Boolean.parseBoolean(expectedValue));
132+
}
133+
}
134+
135+
private void verifyNullableBigDecimal(BigDecimal actualValue, String expectedValue, String fieldDescription, 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).isEqualByComparingTo(new BigDecimal(expectedValue));
140+
}
141+
}
142+
143+
private void verifyNullableLong(Long actualValue, String expectedValue, String fieldDescription, int rowNumber) {
144+
if ("null".equals(expectedValue)) {
145+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isNull();
146+
} else {
147+
assertThat(actualValue).as("%s for row %d", fieldDescription, rowNumber).isEqualTo(Long.parseLong(expectedValue));
148+
}
149+
}
150+
151+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -755,11 +755,14 @@ private PostWorkingCapitalLoansRequest buildCreateLoanRequest(final Long clientI
755755
final String periodPaymentRate = loanData.get(5);
756756
final String discount = loanData.get(6);
757757

758-
return workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId).productId(productId)
759-
.submittedOnDate(submittedOnDate).expectedDisbursementDate(expectedDisbursementDate)
760-
.principalAmount(new BigDecimal(principal)).totalPayment(new BigDecimal(totalPayment))
761-
.periodPaymentRate(new BigDecimal(periodPaymentRate))
762-
.discount(discount != null && !discount.isEmpty() ? new BigDecimal(discount) : null);
758+
return workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId)//
759+
.productId(productId)//
760+
.submittedOnDate(submittedOnDate)//
761+
.expectedDisbursementDate(expectedDisbursementDate)//
762+
.principalAmount(new BigDecimal(principal))//
763+
.totalPayment(new BigDecimal(totalPayment))//
764+
.periodPaymentRate(new BigDecimal(periodPaymentRate))//
765+
.discount(discount != null && !discount.isEmpty() ? new BigDecimal(discount) : null);//
763766
}
764767

765768
private PutWorkingCapitalLoansLoanIdRequest buildModifyLoanRequest(final List<String> loanData) {

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ public class DelinquencyGlobalInitializerStep implements FineractGlobalInitializ
5656
private final FineractFeignClient fineractClient;
5757

5858
private final List<Long> createdRangeIds = new ArrayList<>();
59+
private final List<Long> createdWCRangeIds = new ArrayList<>();
5960

6061
@Override
6162
public void initialize() {
6263
setDefaultDelinquencyRanges();
6364
setDefaultDelinquencyBucket();
65+
setDefaultWCDelinquencyRanges();
6466
setDefaultWCDelinquencyBucket();
6567
}
6668

@@ -120,6 +122,58 @@ public void setDefaultDelinquencyRanges() {
120122
createdRangeIds.add(lastResponse.getResourceId());
121123
}
122124

125+
public void setDefaultWCDelinquencyRanges() {
126+
List<DelinquencyRangeResponse> existingRanges;
127+
try {
128+
existingRanges = fineractClient.delinquencyRangeAndBucketsManagement().getRanges(Map.of());
129+
} catch (Exception e) {
130+
log.debug("Could not retrieve existing delinquency ranges for WCLP, will create them", e);
131+
existingRanges = new ArrayList<>();
132+
}
133+
134+
List<RangeDefinition> wclpRanges = Arrays.asList(new RangeDefinition("D00", 1, 30), new RangeDefinition("D30", 31, 60),
135+
new RangeDefinition("D60", 61, 90), new RangeDefinition("D90", 91, 120), new RangeDefinition("D120", 121, 150),
136+
new RangeDefinition("D150", 151, 180), new RangeDefinition("D180", 181, 210), new RangeDefinition("D210", 211, 240),
137+
new RangeDefinition("D240", 241, 270), new RangeDefinition("D270", 271, null));
138+
139+
for (RangeDefinition rangeDef : wclpRanges) {
140+
String classification = rangeDef.name;
141+
142+
DelinquencyRangeResponse existingRange = existingRanges.stream().filter(r -> classification.equals(r.getClassification()))
143+
.findFirst().orElse(null);
144+
145+
if (existingRange != null) {
146+
createdWCRangeIds.add(existingRange.getId());
147+
continue;
148+
}
149+
150+
DelinquencyRangeRequest rangeRequest = new DelinquencyRangeRequest();
151+
rangeRequest.classification(classification);
152+
rangeRequest.locale(DEFAULT_LOCALE);
153+
rangeRequest.minimumAgeDays(rangeDef.minDays);
154+
rangeRequest.maximumAgeDays(rangeDef.maxDays);
155+
156+
PostDelinquencyRangeResponse response = ok(
157+
() -> fineractClient.delinquencyRangeAndBucketsManagement().createRange(rangeRequest, Map.of()));
158+
createdWCRangeIds.add(response.getResourceId());
159+
}
160+
161+
log.debug("Created WCLP delinquency ranges with IDs: {}", createdWCRangeIds);
162+
}
163+
164+
private static class RangeDefinition {
165+
166+
String name;
167+
Integer minDays;
168+
Integer maxDays;
169+
170+
RangeDefinition(String name, Integer minDays, Integer maxDays) {
171+
this.name = name;
172+
this.minDays = minDays;
173+
this.maxDays = maxDays;
174+
}
175+
}
176+
123177
public void setDefaultDelinquencyBucket() {
124178
try {
125179
List<DelinquencyBucketResponse> existingBuckets = fineractClient.delinquencyRangeAndBucketsManagement().getBuckets(Map.of());
@@ -153,12 +207,12 @@ public void setDefaultWCDelinquencyBucket() {
153207

154208
DelinquencyBucketRequest postDelinquencyBucketWCRequest = new DelinquencyBucketRequest().name(DEFAULT_WC_DELINQUENCY_BUCKET_NAME)
155209
.bucketType(DelinquencyBucketType.WORKING_CAPITAL.name())//
156-
.ranges(List.of(1L)) //
210+
.ranges(createdWCRangeIds) //
157211
.minimumPaymentPeriodAndRule(new MinimumPaymentPeriodAndRule() //
158-
.frequency(1) //
212+
.frequency(30) //
213+
.frequencyType(DelinquencyFrequencyType.DAYS.name()) //
159214
.minimumPaymentType(DelinquencyMinimumPayment.PERCENTAGE.name()) //
160-
.frequencyType(DelinquencyFrequencyType.MONTHS.name()) //
161-
.minimumPayment(BigDecimal.valueOf(1.23))); //
215+
.minimumPayment(BigDecimal.valueOf(3.0))); //
162216

163217
executeVoid(() -> fineractClient.delinquencyRangeAndBucketsManagement().createBucket(postDelinquencyBucketWCRequest, Map.of()));
164218
}

0 commit comments

Comments
 (0)