Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import org.apache.fineract.client.feign.services.InterestRateChartApi;
import org.apache.fineract.client.feign.services.InterestRateSlabAKAInterestBandsApi;
import org.apache.fineract.client.feign.services.InternalCobApi;
import org.apache.fineract.client.feign.services.InternalWorkingCapitalLoansApi;
import org.apache.fineract.client.feign.services.JournalEntriesApi;
import org.apache.fineract.client.feign.services.LikelihoodApi;
import org.apache.fineract.client.feign.services.ListReportMailingJobHistoryApi;
Expand Down Expand Up @@ -154,6 +155,8 @@
import org.apache.fineract.client.feign.services.UserGeneratedDocumentsApi;
import org.apache.fineract.client.feign.services.UsersApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyActionsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyRangeScheduleApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanTransactionsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoansApi;
Expand Down Expand Up @@ -754,6 +757,18 @@ public WorkingCapitalLoanCobCatchUpApi workingCapitalLoanCobCatchUpApi() {
return create(WorkingCapitalLoanCobCatchUpApi.class);
}

public WorkingCapitalLoanDelinquencyActionsApi workingCapitalLoanDelinquencyActions() {
return create(WorkingCapitalLoanDelinquencyActionsApi.class);
}

public WorkingCapitalLoanDelinquencyRangeScheduleApi workingCapitalLoanDelinquencyRangeSchedule() {
return create(WorkingCapitalLoanDelinquencyRangeScheduleApi.class);
}

public InternalWorkingCapitalLoansApi internalWorkingCapitalLoans() {
return create(InternalWorkingCapitalLoansApi.class);
}

public WorkingCapitalLoansApi workingCapitalLoans() {
return create(WorkingCapitalLoansApi.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void encode(Object object, Type bodyType, RequestTemplate template) throw
encodeFileAsMultipart((File) object, template);
} else if (object instanceof String && template.headers().containsKey("Content-Type")) {
String contentType = template.headers().get("Content-Type").iterator().next();
if (contentType.startsWith("text/html") || contentType.startsWith("text/plain")) {
if (contentType.startsWith("text/html") || contentType.startsWith("text/plain") || contentType.startsWith("application/json")) {
byte[] bodyBytes = ((String) object).getBytes(StandardCharsets.UTF_8);
template.body(bodyBytes, StandardCharsets.UTF_8);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.client.feign.services;

import feign.Headers;
import feign.Param;
import feign.RequestLine;

/**
* Internal testing API for Working Capital Loans. These endpoints are only available when the TEST profile is active.
*/
public interface InternalWorkingCapitalLoansApi {

@RequestLine("POST v1/internal/working-capital-loans/{loanId}/activate?disbursementDate={disbursementDate}")
@Headers("Content-Type: application/json")
void activateLoan(@Param("loanId") Long loanId, @Param("disbursementDate") String disbursementDate);

@RequestLine("POST v1/internal/working-capital-loans/{loanId}/generate-next-delinquency-period?businessDate={businessDate}")
@Headers("Content-Type: application/json")
void generateNextDelinquencyPeriod(@Param("loanId") Long loanId, @Param("businessDate") String businessDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,15 @@ public CommandWrapperBuilder undoWorkingCapitalLoanApplicationDisbursal(final Lo
return this;
}

public CommandWrapperBuilder createWcLoanDelinquencyAction(final Long wcLoanId) {
this.actionName = "CREATE";
this.entityName = "WC_DELINQUENCY_ACTION";
this.entityId = wcLoanId;
this.loanId = wcLoanId;
this.href = "/working-capital-loans/" + wcLoanId + "/delinquency-actions";
return this;
}

public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
this.actionName = "CREATE";
this.entityName = "CLIENTIDENTIFIER";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.test.stepdef.loan;

import static org.apache.fineract.client.feign.util.FeignCalls.fail;
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
import static org.assertj.core.api.Assertions.assertThat;

import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.models.CommandProcessingResult;
import org.apache.fineract.client.models.DelinquencyBucketRequest;
import org.apache.fineract.client.models.MinimumPaymentPeriodAndRule;
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
import org.apache.fineract.client.models.PostWorkingCapitalLoanDelinquencyActionRequest;
import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest;
import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse;
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
import org.apache.fineract.client.models.WcLoanDelinquencyActionData;
import org.apache.fineract.client.models.WorkingCapitalLoanDelinquencyRangeScheduleData;
import org.apache.fineract.test.factory.WorkingCapitalRequestFactory;
import org.apache.fineract.test.helper.Utils;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContext;
import org.apache.fineract.test.support.TestContextKey;
import org.springframework.beans.factory.annotation.Autowired;

@Slf4j
@RequiredArgsConstructor
public class WorkingCapitalDelinquencyRescheduleStepDef extends AbstractStepDef {

private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd MMMM yyyy");

private final FineractFeignClient fineractFeignClient;

@Autowired
private WorkingCapitalRequestFactory workingCapitalRequestFactory;

@When("Admin creates a new Working Capital Loan Product with delinquency bucket")
public void createProductWithDelinquencyBucket() {
final Long bucketId = TestContext.GLOBAL.get(TestContextKey.DELINQUENCY_BUCKET_ID);
assertThat(bucketId).isNotNull();

final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory.defaultWorkingCapitalLoanProductRequest()
.name("WCLP-DLQ-" + Utils.randomStringGenerator(8)).delinquencyBucketId(bucketId);
final PostWorkingCapitalLoanProductsResponse response = ok(
() -> fineractFeignClient.workingCapitalLoanProducts().createWorkingCapitalLoanProduct(request));
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request);
log.info("Created WC product id={} with delinquency bucket id={}", response.getResourceId(), bucketId);
}

@When("Admin creates WC Delinquency Bucket with frequency {int} {word} and minimumPayment {int} {word}")
public void createWcDelinquencyBucket(final int frequency, final String frequencyType, final int minimumPayment,
final String minimumPaymentType) {
final DelinquencyBucketRequest request = new DelinquencyBucketRequest().name("DB-WCL-" + Utils.randomStringGenerator(12))
.bucketType("WORKING_CAPITAL").ranges(List.of(1L))
.minimumPaymentPeriodAndRule(new MinimumPaymentPeriodAndRule().frequency(frequency).frequencyType(frequencyType)
.minimumPayment(new BigDecimal(minimumPayment)).minimumPaymentType(minimumPaymentType));

final PostDelinquencyBucketResponse result = ok(
() -> fineractFeignClient.delinquencyRangeAndBucketsManagement().createBucket(request));
assertThat(result).isNotNull();
assertThat(result.getResourceId()).isNotNull();
TestContext.GLOBAL.set(TestContextKey.DELINQUENCY_BUCKET_ID, result.getResourceId());
log.info("Created WC delinquency bucket id={} with frequency={} {} minimumPayment={} {}", result.getResourceId(), frequency,
frequencyType, minimumPayment, minimumPaymentType);
}

@When("Admin creates WC delinquency reschedule action with minimumPayment {int} and frequency {int} {word}")
public void createRescheduleAction(final int minimumPayment, final int frequency, final String frequencyType) {
final Long loanId = getLoanId();
final PostWorkingCapitalLoanDelinquencyActionRequest request = buildRescheduleRequest(new BigDecimal(minimumPayment), frequency,
frequencyType);
log.info("Creating RESCHEDULE action for WC loan {}: minimumPayment={}, frequency={} {}", loanId, minimumPayment, frequency,
frequencyType);

final CommandProcessingResult result = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyActions().createDelinquencyAction(loanId, request));
assertThat(result).isNotNull();
assertThat(result.getResourceId()).isNotNull();
log.info("RESCHEDULE action created with id={}", result.getResourceId());
}

@Then("Admin fails to create WC delinquency reschedule action with minimumPayment {int} and frequency {int} {word}")
public void failToCreateRescheduleAction(final int minimumPayment, final int frequency, final String frequencyType) {
final Long loanId = getLoanId();
final PostWorkingCapitalLoanDelinquencyActionRequest request = buildRescheduleRequest(new BigDecimal(minimumPayment), frequency,
frequencyType);
log.info("Attempting to create RESCHEDULE action for WC loan {} (expecting failure): minimumPayment={}, frequency={} {}", loanId,
minimumPayment, frequency, frequencyType);

fail(() -> fineractFeignClient.workingCapitalLoanDelinquencyActions().createDelinquencyAction(loanId, request));
}

@Then("WC loan delinquency range schedule has the following periods:")
public void verifyPeriods(final DataTable table) {
final Long loanId = getLoanId();
final List<WorkingCapitalLoanDelinquencyRangeScheduleData> periods = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId));

final List<Map<String, String>> expectedRows = table.asMaps();
assertThat(periods).as("Number of periods").hasSize(expectedRows.size());

for (int i = 0; i < expectedRows.size(); i++) {
final Map<String, String> expected = expectedRows.get(i);
final WorkingCapitalLoanDelinquencyRangeScheduleData actual = periods.get(i);
final String periodLabel = "Period " + (i + 1);

assertThat(actual.getPeriodNumber()).as(periodLabel + " periodNumber")
.isEqualTo(Integer.parseInt(expected.get("periodNumber")));
assertThat(actual.getFromDate()).as(periodLabel + " fromDate")
.isEqualTo(LocalDate.parse(expected.get("fromDate"), DATE_FORMAT));
assertThat(actual.getToDate()).as(periodLabel + " toDate").isEqualTo(LocalDate.parse(expected.get("toDate"), DATE_FORMAT));
assertThat(actual.getExpectedAmount()).as(periodLabel + " expectedAmount")
.isEqualByComparingTo(new BigDecimal(expected.get("expectedAmount")));
assertThat(actual.getPaidAmount()).as(periodLabel + " paidAmount")
.isEqualByComparingTo(new BigDecimal(expected.get("paidAmount")));
assertThat(actual.getOutstandingAmount()).as(periodLabel + " outstandingAmount")
.isEqualByComparingTo(new BigDecimal(expected.get("outstandingAmount")));

final String criteriaMetStr = expected.get("minPaymentCriteriaMet");
if (criteriaMetStr == null || criteriaMetStr.isBlank()) {
assertThat(actual.getMinPaymentCriteriaMet()).as(periodLabel + " minPaymentCriteriaMet").isNull();
} else {
assertThat(actual.getMinPaymentCriteriaMet()).as(periodLabel + " minPaymentCriteriaMet")
.isEqualTo(Boolean.parseBoolean(criteriaMetStr));
}
}
}

@Then("WC loan delinquency range schedule has periods with expectedAmount {int}")
public void verifyExpectedAmount(final int expectedAmount) {
final Long loanId = getLoanId();
final List<WorkingCapitalLoanDelinquencyRangeScheduleData> periods = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId));

assertThat(periods).isNotEmpty();
final BigDecimal expected = new BigDecimal(expectedAmount);
for (final WorkingCapitalLoanDelinquencyRangeScheduleData period : periods) {
if (period.getMinPaymentCriteriaMet() == null) {
assertThat(period.getExpectedAmount()).as("Period %d expectedAmount", period.getPeriodNumber())
.isEqualByComparingTo(expected);
}
}
log.info("Verified {} unevaluated periods have expectedAmount={}", periods.size(), expectedAmount);
}

@Then("WC loan delinquency actions contain {int} action(s) with type RESCHEDULE")
public void verifyRescheduleActionExists(final int count) {
final Long loanId = getLoanId();
final List<WcLoanDelinquencyActionData> actions = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyActions().retrieveDelinquencyActions(loanId));

final long rescheduleCount = actions.stream().filter(a -> WcLoanDelinquencyActionData.ActionEnum.RESCHEDULE.equals(a.getAction()))
.count();
assertThat(rescheduleCount).as("RESCHEDULE action count").isEqualTo(count);
}

@Then("WC loan delinquency actions contain {int} action(s)")
public void verifyActionCount(final int count) {
final Long loanId = getLoanId();
final List<WcLoanDelinquencyActionData> actions = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyActions().retrieveDelinquencyActions(loanId));

assertThat(actions).hasSize(count);
}

private Long getLoanId() {
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assertThat(loanResponse).isNotNull();
return loanResponse.getLoanId();
}

private PostWorkingCapitalLoanDelinquencyActionRequest buildRescheduleRequest(final BigDecimal minimumPayment, final int frequency,
final String frequencyType) {
return new PostWorkingCapitalLoanDelinquencyActionRequest().action("reschedule").minimumPayment(minimumPayment).frequency(frequency)
.frequencyType(frequencyType).locale("en");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ private void createWorkingCapitalLoanAccount(final List<String> loanData) {
final PostWorkingCapitalLoansResponse response = ok(
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
trackLoanIdIfEnabled(response.getLoanId());
log.info("Working Capital Loan created with ID: {}", response.getLoanId());
}

Expand Down Expand Up @@ -707,7 +708,20 @@ private Long extractClientId() {
return clientResponse.getClientId();
}

@SuppressWarnings("unchecked")
private void trackLoanIdIfEnabled(final Long loanId) {
final List<Long> trackedIds = testContext().get(TestContextKey.WC_LOAN_IDS);
if (trackedIds != null) {
trackedIds.add(loanId);
}
}

private Long resolveLoanProductId(final String loanProductName) {
if ("WCLP_DELINQUENCY".equals(loanProductName)) {
final PostWorkingCapitalLoanProductsResponse response = testContext()
.get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE);
return response.getResourceId();
}
final DefaultWorkingCapitalLoanProduct product = DefaultWorkingCapitalLoanProduct.valueOf(loanProductName);
return workingCapitalLoanProductResolver.resolve(product);
}
Expand Down
Loading
Loading