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 @@ -68,6 +68,21 @@ public PostWorkingCapitalLoansRequest defaultWorkingCapitalLoansRequest(Long cli
.dateFormat(DATE_FORMAT);//
}

/**
* Loan application shell for ANNUAL_EIR products: no TPV / period payment rate (those fields are not allowed for
* that strategy). Caller sets productId, dates, principal, annualEir and optional discount.
*/
public PostWorkingCapitalLoansRequest defaultAnnualEirWorkingCapitalLoansRequest(final Long clientId) {
return new PostWorkingCapitalLoansRequest()//
.clientId(clientId)//
.productId(workingCapitalLoanProductResolver.resolve(DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT))//
.submittedOnDate(DATE_SUBMIT_STRING)//
.expectedDisbursementDate(DATE_SUBMIT_STRING)//
.principalAmount(DEFAULT_PRINCIPAL)//
.locale(DEFAULT_LOCALE)//
.dateFormat(DATE_FORMAT);//
}

public PutWorkingCapitalLoansLoanIdRequest defaultModifyWorkingCapitalLoansRequest() {
return new PutWorkingCapitalLoansLoanIdRequest()//
.locale(DEFAULT_LOCALE)//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductAll
.allowAttributeOverrides(allowAttributeOverrides);
}

/**
* Annual EIR strategy product: no period payment rate, required discount, and discount override enabled so loan
* applications may pass a discount (as in the reference EIR calculation scenarios).
*/
public PostWorkingCapitalLoanProductsRequest defaultAnnualEirWorkingCapitalLoanProductRequest(final BigDecimal annualEir,
final BigDecimal discount) {
return defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() //
.paymentAmountCalculationStrategy(PostWorkingCapitalLoanProductsRequest.PaymentAmountCalculationStrategyEnum.ANNUAL_EIR) //
.annualEir(annualEir) //
.discount(discount) //
.periodPaymentRate(null);
}

public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductBreachRequest() {
String name = Utils.randomStringGenerator(WCLP_NAME_PREFIX, 10);
String shortName = loanProductsRequestFactory.generateShortNameSafely();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public Long insertLoan(final LoanStatus status, final LocalDate lastClosedBusine
.addValue("breach_start_type", "DISBURSEMENT")//
// SimpleJdbcInsert without usingColumns() binds explicit NULL for every unmapped
// table column, which bypasses the DB default and violates the NOT NULL constraint.
.addValue("payment_amount_calculation_strategy", "TPV")//
.addValue("is_charged_off", false).addValue("is_fraud", Boolean.FALSE);
final Number key = wcLoanInsert.executeAndReturnKey(params);
return Objects.requireNonNull(key, "Generated key must not be null").longValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,74 @@ public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) {
submitLoanUsingCreatedProduct(table, null, null);
}

@When("Admin creates a working capital loan with annual EIR using created product with the following data:")
public void createWorkingCapitalLoanWithAnnualEirUsingCreatedProduct(final DataTable table) {
final Map<String, String> rawData = table.asMaps().getFirst();
final Long clientId = extractClientId();
final PostWorkingCapitalLoanProductsResponse productResponse = testContext()
.get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE);
final Long loanProductId = productResponse.getResourceId();

final PostWorkingCapitalLoansRequest loansRequest = workingCapitalLoanRequestFactory
.defaultAnnualEirWorkingCapitalLoansRequest(clientId).productId(loanProductId)
.submittedOnDate(rawData.get("submittedOnDate")).expectedDisbursementDate(rawData.get("expectedDisbursementDate"))
.principalAmount(new BigDecimal(rawData.get("principalAmount")))
.discount(blankToNull(rawData.get("discount")) != null ? new BigDecimal(rawData.get("discount").trim()) : null);
if (blankToNull(rawData.get("annualEir")) != null) {
loansRequest.annualEir(new BigDecimal(rawData.get("annualEir").trim()));
}
testContext().set(TestContextKey.LOAN_CREATE_REQUEST, loansRequest);

final PostWorkingCapitalLoansResponse response = ok(
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_CREATE_RESPONSE, response);
trackLoanIdIfEnabled(response.getLoanId());
log.info("Working Capital Loan with annual EIR created, Loan ID: {}", response.getLoanId());
}

@Then("Admin creates a working capital loan with annual EIR using created product with the following data expecting error:")
public void createWorkingCapitalLoanWithAnnualEirUsingCreatedProductExpectingError(final DataTable table) {
final Map<String, String> rawData = table.asMaps().getFirst();
final Long clientId = extractClientId();
final PostWorkingCapitalLoanProductsResponse productResponse = testContext()
.get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE);
final Long loanProductId = productResponse.getResourceId();

// Start from the Annual EIR application shell (no TPV fields). Callers that need TPV fields for a
// mixed-strategy
// negative case pass them explicitly in the table.
final PostWorkingCapitalLoansRequest loansRequest = workingCapitalLoanRequestFactory
.defaultAnnualEirWorkingCapitalLoansRequest(clientId).productId(loanProductId)
.submittedOnDate(rawData.get("submittedOnDate")).expectedDisbursementDate(rawData.get("expectedDisbursementDate"))
.principalAmount(new BigDecimal(rawData.get("principalAmount")))
.discount(blankToNull(rawData.get("discount")) != null ? new BigDecimal(rawData.get("discount").trim()) : null);
if (blankToNull(rawData.get("annualEir")) != null) {
loansRequest.annualEir(new BigDecimal(rawData.get("annualEir").trim()));
}
if (blankToNull(rawData.get("totalPaymentVolume")) != null) {
loansRequest.totalPaymentVolume(new BigDecimal(rawData.get("totalPaymentVolume").trim()));
}
if (blankToNull(rawData.get("periodPaymentRate")) != null) {
loansRequest.periodPaymentRate(new BigDecimal(rawData.get("periodPaymentRate").trim()));
}

final CallFailedRuntimeException exception = fail(
() -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(loansRequest));
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, exception);

final int expectedHttpCode = Integer.parseInt(rawData.get("httpCode"));
final String expectedErrorMessage = rawData.get("errorMessage").trim();
assertHttpStatus(exception, expectedHttpCode);
assertValidationError(exception, expectedErrorMessage);
log.info("Verified Working Capital Loan create with annual EIR failed with status {} and message: {}", expectedHttpCode,
expectedErrorMessage);
}

private static String blankToNull(final String value) {
return value == null || value.isBlank() ? null : value.trim();
}

@When("Admin creates a working capital loan using created product with breachGraceDays {int} and the following data:")
public void createWorkingCapitalLoanUsingCreatedProductWithBreachGraceDays(final int breachGraceDays, final DataTable table) {
submitLoanUsingCreatedProduct(table, breachGraceDays, null);
Expand Down Expand Up @@ -2519,6 +2587,25 @@ public void adminAddWorkingCapitalPeriodPaymentRateInvalidDataFailure(final Stri
updatePeriodPaymentRateFailed(periodPaymentRate, errorMessage);
}

@Then("Admin update Working Capital period payment rate with {string} value expecting error:")
public void adminUpdateWorkingCapitalPeriodPaymentRateExpectingError(final String periodPaymentRate, final DataTable table) {
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.getLoanId();
final Map<String, String> expectedData = table.asMaps().getFirst();
final int expectedHttpCode = Integer.parseInt(expectedData.get("httpCode"));
final String expectedErrorMessage = expectedData.get("errorMessage").trim();

final PutWorkingCapitalLoansLoanIdRateRequest rateChangeRequest = workingCapitalLoanRequestFactory
.defaultWorkingCapitalLoanUpdateRateRequest().periodPaymentRate(new BigDecimal(periodPaymentRate));
final CallFailedRuntimeException exception = fail(
() -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateById(loanId, rateChangeRequest));

assertHttpStatus(exception, expectedHttpCode);
assertValidationError(exception, expectedErrorMessage);
log.info("Verified period payment rate update on loan {} failed with status {} and message: {}", loanId, expectedHttpCode,
expectedErrorMessage);
}

@When("Admin update Working Capital period payment rate failed with {string} value cause unable to calculate EIR")
public void adminAddWorkingCapitalPeriodPaymentRateCauseUnableCalculateEIrFailure(final String periodPaymentRate) {
String errorMessage = ErrorMessageHelper.workingCapitalInputValuesCauseUnableCalculateEIrFailure();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -147,6 +148,76 @@ public void createWorkingCapitalLoanProduct() {
checkWorkingCapitalLoanProductCreate();
}

@When("Admin creates a new Working Capital Loan Product with Annual EIR strategy, annualEir {string} and discount {string}")
public void createWorkingCapitalLoanProductWithAnnualEirStrategy(final String annualEir, final String discount) {
final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", RANDOM_NAME_SUFFIX_LENGTH);
final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory
.defaultAnnualEirWorkingCapitalLoanProductRequest(new BigDecimal(annualEir), new BigDecimal(discount))//
.name(name);
final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request);
checkWorkingCapitalLoanProductCreate();
}

@When("Admin creates a new Working Capital Loan Product with Annual EIR strategy, annualEir {string}, discount {string}, minAnnualEir {string} and maxAnnualEir {string}")
public void createWorkingCapitalLoanProductWithAnnualEirStrategyAndMinMax(final String annualEir, final String discount,
final String minAnnualEir, final String maxAnnualEir) {
final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", RANDOM_NAME_SUFFIX_LENGTH);
final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory
.defaultAnnualEirWorkingCapitalLoanProductRequest(new BigDecimal(annualEir), new BigDecimal(discount))//
.name(name)//
.minAnnualEir(new BigDecimal(minAnnualEir))//
.maxAnnualEir(new BigDecimal(maxAnnualEir));
final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request);
checkWorkingCapitalLoanProductCreate();
}

@Then("Admin creates a Working Capital Loan Product with the following payment strategy data expecting error:")
public void createWorkingCapitalLoanProductWithPaymentStrategyDataExpectingError(final DataTable table) {
final Map<String, String> rawData = table.asMaps().getFirst();
final String name = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", RANDOM_NAME_SUFFIX_LENGTH);
final String strategy = blankToNull(rawData.get("paymentAmountCalculationStrategy"));

final PostWorkingCapitalLoanProductsRequest request;
if ("TPV".equalsIgnoreCase(strategy)) {
request = workingCapitalRequestFactory.defaultWorkingCapitalLoanProductRequest().name(name);
applyOptionalBigDecimal(rawData, "annualEir", request::setAnnualEir);
applyOptionalBigDecimal(rawData, "discount", request::setDiscount);
} else {
request = workingCapitalRequestFactory.defaultAnnualEirWorkingCapitalLoanProductRequest(
parseOptionalBigDecimal(rawData.get("annualEir")), parseOptionalBigDecimal(rawData.get("discount"))).name(name);
}
applyOptionalBigDecimal(rawData, "periodPaymentRate", request::setPeriodPaymentRate);
applyOptionalBigDecimal(rawData, "minPeriodPaymentRate", request::setMinPeriodPaymentRate);
applyOptionalBigDecimal(rawData, "maxPeriodPaymentRate", request::setMaxPeriodPaymentRate);
applyOptionalBigDecimal(rawData, "minAnnualEir", request::setMinAnnualEir);
applyOptionalBigDecimal(rawData, "maxAnnualEir", request::setMaxAnnualEir);

final int expectedHttpCode = Integer.parseInt(rawData.get("httpCode"));
final String expectedErrorMessage = rawData.get("errorMessage").trim();
checkCreateWorkingCapitalLoanProductWithInvalidDataFailure(request, expectedHttpCode, expectedErrorMessage);
log.info("Verified Working Capital Loan Product create failed with status {} and message: {}", expectedHttpCode,
expectedErrorMessage);
}

private static String blankToNull(final String value) {
return value == null || value.isBlank() ? null : value.trim();
}

private static BigDecimal parseOptionalBigDecimal(final String value) {
final String trimmed = blankToNull(value);
return trimmed == null ? null : new BigDecimal(trimmed);
}

private static void applyOptionalBigDecimal(final Map<String, String> rawData, final String key, final Consumer<BigDecimal> setter) {
if (rawData.containsKey(key) && blankToNull(rawData.get(key)) != null) {
setter.accept(new BigDecimal(rawData.get(key).trim()));
}
}

@When("Admin creates a new Working Capital Loan Product with breach and near breach")
public void createWorkingCapitalLoanProductWithBreachAndNearBreach() {
final Long breachId = getWcBreachIdForFrequency(2, WorkingCapitalBreachFrequencyType.MONTHS.getCode());
Expand Down Expand Up @@ -1286,9 +1357,12 @@ public void checkWorkingCapitalLoanProductCreate(PostWorkingCapitalLoanProductsR
assertions.assertThat(getWorkingCapitalProductResponse.getRepaymentFrequencyType()).isNotNull();
assertions.assertThat(workingCapitalLoanProductCreateRequest.getRepaymentFrequencyType().getValue())
.isEqualTo(getWorkingCapitalProductResponse.getRepaymentFrequencyType().getCode());
assertions.assertThat(workingCapitalLoanProductCreateRequest.getPeriodPaymentRate()).isNotNull();
assertions.assertThat(workingCapitalLoanProductCreateRequest.getPeriodPaymentRate()
.compareTo(getWorkingCapitalProductResponse.getPeriodPaymentRate())).isEqualTo(0);
if (workingCapitalLoanProductCreateRequest.getPeriodPaymentRate() != null) {
assertions.assertThat(workingCapitalLoanProductCreateRequest.getPeriodPaymentRate()
.compareTo(getWorkingCapitalProductResponse.getPeriodPaymentRate())).isEqualTo(0);
} else {
assertions.assertThat(getWorkingCapitalProductResponse.getPeriodPaymentRate()).isNull();
}
assertions.assertThat(workingCapitalLoanProductCreateRequest.getMinPeriodPaymentRate())
.isEqualTo(getWorkingCapitalProductResponse.getMinPeriodPaymentRate());
assertions.assertThat(workingCapitalLoanProductCreateRequest.getMaxPeriodPaymentRate())
Expand Down
Loading