Skip to content

Commit dc31cd1

Browse files
authored
Merge branch 'apache:develop' into FINERACT-1152-fix-loan-reschedule-end-date-validation
2 parents 02409ef + c90a874 commit dc31cd1

24 files changed

Lines changed: 4821 additions & 75 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.UsersApi;
156156
import org.apache.fineract.client.feign.services.WorkingCapitalLoanCobCatchUpApi;
157157
import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
158+
import org.apache.fineract.client.feign.services.WorkingCapitalLoanTransactionsApi;
158159
import org.apache.fineract.client.feign.services.WorkingCapitalLoansApi;
159160
import org.apache.fineract.client.feign.services.WorkingDaysApi;
160161

@@ -757,6 +758,10 @@ public WorkingCapitalLoansApi workingCapitalLoans() {
757758
return create(WorkingCapitalLoansApi.class);
758759
}
759760

761+
public WorkingCapitalLoanTransactionsApi workingCapitalLoanTransactions() {
762+
return create(WorkingCapitalLoanTransactionsApi.class);
763+
}
764+
760765
public WorkingDaysApi workingDays() {
761766
return create(WorkingDaysApi.class);
762767
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/AssetExternalizationErrorMessage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public enum AssetExternalizationErrorMessage {
3131
LOAN_CLOSED_OBLIGATIONS_MET_INVALID("Loan status CLOSED_OBLIGATIONS_MET is not valid for transfer."), //
3232
LOAN_SUBMITTED_AND_PENDING_APPROVAL_INVALID("Loan status SUBMITTED_AND_PENDING_APPROVAL is not valid for transfer."), //
3333
LOAN_APPROVED_INVALID("Loan status APPROVED is not valid for transfer."), //
34+
ALREADY_IN_PROGRESS("This loan cannot be sold, there is already an in progress transfer"), //
3435
INVALID_REQUEST("The request was invalid. This typically will happen due to validation errors which are provided."); //
3536

3637
public final String value;

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ private void createAssetExternalizationRequestByLoanId(DataTable table, String t
159159
} else if ((transferData.get(0).equals(TRANSACTION_TYPE_SALE) || transferData.get(0).equals(TRANSACTION_TYPE_INTERMEDIARY_SALE))) {
160160
String ownerExternalId;
161161
if (regenerateOwner) {
162+
// For owner-to-owner transfers: preserve the current owner as previous owner
163+
String currentOwner = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_OWNER_EXTERNAL_ID);
164+
if (currentOwner != null && !transferData.get(0).equals(TRANSACTION_TYPE_INTERMEDIARY_SALE)) {
165+
testContext().set(TestContextKey.ASSET_EXTERNALIZATION_PREVIOUS_OWNER_EXTERNAL_ID, currentOwner);
166+
}
162167
ownerExternalId = Utils.randomStringGenerator(OWNER_EXTERNAL_ID_PREFIX, 10);
163168
} else {
164169
ownerExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_OWNER_EXTERNAL_ID);
@@ -369,13 +374,20 @@ private void checkExternalAssetDetails(Long loanId, String loanExternalId, PageE
369374
previousAssetOwner = null;
370375
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
371376
}
372-
} else { // in case transfer has previous intermediarySale transfer
377+
} else { // in case transfer has previous intermediarySale or owner-to-owner transfer
373378
if (transactionType.equalsIgnoreCase(TRANSACTION_TYPE_SALE)
374379
&& (status.equals(ExternalTransferData.StatusEnum.ACTIVE.getValue())
375380
|| status.equals(ExternalTransferData.StatusEnum.PENDING.getValue()))) {
376381
ownerExternalId = ownerExternalIdStored;
377382
previousAssetOwner = intermediarySaleAssetOwner;
378383
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
384+
} else if (transactionType.equalsIgnoreCase(TRANSACTION_TYPE_SALE)
385+
&& (status.equals(ExternalTransferData.StatusEnum.DECLINED.getValue())
386+
|| status.equals(ExternalTransferData.StatusEnum.CANCELLED.getValue()))) {
387+
// DECLINED and CANCELLED records have previousOwner = null in the API response
388+
ownerExternalId = ownerExternalIdStored;
389+
previousAssetOwner = null;
390+
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
379391
} else if (transactionType.equalsIgnoreCase(TRANSACTION_TYPE_BUYBACK)
380392
&& (status.equals(ExternalTransferData.StatusEnum.BUYBACK.getValue())
381393
|| status.equals(ExternalTransferData.StatusEnum.BUYBACK_INTERMEDIATE.getValue()))) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public void makeLoanRepaymentAndCheckOwner(String repaymentType, String transact
104104
makeRepayment(repaymentType, transactionDate, transactionAmount, transferExternalOwnerId);
105105
}
106106

107+
@And("Customer makes {string} repayment on {string} with {double} EUR transaction amount and check previous external owner")
108+
public void makeLoanRepaymentAndCheckPreviousOwner(String repaymentType, String transactionDate, double transactionAmount)
109+
throws IOException {
110+
String previousOwnerId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_PREVIOUS_OWNER_EXTERNAL_ID);
111+
makeRepayment(repaymentType, transactionDate, transactionAmount, previousOwnerId);
112+
}
113+
107114
private void makeRepayment(String repaymentType, String transactionDate, double transactionAmount, String transferExternalOwnerId)
108115
throws IOException {
109116
eventStore.reset();

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,14 @@ public void createTransactionWithAutoIdempotencyKeyWithExternalOwner(String tran
389389
transactionAmount, transferExternalOwnerId);
390390
}
391391

392+
@When("Customer makes {string} transaction with {string} payment type on {string} with {double} EUR transaction amount and system-generated Idempotency key and check previous external owner")
393+
public void createTransactionWithAutoIdempotencyKeyWithPreviousExternalOwner(String transactionTypeInput, String transactionPaymentType,
394+
String transactionDate, double transactionAmount) throws IOException {
395+
String previousOwnerId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_PREVIOUS_OWNER_EXTERNAL_ID);
396+
createTransactionWithAutoIdempotencyKeyAndWithExternalOwner(transactionTypeInput, transactionPaymentType, transactionDate,
397+
transactionAmount, previousOwnerId);
398+
}
399+
392400
@When("Customer makes {string} transaction with {string} payment type on {string} with {double} EUR transaction amount and system-generated Idempotency key and interestRefundCalculation {booleanValue}")
393401
public void createTransactionWithAutoIdempotencyKeyAndWithInterestRefundCalculationFlagProvided(final String transactionTypeInput,
394402
final String transactionPaymentType, final String transactionDate, final double transactionAmount,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 io.cucumber.java.en.When;
28+
import java.math.BigDecimal;
29+
import java.time.LocalDate;
30+
import java.util.Map;
31+
import lombok.RequiredArgsConstructor;
32+
import lombok.extern.slf4j.Slf4j;
33+
import org.apache.fineract.client.feign.FineractFeignClient;
34+
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
35+
import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
36+
import org.apache.fineract.client.models.WorkingCapitalLoanCommandTemplateData;
37+
import org.apache.fineract.test.stepdef.AbstractStepDef;
38+
import org.apache.fineract.test.support.TestContextKey;
39+
import org.assertj.core.api.SoftAssertions;
40+
41+
@Slf4j
42+
@RequiredArgsConstructor
43+
public class WorkingCapitalLoanActionTemplateStepDef extends AbstractStepDef {
44+
45+
private final FineractFeignClient fineractClient;
46+
47+
@When("Admin retrieves the working capital loan action template with templateType {string}")
48+
public void retrieveWcLoanActionTemplate(final String templateType) {
49+
final Long loanId = getCreatedLoanId();
50+
51+
final WorkingCapitalLoanCommandTemplateData response = ok(
52+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, templateType));
53+
testContext().set(TestContextKey.WC_LOAN_ACTION_TEMPLATE_RESPONSE, response);
54+
log.info("Retrieved WC loan action template for loan ID: {} with templateType: {}", loanId, templateType);
55+
}
56+
57+
@Then("The working capital loan approve template has the following data:")
58+
public void verifyApproveTemplateData(final DataTable table) {
59+
verifyTemplateData(table);
60+
}
61+
62+
@Then("The working capital loan disburse template has the following data:")
63+
public void verifyDisburseTemplateData(final DataTable table) {
64+
verifyTemplateData(table);
65+
}
66+
67+
@Then("Retrieving WC loan action template with invalid templateType {string} results in an error")
68+
public void retrieveTemplateWithInvalidType(final String templateType) {
69+
final Long loanId = getCreatedLoanId();
70+
71+
final CallFailedRuntimeException exception = fail(
72+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, templateType));
73+
74+
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400);
75+
assertThat(exception.getMessage()).as("Error message should reference the invalid command").contains(templateType);
76+
log.info("Verified WC loan action template retrieval failed with invalid templateType: {}", templateType);
77+
}
78+
79+
@Then("Retrieving WC loan action template without templateType results in an error")
80+
public void retrieveTemplateWithoutType() {
81+
final Long loanId = getCreatedLoanId();
82+
83+
final CallFailedRuntimeException exception = fail(
84+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, (String) null));
85+
86+
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(400);
87+
assertThat(exception.getMessage()).as("Error message should reference unrecognized command").contains("command");
88+
log.info("Verified WC loan action template retrieval failed without templateType");
89+
}
90+
91+
@Then("Retrieving WC loan action template for non-existent loan id {long} results in a 404 error")
92+
public void retrieveTemplateForNonExistentLoan(final Long loanId) {
93+
final CallFailedRuntimeException exception = fail(
94+
() -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTemplate1(loanId, "approve"));
95+
96+
assertThat(exception.getStatus()).as("HTTP status code").isEqualTo(404);
97+
assertThat(exception.getMessage()).as("Error message should indicate loan not found").contains("does not exist");
98+
log.info("Verified WC loan action template retrieval failed for non-existent loan ID: {}", loanId);
99+
}
100+
101+
private void verifyTemplateData(final DataTable table) {
102+
final WorkingCapitalLoanCommandTemplateData response = testContext().get(TestContextKey.WC_LOAN_ACTION_TEMPLATE_RESPONSE);
103+
assertThat(response).as("Template response should not be null").isNotNull();
104+
105+
final Map<String, String> expected = table.asMaps().get(0);
106+
107+
SoftAssertions.assertSoftly(softly -> expected.forEach((field, value) -> {
108+
switch (field) {
109+
case "approvalAmount" ->
110+
softly.assertThat(response.getApprovalAmount()).as(field).isNotNull().isEqualByComparingTo(new BigDecimal(value));
111+
case "approvalDate" ->
112+
softly.assertThat(response.getApprovalDate()).as(field).isNotNull().isEqualTo(LocalDate.parse(value));
113+
case "expectedDisbursementDate" ->
114+
softly.assertThat(response.getExpectedDisbursementDate()).as(field).isNotNull().isEqualTo(LocalDate.parse(value));
115+
case "expectedAmount" -> {
116+
if ("null".equals(value)) {
117+
softly.assertThat(response.getExpectedAmount()).as(field).isNull();
118+
} else {
119+
softly.assertThat(response.getExpectedAmount()).as(field).isNotNull().isEqualByComparingTo(new BigDecimal(value));
120+
}
121+
}
122+
case "paymentTypeOptionsPresent" -> {
123+
if (Boolean.parseBoolean(value)) {
124+
softly.assertThat(response.getPaymentTypeOptions()).as(field).isNotNull().isNotEmpty();
125+
} else {
126+
softly.assertThat(response.getPaymentTypeOptions()).as(field).isNullOrEmpty();
127+
}
128+
}
129+
default -> softly.fail("Unknown template field in DataTable: " + field);
130+
}
131+
}));
132+
133+
log.info("Verified WC loan action template data");
134+
}
135+
136+
private Long getCreatedLoanId() {
137+
final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
138+
assertThat(loanResponse).as("No loan create response in context — did a previous loan creation step run?").isNotNull();
139+
return loanResponse.getLoanId();
140+
}
141+
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/reporting/ReportingStepDef.java

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
public class ReportingStepDef extends AbstractStepDef {
4444

4545
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.ENGLISH);
46-
4746
private final FineractFeignClient fineractClient;
4847

4948
@Then("Transaction Summary Report for date {string} has the following data:")
@@ -56,14 +55,18 @@ public void transactionSummaryReportWithAssetOwnerHasData(final String dateStr,
5655
verifyReportData("Transaction Summary Report with Asset Owner", dateStr, dataTable);
5756
}
5857

59-
private void verifyReportData(final String reportName, final String dateStr, final DataTable dataTable) {
60-
final PostOfficesResponse officeResponse = testContext().get(TestContextKey.OFFICE_CREATE_RESPONSE);
61-
assertThat(officeResponse).as("No office was created. Use 'Admin creates a new office' step first.").isNotNull();
58+
@Then("Transaction Summary Report with Asset Owner for date {string} column {string} has non-empty value for all rows")
59+
public void transactionSummaryReportWithAssetOwnerColumnNonEmpty(final String dateStr, final String columnName) {
60+
verifyColumnNullability("Transaction Summary Report with Asset Owner", dateStr, columnName, false);
61+
}
6262

63-
final String date = LocalDate.parse(dateStr, FORMATTER).toString();
64-
final RunReportsResponse response = fineractClient.runReports().runReportGetData(reportName, Map.of("R_endDate", date, "R_officeId",
65-
String.valueOf(officeResponse.getOfficeId()), "locale", "en", "dateFormat", "yyyy-MM-dd"));
66-
assertThat(response.getData()).as("Report '%s' returned no data", reportName).isNotNull();
63+
@Then("Transaction Summary Report with Asset Owner for date {string} column {string} has empty value for all rows")
64+
public void transactionSummaryReportWithAssetOwnerColumnEmpty(final String dateStr, final String columnName) {
65+
verifyColumnNullability("Transaction Summary Report with Asset Owner", dateStr, columnName, true);
66+
}
67+
68+
private void verifyReportData(final String reportName, final String dateStr, final DataTable dataTable) {
69+
final RunReportsResponse response = executeReport(reportName, dateStr);
6770

6871
final List<List<String>> expected = dataTable.asLists();
6972
final List<String> headers = expected.getFirst();
@@ -98,6 +101,40 @@ private void verifyReportData(final String reportName, final String dateStr, fin
98101
}
99102
}
100103

104+
private void verifyColumnNullability(final String reportName, final String dateStr, final String columnName,
105+
final boolean expectEmpty) {
106+
final RunReportsResponse response = executeReport(reportName, dateStr);
107+
108+
assertThat(response.getColumnHeaders()).isNotNull();
109+
final int colIdx = findColumnIndex(response.getColumnHeaders(), columnName);
110+
111+
for (int i = 0; i < response.getData().size(); i++) {
112+
final List<Object> row = response.getData().get(i).getRow();
113+
assertThat(row).as("Report '%s', row %d: null cell list", reportName, i + 1).isNotNull();
114+
assertThat(colIdx).as("Report '%s', row %d: column index out of bounds", reportName, i + 1).isLessThan(row.size());
115+
final String value = stringify(row.get(colIdx));
116+
final boolean isEmpty = value.isEmpty() || "null".equals(value);
117+
if (expectEmpty) {
118+
assertThat(isEmpty)
119+
.as("Report '%s', row %d, column '%s': expected empty but was '%s'", reportName, i + 1, columnName, value).isTrue();
120+
} else {
121+
assertThat(isEmpty).as("Report '%s', row %d, column '%s': expected non-empty but was empty", reportName, i + 1, columnName)
122+
.isFalse();
123+
}
124+
}
125+
}
126+
127+
private RunReportsResponse executeReport(final String reportName, final String dateStr) {
128+
final PostOfficesResponse officeResponse = testContext().get(TestContextKey.OFFICE_CREATE_RESPONSE);
129+
assertThat(officeResponse).as("No office was created. Use 'Admin creates a new office' step first.").isNotNull();
130+
131+
final String date = LocalDate.parse(dateStr, FORMATTER).toString();
132+
final RunReportsResponse response = fineractClient.runReports().runReportGetData(reportName, Map.of("R_endDate", date, "R_officeId",
133+
String.valueOf(officeResponse.getOfficeId()), "locale", "en", "dateFormat", "yyyy-MM-dd"));
134+
assertThat(response.getData()).as("Report '%s' returned no data", reportName).isNotNull();
135+
return response;
136+
}
137+
101138
private boolean valuesMatch(final String expected, final String actual) {
102139
if (Objects.equals(expected, actual)) {
103140
return true;

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,6 @@ public abstract class TestContextKey {
332332
public static final String DELINQUENCY_BUCKET_ID_FOR_UPDATE = "delinquencyBucketIdForUpdate";
333333
public static final String DELINQUENCY_BUCKET_CREATE_REQUEST_FOR_UPDATE = "delinquencyBucketCreateRequestForUpdate";
334334
public static final String DELINQUENCY_BUCKET_CREATE_RESPONSE_FOR_UPDATE_DUPLICATE = "delinquencyBucketUpdateRequestForUpdateDuplicate";
335+
336+
public static final String WC_LOAN_ACTION_TEMPLATE_RESPONSE = "wcLoanActionTemplateResponse";
335337
}

0 commit comments

Comments
 (0)