From 77fd4ff4c1fc25cb8d2f9955a8b68c2b5657bfcb Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Wed, 11 May 2022 11:30:00 +0200 Subject: [PATCH] Fine tuning the batch APIs and minor adjustments --- .../fineract/client/util/FineractClient.java | 3 + .../fineract/batch/api/BatchApiResource.java | 5 +- .../command/CommandStrategyProvider.java | 5 + .../internal/GetLoanByIdCommandStrategy.java | 138 +++++++++++ .../GetTransactionByIdCommandStrategy.java | 87 +++++++ .../populator/AbstractWorkbookPopulator.java | 2 +- .../core/api/MutableUriInfo.java | 147 +++++++++++ .../api/DatatablesApiResource.java | 4 +- .../api/DatatablesApiResourceSwagger.java | 58 ++--- .../api/CurrenciesApiResourceSwagger.java | 4 +- .../charge/api/ChargesApiResourceSwagger.java | 4 + .../client/api/ClientsApiResourceSwagger.java | 4 + .../LoanTransactionsApiResourceSwagger.java | 21 +- .../api/LoansApiResourceSwagger.java | 230 +++++++++++++++++- .../portfolio/loanaccount/domain/Loan.java | 16 +- .../domain/LoanApplicationTerms.java | 10 +- .../service/LoanReadPlatformServiceImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 11 +- .../api/LoanProductsApiResourceSwagger.java | 121 +++++++-- .../api/SelfLoansApiResourceSwagger.java | 2 + .../command/CommandStrategyProviderTest.java | 134 ++++++++++ .../GetLoanByIdCommandStrategyTest.java | 186 ++++++++++++++ ...GetTransactionByIdCommandStrategyTest.java | 205 ++++++++++++++++ .../loanaccount/domain/LoanTest.java | 88 +++++++ .../integrationtests/BatchApiTest.java | 207 +++++++++++++++- .../integrationtests/common/BatchHelper.java | 98 +++++++- 26 files changed, 1703 insertions(+), 89 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategy.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategy.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/MutableUriInfo.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategyTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategyTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index dcaa1ccff..fa141a63b 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -77,6 +77,7 @@ import org.apache.fineract.client.services.JournalEntriesApi; import org.apache.fineract.client.services.ListReportMailingJobHistoryApi; import org.apache.fineract.client.services.LoanChargesApi; import org.apache.fineract.client.services.LoanCollateralApi; +import org.apache.fineract.client.services.LoanDisbursementDetailsApi; import org.apache.fineract.client.services.LoanProductsApi; import org.apache.fineract.client.services.LoanReschedulingApi; import org.apache.fineract.client.services.LoanTransactionsApi; @@ -208,6 +209,7 @@ public final class FineractClient { public final LoanProductsApi loanProducts; public final LoanReschedulingApi loanSchedules; public final LoansApi loans; + public final LoanDisbursementDetailsApi loanDisbursementDetails; public final LoanTransactionsApi loanTransactions; public final MakerCheckerOr4EyeFunctionalityApi makerCheckers; public final MappingFinancialActivitiesToAccountsApi financialActivyAccountMappings; @@ -318,6 +320,7 @@ public final class FineractClient { loanProducts = retrofit.create(LoanProductsApi.class); loanSchedules = retrofit.create(LoanReschedulingApi.class); loans = retrofit.create(LoansApi.class); + loanDisbursementDetails = retrofit.create(LoanDisbursementDetailsApi.class); loanTransactions = retrofit.create(LoanTransactionsApi.class); makerCheckers = retrofit.create(MakerCheckerOr4EyeFunctionalityApi.class); financialActivyAccountMappings = retrofit.create(MappingFinancialActivitiesToAccountsApi.class); diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java index f8fbcb430..f49003e41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java @@ -20,6 +20,7 @@ package org.apache.fineract.batch.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; @@ -92,9 +93,9 @@ public class BatchApiResource { @Operation(summary = "Batch requests in a single transaction", description = "The Apache Fineract Batch API is also capable of executing all the requests in a single transaction, by setting a Query Parameter, \"enclosingTransaction=true\". So, if one or more of the requests in a batch returns an erroneous response all of the Data base transactions made by other successful requests will be rolled back.\n" + "\n" + "If there has been a rollback in a transaction then a single response will be provided, with a '400' status code and a body consisting of the error details of the first failed request.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = BatchApiResourceSwagger.PostBatchesRequest.class, description = "request body"))) + @RequestBody(required = true, content = @Content(array = @ArraySchema(schema = @Schema(implementation = BatchRequest.class, description = "request body")))) @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Success", content = @Content(schema = @Schema(implementation = BatchResponse.class))) }) + @ApiResponse(responseCode = "200", description = "Success", content = @Content(array = @ArraySchema(schema = @Schema(implementation = BatchResponse.class)))) }) public String handleBatchRequests( @DefaultValue("false") @QueryParam("enclosingTransaction") @Parameter(description = "enclosingTransaction", required = false) final boolean enclosingTransaction, @Parameter(hidden = true) final String jsonRequestString, @Context UriInfo uriInfo) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java index c8316fe91..d14afd0d7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java @@ -84,6 +84,9 @@ public class CommandStrategyProvider { this.commandStrategies.put(CommandContext.resource("clients").method("POST").build(), "createClientCommandStrategy"); this.commandStrategies.put(CommandContext.resource("clients\\/\\d+").method("PUT").build(), "updateClientCommandStrategy"); this.commandStrategies.put(CommandContext.resource("loans").method("POST").build(), "applyLoanCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("loans\\/\\d+").method("GET").build(), "getLoanByIdCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("loans/\\d+(\\?(\\w+(?:\\=[\\w,]+|&)+)+)").method("GET").build(), + "getLoanByIdCommandStrategy"); this.commandStrategies.put(CommandContext.resource("savingsaccounts").method("POST").build(), "applySavingsCommandStrategy"); this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/charges").method("POST").build(), "createChargeCommandStrategy"); this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/charges").method("GET").build(), @@ -98,6 +101,8 @@ public class CommandStrategyProvider { "disburseLoanCommandStrategy"); this.commandStrategies.put(CommandContext.resource("rescheduleloans\\/\\d+\\?command=approve").method("POST").build(), "approveLoanRescheduleCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/transactions\\/\\d+").method("GET").build(), + "getTransactionByIdCommandStrategy"); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategy.java new file mode 100644 index 000000000..7408aee42 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategy.java @@ -0,0 +1,138 @@ +/** + * 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.batch.command.internal; + +import java.util.HashMap; +import java.util.Map; +import javax.ws.rs.core.UriInfo; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.batch.exception.ErrorHandler; +import org.apache.fineract.batch.exception.ErrorInfo; +import org.apache.fineract.infrastructure.core.api.MutableUriInfo; +import org.apache.fineract.portfolio.loanaccount.api.LoansApiResource; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Implements {@link CommandStrategy} and get loan by id. It passes the contents of the body from the BatchRequest to + * {@link LoansApiResource} and gets back the response. This class will also catch any errors raised by + * {@link LoansApiResource} and map those errors to appropriate status codes in BatchResponse. + */ +@Component +@RequiredArgsConstructor +public class GetLoanByIdCommandStrategy implements CommandStrategy { + + /** + * Loans api resource {@link LoansApiResource}. + */ + private final LoansApiResource loansApiResource; + + @Override + public BatchResponse execute(final BatchRequest request, final UriInfo uriInfo) { + final MutableUriInfo parameterizedUriInfo = new MutableUriInfo(uriInfo); + + final BatchResponse response = new BatchResponse(); + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); + + final String relativeUrl = request.getRelativeUrl(); + Long loanId; + + // Try-catch blocks to map exceptions to appropriate status codes + try { + // uriInfo will contain the query parameter value(s) that are sent in the actual batch uri. + // for example: batches?enclosingTransaction=true + // But the query parameters that are sent in the batch relative url has to be sent to + // LoansApiResource.retrieveLoan + // To use the relative url query parameters + // - Parse and fetch the query parameters sent in the relative url + // (loans/66?fields=id,principal,annualInterestRate) + // - Add them to the UriInfo query parameters list + // - Call loansApiResource.retrieveLoan(loanId, false, uriInfo) + // - Remove the relative url query parameters from UriInfo in the finally (after loan details are retrieved) + if (relativeUrl.indexOf('?') > 0) { + loanId = Long.parseLong(StringUtils.substringBetween(relativeUrl, "/", "?")); + Map queryParameters = getQueryParameters(relativeUrl); + + // Add the query parameters sent in the relative URL to UriInfo + addQueryParametersToUriInfo(parameterizedUriInfo, queryParameters); + } else { + loanId = Long.parseLong(StringUtils.substringAfter(relativeUrl, "/")); + } + + // Calls 'retrieveLoan' function from 'LoansApiResource' to + // get the loan details based on the loan id + responseBody = loansApiResource.retrieveLoan(loanId, false, parameterizedUriInfo); + + response.setStatusCode(HttpStatus.SC_OK); + + // Sets the response after retrieving the loan + response.setBody(responseBody); + + } catch (RuntimeException e) { + + // Gets an object of type ErrorInfo, containing information about + // raised exception + ErrorInfo ex = ErrorHandler.handler(e); + + response.setStatusCode(ex.getStatusCode()); + response.setBody(ex.getMessage()); + } + + return response; + } + + /** + * Get query parameters from relative URL. + * + * @param relativeUrl + * the relative URL + * @return the query parameters in a map + */ + private Map getQueryParameters(final String relativeUrl) { + final String queryParameterStr = StringUtils.substringAfter(relativeUrl, "?"); + final String[] queryParametersArray = StringUtils.split(queryParameterStr, "&"); + final Map queryParametersMap = new HashMap<>(); + for (String parameterStr : queryParametersArray) { + String[] keyValue = StringUtils.split(parameterStr, "="); + queryParametersMap.put(keyValue[0], keyValue[1]); + } + return queryParametersMap; + } + + /** + * Add query parameters(received in the relative URL) to URI info query parameters. + * + * @param uriInfo + * the URI info + * @param queryParameters + * the query parameters + */ + private void addQueryParametersToUriInfo(final MutableUriInfo uriInfo, final Map queryParameters) { + for (Map.Entry entry : queryParameters.entrySet()) { + uriInfo.addAdditionalQueryParameter(entry.getKey(), entry.getValue()); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategy.java new file mode 100644 index 000000000..e736c6a54 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategy.java @@ -0,0 +1,87 @@ +/** + * 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.batch.command.internal; + +import com.google.common.base.Splitter; +import java.util.List; +import javax.ws.rs.core.UriInfo; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.batch.exception.ErrorHandler; +import org.apache.fineract.batch.exception.ErrorInfo; +import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Implements {@link CommandStrategy} to retrieve a transaction by id. It passes the contents of the body from the + * BatchRequest to {@link LoanTransactionsApiResource} and gets back the response. This class will also catch any errors + * raised by {@link LoanTransactionsApiResource} and map those errors to appropriate status codes in BatchResponse. + * + * @see CommandStrategy + * @see BatchRequest + * @see BatchResponse + */ +@Component +@RequiredArgsConstructor +public class GetTransactionByIdCommandStrategy implements CommandStrategy { + + private final LoanTransactionsApiResource loanTransactionsApiResource; + + @Override + public BatchResponse execute(final BatchRequest request, UriInfo uriInfo) { + + final BatchResponse response = new BatchResponse(); + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); + + // Get the loan and transaction ids for use in loanTransactionsApiResource + final List pathParameters = Splitter.on('/').splitToList(request.getRelativeUrl()); + final Long loanId = Long.parseLong(pathParameters.get(1)); + final Long transactionId = Long.parseLong(pathParameters.get(3)); + + // Try-catch blocks to map exceptions to appropriate status codes + try { + + // Calls 'retrieveTransaction' function from 'loanTransactionsApiResource' + responseBody = loanTransactionsApiResource.retrieveTransaction(loanId, transactionId, uriInfo); + + response.setStatusCode(HttpStatus.SC_OK); + + // Sets the body of the response after retrieving the transaction + response.setBody(responseBody); + + } catch (RuntimeException e) { + + // Gets an object of type ErrorInfo, containing information about + // raised exception + ErrorInfo ex = ErrorHandler.handler(e); + + response.setStatusCode(ex.getStatusCode()); + response.setBody(ex.getMessage()); + } + + return response; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/AbstractWorkbookPopulator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/AbstractWorkbookPopulator.java index 0251a234a..8a9bfe7f3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/AbstractWorkbookPopulator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/AbstractWorkbookPopulator.java @@ -46,7 +46,7 @@ public abstract class AbstractWorkbookPopulator implements WorkbookPopulator { } protected void writeLong(int colIndex, Row row, long value) { - row.createCell(colIndex).setCellValue(value); + row.createCell(colIndex).setCellValue((double) value); } protected void writeString(int colIndex, Row row, String value) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/MutableUriInfo.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/MutableUriInfo.java new file mode 100644 index 000000000..cba28fa5d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/MutableUriInfo.java @@ -0,0 +1,147 @@ +/** + * 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.infrastructure.core.api; + +import java.net.URI; +import java.util.List; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MutableUriInfo implements UriInfo { + + private final UriInfo delegate; + + @Getter + private final MultivaluedMap additionalQueryParameters = new MultivaluedHashMap<>(); + + @Override + public MultivaluedMap getQueryParameters() { + return fillAdditionalQueryParameters(delegate.getQueryParameters()); + } + + @Override + public MultivaluedMap getQueryParameters(boolean decode) { + return fillAdditionalQueryParameters(delegate.getQueryParameters(decode)); + } + + private MultivaluedMap fillAdditionalQueryParameters(MultivaluedMap queryParameters) { + MultivaluedMap newQueryParameters = new MultivaluedHashMap<>(queryParameters); + newQueryParameters.putAll(additionalQueryParameters); + return newQueryParameters; + } + + public void addAdditionalQueryParameter(String key, String value) { + additionalQueryParameters.add(key, value); + } + + public void putAdditionalQueryParameter(String key, List values) { + additionalQueryParameters.put(key, values); + } + + @Override + public String getPath() { + return delegate.getPath(); + } + + @Override + public String getPath(boolean decode) { + return delegate.getPath(decode); + } + + @Override + public List getPathSegments() { + return delegate.getPathSegments(); + } + + @Override + public List getPathSegments(boolean decode) { + return delegate.getPathSegments(decode); + } + + @Override + public URI getRequestUri() { + return delegate.getRequestUri(); + } + + @Override + public UriBuilder getRequestUriBuilder() { + return delegate.getRequestUriBuilder(); + } + + @Override + public URI getAbsolutePath() { + return delegate.getAbsolutePath(); + } + + @Override + public UriBuilder getAbsolutePathBuilder() { + return delegate.getAbsolutePathBuilder(); + } + + @Override + public URI getBaseUri() { + return delegate.getBaseUri(); + } + + @Override + public UriBuilder getBaseUriBuilder() { + return delegate.getBaseUriBuilder(); + } + + @Override + public MultivaluedMap getPathParameters() { + return delegate.getPathParameters(); + } + + @Override + public MultivaluedMap getPathParameters(boolean decode) { + return delegate.getPathParameters(decode); + } + + @Override + public List getMatchedURIs() { + return delegate.getMatchedURIs(); + } + + @Override + public List getMatchedURIs(boolean decode) { + return delegate.getMatchedURIs(decode); + } + + @Override + public List getMatchedResources() { + return delegate.getMatchedResources(); + } + + @Override + public URI resolve(URI uri) { + return delegate.resolve(uri); + } + + @Override + public URI relativize(URI uri) { + return delegate.relativize(uri); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java index 4918f77d4..41ceaec4d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -306,7 +306,7 @@ public class DatatablesApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update Entry in Data Table (One to One)", description = "Updates the row (if it exists) of the data table.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.PutDataTablesAppTableIdRequest.class))) + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = String.class))) @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.PutDataTablesAppTableIdResponse.class))) }) public String updateDatatableEntryOnetoOne(@PathParam("datatable") @Parameter(description = "datatable") final String datatable, @@ -328,7 +328,7 @@ public class DatatablesApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update Entry in Data Table (One to Many)", description = "Updates the row (if it exists) of the data table.") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.PutDataTablesAppTableIdDatatableIdRequest.class))) + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = String.class))) @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.PutDataTablesAppTableIdDatatableIdResponse.class))) }) public String updateDatatableEntryOneToMany(@PathParam("datatable") @Parameter(description = "datatable") final String datatable, diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResourceSwagger.java index 379d51fe4..7ae68b4c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResourceSwagger.java @@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.dataqueries.api; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; +import java.util.Map; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; @@ -218,17 +219,6 @@ final class DatatablesApiResourceSwagger { public List data; } - @Schema(description = "PutDataTablesAppTableIdRequest") - public static final class PutDataTablesAppTableIdRequest { - - private PutDataTablesAppTableIdRequest() { - - } - - @Schema(example = "Livestock sales updated") - public String BusinessDescription; - } - @Schema(description = "PutDataTablesAppTableIdResponse") public static final class PutDataTablesAppTableIdResponse { @@ -236,40 +226,15 @@ final class DatatablesApiResourceSwagger { } - static final class PutDataTablesAppTableIdResponseChanges { - - private PutDataTablesAppTableIdResponseChanges() {} - - @Schema(example = "Livestock sales updated") - public String BusinessDescription; - } - + @Schema(example = "1") + public Long officeId; + @Schema(example = "1") + public Long clientId; + @Schema(example = "1") + public Long loanId; @Schema(example = "1") public Long resourceId; - public PutDataTablesAppTableIdResponseChanges changes; - } - - @Schema(description = "PutDataTablesAppTableIdDatatableIdRequest") - public static final class PutDataTablesAppTableIdDatatableIdRequest { - - private PutDataTablesAppTableIdDatatableIdRequest() { - - } - - @Schema(example = "01 June 1982") - public String DateOfBirth; - @Schema(example = "5") - public Long Education_cdHighest; - @Schema(example = "June") - public String Name; - @Schema(example = "More notes") - public String OtherNotes; - @Schema(example = "20") - public Long PointsScore; - @Schema(example = "dd MMMM yyyy") - public String dateFormat; - @Schema(example = "en") - public String locale; + public Map changes; } @Schema(description = "PutDataTablesAppTableIdDatatableIdResponse ") @@ -279,8 +244,15 @@ final class DatatablesApiResourceSwagger { } + @Schema(example = "1") + public Long officeId; + @Schema(example = "1") + public Long clientId; + @Schema(example = "1") + public Long loanId; @Schema(example = "1") public Long resourceId; + public Map changes; } @Schema(description = "DeleteDataTablesDatatableAppTableIdResponse ") diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java index 4b2ba30bc..4a59beda4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResourceSwagger.java @@ -51,7 +51,7 @@ final class CurrenciesApiResourceSwagger { @Schema(example = "[\"KES\",\n" + " \"BND\",\n" + " \"LBP\",\n" + " \"GHC\",\n" + " \"USD\",\n" + " \"XOF\",\n" + " \"AED\",\n" + " \"AMD\"]") - public String currencies; + public String[] currencies; } @@ -64,6 +64,6 @@ final class CurrenciesApiResourceSwagger { @Schema(example = "[\"KES\",\n" + " \"BND\",\n" + " \"LBP\",\n" + " \"GHC\",\n" + " \"USD\",\n" + " \"XOF\",\n" + " \"AED\",\n" + " \"AMD\"]") - public String currencies; + public String[] currencies; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java index c832dc0b1..5aece98ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResourceSwagger.java @@ -135,6 +135,10 @@ final class ChargesApiResourceSwagger { public Integer chargePaymentMode; @Schema(example = "true") public String active; + @Schema(example = "dd MMMM") + public String monthDayFormat; + @Schema(example = "false") + public String penalty; } @Schema(description = "PostChargesResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientsApiResourceSwagger.java index 4c9fce88a..2fe5583d3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientsApiResourceSwagger.java @@ -302,6 +302,8 @@ final class ClientsApiResourceSwagger { public String externalId; @Schema(example = "Client_LastName") public String lastname; + @Schema(example = "[2013, 1, 1]") + public LocalDate dateOfBirth; @Schema(example = "1") public Integer groupId; @Schema(example = "dd MMMM yyyy") @@ -312,6 +314,8 @@ final class ClientsApiResourceSwagger { public Boolean active; @Schema(example = "04 March 2009") public String activationDate; + @Schema(example = "+353851239876") + public String mobileNo; @Schema(description = "List of PostClientsDatatable") public List datatables; @Schema(description = "Address requests") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index ea6c42087..b66f1af42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -133,6 +133,8 @@ final class LoanTransactionsApiResourceSwagger { public GetLoansType type; @Schema(example = "[2012, 5, 14]") public LocalDate date; + @Schema(example = "[2012, 5, 14]") + public LocalDate submittedOnDate; @Schema(example = "false") public Boolean manuallyReversed; public GetLoansCurrency currency; @@ -146,6 +148,21 @@ final class LoanTransactionsApiResourceSwagger { public static final class PostLoansLoanIdTransactionsRequest { private PostLoansLoanIdTransactionsRequest() {} + + @Schema(example = "en_GB") + public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; + @Schema(example = "[2012, 5, 25]") + public LocalDate transactionDate; + @Schema(example = "50000.00") + public Double transactionAmount; + @Schema(example = "An optional note about why your adjusting or changing the transaction.") + public String note; + @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002") + public String externalId; + @Schema(example = "3") + public Integer paymentTypeId; } @Schema(description = "PostLoansLoanIdTransactionsResponse") @@ -170,8 +187,8 @@ final class LoanTransactionsApiResourceSwagger { public String locale; @Schema(example = "dd MMMM yyyy") public String dateFormat; - @Schema(example = "25 May 2012") - public String transactionDate; + @Schema(example = "[2012, 5, 25]") + public LocalDate transactionDate; @Schema(example = "50,000.00") public Double transactionAmount; @Schema(example = "An optional note about why your adjusting or changing the transaction.") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index dfc05cbf3..ce10d20ca 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.api; import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.Set; /** @@ -230,6 +231,124 @@ final class LoansApiResourceSwagger { public String disbursedByLastname; @Schema(example = "[2012, 4, 10]") public LocalDate expectedMaturityDate; + @Schema(example = "[2012, 4, 3]") + public LocalDate closedOnDate; + } + + static final class GetLoansLoanIdRepaymentSchedule { + + private GetLoansLoanIdRepaymentSchedule() {} + + public GetLoansLoanIdCurrency currency; + @Schema(example = "30") + public Long loanTermInDays; + @Schema(example = "200.000000") + public Double totalPrincipalDisbursed; + @Schema(example = "200.00") + public Double totalPrincipalExpected; + @Schema(example = "200.00") + public Double totalPrincipalPaid; + @Schema(example = "0.00") + public Double totalInterestCharged; + @Schema(example = "0.00") + public Double totalFeeChargesCharged; + @Schema(example = "0.00") + public Double totalPenaltyChargesCharged; + @Schema(example = "0.00") + public Double totalWaived; + @Schema(example = "0.00") + public Double totalWrittenOff; + @Schema(example = "200.00") + public Double totalRepaymentExpected; + @Schema(example = "200.00") + public Double totalPaidInAdvance; + @Schema(example = "0.00") + public Double totalPaidLate; + @Schema(example = "0.00") + public Double totalOutstanding; + public Set periods; + } + + static final class GetLoansLoanIdRepaymentPeriod { + + private GetLoansLoanIdRepaymentPeriod() {} + + @Schema(example = "1") + public Integer period; + @Schema(example = "[2012, 4, 3]") + public LocalDate fromDate; + @Schema(example = "[2012, 4, 3]") + public LocalDate dueDate; + @Schema(example = "[2012, 4, 3]") + public LocalDate obligationsMetOnDate; + @Schema(example = "true") + public Boolean complete; + @Schema(example = "30") + public Long daysInPeriod; + @Schema(example = "200.000000") + public Double principalOriginalDue; + @Schema(example = "200.000000") + public Double principalDue; + @Schema(example = "200.000000") + public Double principalPaid; + @Schema(example = "0") + public Long principalWrittenOff; + @Schema(example = "20.000000") + public Double principalOutstanding; + @Schema(example = "20.000000") + public Double principalLoanBalanceOutstanding; + @Schema(example = "0") + public Long interestOriginalDue; + @Schema(example = "0") + public Long interestDue; + @Schema(example = "0") + public Long interestPaid; + @Schema(example = "0") + public Long interestWaived; + @Schema(example = "0") + public Long interestWrittenOff; + @Schema(example = "0") + public Long interestOutstanding; + @Schema(example = "0") + public Long feeChargesDue; + @Schema(example = "20") + public Long feeChargesPaid; + @Schema(example = "20") + public Long feeChargesWaived; + @Schema(example = "20") + public Long feeChargesWrittenOff; + @Schema(example = "20") + public Long feeChargesOutstanding; + @Schema(example = "20") + public Long penaltyChargesDue; + @Schema(example = "20") + public Long penaltyChargesPaid; + @Schema(example = "20") + public Long penaltyChargesWaived; + @Schema(example = "20") + public Long penaltyChargesWrittenOff; + @Schema(example = "20") + public Long penaltyChargesOutstanding; + @Schema(example = "20.000000") + public Double totalOriginalDueForPeriod; + @Schema(example = "20.000000") + public Double totalDueForPeriod; + @Schema(example = "20.000000") + public Double totalPaidForPeriod; + @Schema(example = "20.000000") + public Double totalPaidInAdvanceForPeriod; + @Schema(example = "20") + public Long totalPaidLateForPeriod; + @Schema(example = "20") + public Long totalWaivedForPeriod; + @Schema(example = "20") + public Long totalWrittenOffForPeriod; + @Schema(example = "200.000000") + public Double totalOutstandingForPeriod; + @Schema(example = "20") + public Long totalActualCostOfLoanForPeriod; + @Schema(example = "200.000000") + public Double totalInstallmentAmountForPeriod; } static final class GetLoansLoanIdSummary { @@ -448,7 +567,11 @@ final class LoansApiResourceSwagger { public GetLoansLoanIdLoanType loanType; public GetLoansLoanIdCurrency currency; @Schema(example = "1000000") - public Long principal; + public BigDecimal principal; + @Schema(example = "1000.000000") + public Double approvedPrincipal; + @Schema(example = "200.000000") + public Double netDisbursalAmount; @Schema(example = "12") public Integer termFrequency; public GetLoansLoanIdTermPeriodFrequencyType termPeriodFrequencyType; @@ -471,6 +594,7 @@ final class LoansApiResourceSwagger { public Integer transactionProcessingStrategyId; public GetLoansLoanIdTimeline timeline; public GetLoansLoanIdSummary summary; + public GetLoansLoanIdRepaymentSchedule repaymentSchedule; } @Schema(description = "GetLoansResponse") @@ -486,8 +610,20 @@ final class LoansApiResourceSwagger { @Schema(description = "PostLoansRequest") public static final class PostLoansRequest { + static final class PostLoansDisbursementData { + + private PostLoansDisbursementData() {} + + @Schema(example = "[2013, 11, 1]") + public LocalDate expectedDisbursementDate; + @Schema(example = "1000.00") + public Double principal; + } + private PostLoansRequest() {} + @Schema(example = "1") + public Long clientId; @Schema(example = "dd MMMM yyyy") public String dateFormat; @Schema(example = "en_GB") @@ -495,7 +631,7 @@ final class LoansApiResourceSwagger { @Schema(example = "1") public Integer productId; @Schema(example = "100,000.00") - public Double principal; + public String principal; @Schema(example = "12") public Integer loanTermFrequency; @Schema(example = "2") @@ -522,6 +658,14 @@ final class LoansApiResourceSwagger { public Integer transactionProcessingStrategyId; @Schema(example = "360", allowableValues = "1, 360, 364, 36") public Integer daysInYearType; + @Schema(example = "individual") + public String loanType; + @Schema(example = "[2012, 4, 3]") + public LocalDate submittedOnDate; + @Schema(example = "786444UUUYYH7") + public String externalId; + @Schema(description = "List of PostLoansDisbursementData") + public List disbursementData; } @Schema(description = "PostLoansResponse") @@ -583,6 +727,14 @@ final class LoansApiResourceSwagger { @Schema(example = "0") public Long totalOutstanding; public Set periods; + @Schema(example = "2") + public Integer officeId; + @Schema(example = "1") + public Integer clientId; + @Schema(example = "1") + public Integer loanId; + @Schema(example = "1") + public Integer resourceId; } @Schema(description = "PutLoansLoanIdRequest") @@ -670,6 +822,16 @@ final class LoansApiResourceSwagger { private PostLoansLoanIdRequest() {} + static final class PostLoansLoanIdDisbursementData { + + private PostLoansLoanIdDisbursementData() {} + + @Schema(example = "[2012, 4, 3]") + public LocalDate expectedDisbursementDate; + @Schema(example = "22000") + public Double principal; + } + @Schema(example = "2") public Integer toLoanOfficerId; @Schema(example = "02 September 2014") @@ -680,6 +842,24 @@ final class LoansApiResourceSwagger { public String dateFormat; @Schema(example = "") public Integer fromLoanOfficerId; + @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002") + public String externalId; + @Schema(example = "5000.33") + public String transactionAmount; + @Schema(example = "Description of disbursement details.") + public String note; + @Schema(example = "[2012, 4, 12]") + public LocalDate actualDisbursementDate; + @Schema(example = "3") + public Integer paymentTypeId; + @Schema(example = "[2012, 4, 3]") + public LocalDate approvedOnDate; + @Schema(example = "1000") + public String approvedLoanAmount; + @Schema(example = "[2012, 4, 10]") + public LocalDate expectedDisbursementDate; + @Schema(description = "List of PostLoansLoanIdDisbursementData") + public List disbursementData; } @Schema(description = "PostLoansLoanIdResponse") @@ -687,6 +867,50 @@ final class LoansApiResourceSwagger { private PostLoansLoanIdResponse() {} + static final class PostLoansLoanIdStatus { + + private PostLoansLoanIdStatus() {} + + @Schema(example = "300") + public Integer id; + @Schema(example = "loanStatusType.approved") + public String code; + @Schema(example = "Approved") + public String value; + @Schema(example = "false") + public Boolean pendingApproval; + @Schema(example = "false") + public Boolean waitingForDisbursal; + @Schema(example = "true") + public Boolean active; + @Schema(example = "false") + public Boolean closedObligationsMet; + @Schema(example = "false") + public Boolean closedWrittenOff; + @Schema(example = "false") + public Boolean closedRescheduled; + @Schema(example = "false") + public Boolean closed; + @Schema(example = "false") + public Boolean overpaid; + } + + static final class PostLoansLoanIdChanges { + + private PostLoansLoanIdChanges() {} + + @Schema(example = "en") + public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; + @Schema(example = "[2012, 4, 3]") + public LocalDate approvedOnDate; + @Schema(example = "Loan approval note") + public String note; + @Schema(description = "PostLoansLoanIdStatus") + public PostLoansLoanIdStatus status; + } + @Schema(example = "2") public Integer officeId; @Schema(example = "6") @@ -695,5 +919,7 @@ final class LoansApiResourceSwagger { public Integer loanId; @Schema(example = "3") public Integer resourceId; + @Schema(description = "PostLoansLoanIdChanges") + public PostLoansLoanIdChanges changes; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 12a804bd8..222e4a0e9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -45,6 +45,7 @@ import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -1953,7 +1954,7 @@ public class Loan extends AbstractPersistableCustom { private void removeChargesByDisbursementID(Long id) { List tempCharges = new ArrayList<>(); - for (LoanCharge charge : this.charges) { + for (LoanCharge charge : getCharges()) { LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); if (transCharge != null && id.equals(transCharge.getloanDisbursementDetails().getId())) { tempCharges.add(charge); @@ -1966,7 +1967,7 @@ public class Loan extends AbstractPersistableCustom { private List fetchLoanTrancheChargeIds() { List list = new ArrayList<>(); - for (LoanCharge charge : this.charges) { + for (LoanCharge charge : getCharges()) { if (charge.isTrancheDisbursementCharge() && charge.isActive()) { list.add(charge.getId()); } @@ -6887,4 +6888,15 @@ public class Loan extends AbstractPersistableCustom { this.netDisbursalAmount = adjustedAmount.subtract(this.deriveSumTotalOfChargesDueAtDisbursement()); } + /** + * Get the charges. + * + * @return the charges + */ + public Collection getCharges() { + // At the time of loan creation, "this.charges" will be null if no charges found in the request. + // In that case, fetch loan (before commit) will return null for the charges. + // Return empty set instead of null to avoid NPE + return Optional.ofNullable(this.charges).orElse(new HashSet<>()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 4759d4b27..49b996356 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -27,6 +27,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -53,13 +54,10 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCa import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +@Slf4j public final class LoanApplicationTerms { - private static final Logger LOG = LoggerFactory.getLogger(LoanApplicationTerms.class); - private final ApplicationCurrency currency; private final Calendar loanCalendar; @@ -675,7 +673,7 @@ public final class LoanApplicationTerms { case INVALID: break; case WHOLE_TERM: - LOG.error("TODO Implement getPeriodEndDate for WHOLE_TERM"); + log.error("TODO Implement getPeriodEndDate for WHOLE_TERM"); break; } return dueRepaymentPeriodDate; @@ -1080,7 +1078,7 @@ public final class LoanApplicationTerms { periodicInterestRate = oneDayOfYearInterestRate.multiply(numberOfDaysInPeriod, mc); break; case WHOLE_TERM: - LOG.error("TODO Implement periodicInterestRate for WHOLE_TERM"); + log.error("TODO Implement periodicInterestRate for WHOLE_TERM"); break; } break; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index c9504b4b2..9afbe449e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -2109,7 +2109,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " (SUM(COALESCE(mr.principal_completed_derived, 0)) " + " + SUM(COALESCE(mr.interest_completed_derived, 0)) " + " + SUM(COALESCE(mr.fee_charges_completed_derived, 0)) " + "+ SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0"; - BigDecimal bigDecimal = this.jdbcTemplate.queryForObject(sql, BigDecimal.class, new Object[] { loanId }); // NOSONAR + BigDecimal bigDecimal = this.jdbcTemplate.queryForObject(sql, BigDecimal.class, loanId); // NOSONAR return new PaidInAdvanceData(bigDecimal); } catch (DataAccessException e) { return new PaidInAdvanceData(new BigDecimal(0)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index ee5772b7a..e1493b608 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -546,7 +546,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf createStandingInstruction(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - } final Set loanCharges = loan.charges(); @@ -585,9 +584,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_DISBURSAL, constructEntityMap(BusinessEntity.LOAN, loan)); + + Long entityId = loan.getId(); + + // During a disbursement, the entityId should be the disbursement transaction id + if (!isAccountTransfer) { + entityId = loan.getLoanTransactions().get(loan.getLoanTransactions().size() - 1).getId(); + } + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // - .withEntityId(loan.getId()) // + .withEntityId(entityId) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 6d18aaf66..28ead7a7b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -21,8 +21,11 @@ package org.apache.fineract.portfolio.loanproduct.api; import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Collection; import java.util.List; import java.util.Set; +import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; +import org.apache.fineract.portfolio.rate.data.RateData; /** * Created by Chirag Gupta on 12/27/17. @@ -36,6 +39,36 @@ final class LoanProductsApiResourceSwagger { private PostLoanProductsRequest() {} + static final class AllowAttributeOverrides { + + private AllowAttributeOverrides() {} + + @Schema(example = "true") + public boolean amortizationType; + @Schema(example = "true") + public boolean interestType; + @Schema(example = "true") + public boolean transactionProcessingStrategyId; + @Schema(example = "true") + public boolean interestCalculationPeriodType; + @Schema(example = "true") + public boolean inArrearsTolerance; + @Schema(example = "true") + public boolean repaymentEvery; + @Schema(example = "true") + public boolean graceOnPrincipalAndInterestPayment; + @Schema(example = "true") + public boolean graceOnArrearsAgeing; + } + + static final class ChargeId { + + private ChargeId() {} + + @Schema(example = "1") + public Integer id; + } + @Schema(example = "LP Accrual Accounting") public String name; @Schema(example = "LPAA") @@ -98,6 +131,66 @@ final class LoanProductsApiResourceSwagger { public Integer overpaymentLiabilityAccountId; @Schema(example = "41") public Integer writeOffAccountId; + @Schema(example = "false") + public Boolean includeInBorrowerCycle; + @Schema(example = "false") + public Boolean useBorrowerCycle; + @Schema(example = "[]") + public List principalVariationsForBorrowerCycle; + @Schema(example = "[]") + public List interestRateVariationsForBorrowerCycle; + @Schema(example = "[]") + public List numberOfRepaymentVariationsForBorrowerCycle; + @Schema(example = "true") + public Boolean multiDisburseLoan; + @Schema(example = "0") + public Integer interestRecalculationCompoundingMethod; + @Schema(example = "2") + public Integer rescheduleStrategyMethod; + @Schema(example = "1") + public Integer preClosureInterestCalculationStrategy; + @Schema(example = "false") + public Boolean isLinkedToFloatingInterestRates; + @Schema(example = "false") + public Boolean allowVariableInstallments; + @Schema(example = "non-interest bearing product") + public String description; + @Schema(example = "1") + public Integer installmentAmountInMultiplesOf; + @Schema(example = "5000.000000") + public Double minPrincipal; + @Schema(example = "15000.000000") + public Double maxPrincipal; + @Schema(example = "30") + public Integer minimumDaysBetweenDisbursalAndFirstRepayment; + @Schema(example = "true") + public Boolean allowPartialPeriodInterestCalcualtion; + @Schema(example = "true") + public Boolean disallowExpectedDisbursements; + @Schema(example = "true") + public Boolean allowApprovedDisbursedAmountsOverApplied; + @Schema(example = "percentage") + public String overAppliedCalculationType; + @Schema(example = "50") + public Integer overAppliedNumber; + @Schema(example = "true") + public Boolean canDefineInstallmentAmount; + @Schema(example = "50") + public Integer principalThresholdForLastInstallment; + @Schema(example = "50") + public Integer recalculationRestFrequencyType; + @Schema(example = "3") + public Integer maxTrancheCount; + @Schema(example = "36000.000000") + public Double outstandingLoanBalance; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; + public Set paymentChannelToFundSourceMappings; + public Set feeToIncomeAccountMappings; + public Collection penaltyToIncomeAccountMappings; + public AllowAttributeOverrides allowAttributeOverrides; + public List charges; + public List rates; } @Schema(description = "PostLoanProductsResponse") @@ -313,11 +406,11 @@ final class LoanProductsApiResourceSwagger { public String status; public GetLoanProductsCurrency currency; @Schema(example = "10000.000000") - public Float principal; + public Double principal; @Schema(example = "5000.000000") - public Float minPrincipal; + public Double minPrincipal; @Schema(example = "15000.000000") - public Float maxPrincipal; + public Double maxPrincipal; @Schema(example = "10") public Integer numberOfRepayments; @Schema(example = "5") @@ -328,10 +421,10 @@ final class LoanProductsApiResourceSwagger { public Integer repaymentEvery; public GetLoanProductsRepaymentFrequencyType repaymentFrequencyType; @Schema(example = "15.000000") - public Float interestRatePerPeriod; + public Double interestRatePerPeriod; public GetLoanProductsResponse.GetLoanProductsInterestRateFrequencyType interestRateFrequencyType; @Schema(example = "15.000000") - public Float annualInterestRate; + public Double annualInterestRate; public GetLoanProductsAmortizationType amortizationType; @Schema(example = "5.5") public BigDecimal fixedPrincipalPercentagePerInstallment; @@ -862,11 +955,11 @@ final class LoanProductsApiResourceSwagger { public GetLoanProductsParamType paramType; public GetLoanProductsValueConditionType valueConditionType; @Schema(example = "2000.000000") - public Float minValue; + public Double minValue; @Schema(example = "20000.000000") - public Float maxValue; + public Double maxValue; @Schema(example = "15000.000000") - public Float defaultValue; + public Double defaultValue; } static final class GetLoanAccountingMappings { @@ -1033,21 +1126,21 @@ final class LoanProductsApiResourceSwagger { public String status; public GetLoanProductsResponse.GetLoanProductsCurrency currency; @Schema(example = "10000.000000") - public Float principal; + public Double principal; @Schema(example = "2000.000000") - public Float minPrincipal; + public Double minPrincipal; @Schema(example = "15000.000000") - public Float maxPrincipal; + public Double maxPrincipal; @Schema(example = "7") public Integer numberOfRepayments; @Schema(example = "7") public Integer repaymentEvery; public GetLoanProductsResponse.GetLoanProductsRepaymentFrequencyType repaymentFrequencyType; @Schema(example = "5.000000") - public Float interestRatePerPeriod; + public Double interestRatePerPeriod; public GetLoanProductsProductIdResponse.GetLoanProductsInterestRateFrequencyType interestRateFrequencyType; @Schema(example = "60.000000") - public Float annualInterestRate; + public Double annualInterestRate; public GetLoanProductsResponse.GetLoanProductsAmortizationType amortizationType; @Schema(example = "5.5") public BigDecimal fixedPrincipalPercentagePerInstallment; @@ -1073,7 +1166,7 @@ final class LoanProductsApiResourceSwagger { @Schema(example = "3") public Integer maxTrancheCount; @Schema(example = "36000.000000") - public Float outstandingLoanBalance; + public Double outstandingLoanBalance; @Schema(example = "2") public Integer overdueDaysForNPA; @Schema(example = "50") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java index 2a74cfada..2f3091c58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java @@ -197,6 +197,8 @@ final class SelfLoansApiResourceSwagger { public String disbursedByLastname; @Schema(example = "[2012, 4, 10]") public LocalDate expectedMaturityDate; + @Schema(example = "[2012, 4, 3]") + public LocalDate closedOnDate; } static final class GetLoansLoanIdSummary { diff --git a/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java b/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java new file mode 100644 index 000000000..7ce584cd1 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java @@ -0,0 +1,134 @@ +/** + * 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.batch.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; +import javax.ws.rs.HttpMethod; +import org.apache.fineract.batch.command.internal.ActivateClientCommandStrategy; +import org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy; +import org.apache.fineract.batch.command.internal.ApplySavingsCommandStrategy; +import org.apache.fineract.batch.command.internal.ApproveLoanCommandStrategy; +import org.apache.fineract.batch.command.internal.ApproveLoanRescheduleCommandStrategy; +import org.apache.fineract.batch.command.internal.CollectChargesCommandStrategy; +import org.apache.fineract.batch.command.internal.CreateChargeCommandStrategy; +import org.apache.fineract.batch.command.internal.CreateClientCommandStrategy; +import org.apache.fineract.batch.command.internal.DisburseLoanCommandStrategy; +import org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy; +import org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy; +import org.apache.fineract.batch.command.internal.RepayLoanCommandStrategy; +import org.apache.fineract.batch.command.internal.UnknownCommandStrategy; +import org.apache.fineract.batch.command.internal.UpdateClientCommandStrategy; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.context.ApplicationContext; + +/** + * Tests {@link CommandStrategyProvider}. + */ +public class CommandStrategyProviderTest { + + /** + * Command strategy provider. + * + * @return the test data stream + */ + private static Stream provideCommandStrategies() { + return Stream.of(Arguments.of("clients", HttpMethod.POST, "createClientCommandStrategy", mock(CreateClientCommandStrategy.class)), + Arguments.of("clients/123", HttpMethod.PUT, "updateClientCommandStrategy", mock(UpdateClientCommandStrategy.class)), + Arguments.of("loans", HttpMethod.POST, "applyLoanCommandStrategy", mock(ApplyLoanCommandStrategy.class)), + Arguments.of("loans/123", HttpMethod.GET, "getLoanByIdCommandStrategy", mock(GetLoanByIdCommandStrategy.class)), + Arguments.of("loans/123?associations=all", HttpMethod.GET, "getLoanByIdCommandStrategy", + mock(GetLoanByIdCommandStrategy.class)), + Arguments.of("loans/123?associations=all&exclude=guarantors", HttpMethod.GET, "getLoanByIdCommandStrategy", + mock(GetLoanByIdCommandStrategy.class)), + Arguments.of("savingsaccounts", HttpMethod.POST, "applySavingsCommandStrategy", mock(ApplySavingsCommandStrategy.class)), + Arguments.of("loans/123/charges", HttpMethod.POST, "createChargeCommandStrategy", mock(CreateChargeCommandStrategy.class)), + Arguments.of("loans/123/charges", HttpMethod.GET, "collectChargesCommandStrategy", + mock(CollectChargesCommandStrategy.class)), + Arguments.of("loans/123/transactions?command=repayment", HttpMethod.POST, "repayLoanCommandStrategy", + mock(RepayLoanCommandStrategy.class)), + Arguments.of("clients/456?command=activate", HttpMethod.POST, "activateClientCommandStrategy", + mock(ActivateClientCommandStrategy.class)), + Arguments.of("loans/123?command=approve", HttpMethod.POST, "approveLoanCommandStrategy", + mock(ApproveLoanCommandStrategy.class)), + Arguments.of("loans/123?command=disburse", HttpMethod.POST, "disburseLoanCommandStrategy", + mock(DisburseLoanCommandStrategy.class)), + Arguments.of("rescheduleloans/123?command=approve", HttpMethod.POST, "approveLoanRescheduleCommandStrategy", + mock(ApproveLoanRescheduleCommandStrategy.class)), + Arguments.of("loans/123/transactions/123", HttpMethod.GET, "getTransactionByIdCommandStrategy", + mock(GetTransactionByIdCommandStrategy.class))); + } + + /** + * Tests {@link CommandStrategyProvider#getCommandStrategy} for success scenarios. + * + * @param url + * the resource URL + * @param httpMethod + * the resource HTTP method + * @param beanName + * the context bean name + * @param commandStrategy + * the command strategy + */ + @ParameterizedTest + @MethodSource("provideCommandStrategies") + public void testGetCommandStrategySuccess(final String url, final String httpMethod, final String beanName, + final CommandStrategy commandStrategy) { + final ApplicationContext applicationContext = mock(ApplicationContext.class); + final CommandStrategyProvider commandStrategyProvider = new CommandStrategyProvider(applicationContext); + when(applicationContext.getBean(beanName)).thenReturn(commandStrategy); + + final CommandStrategy result = commandStrategyProvider.getCommandStrategy(CommandContext.resource(url).method(httpMethod).build()); + assertEquals(commandStrategy, result); + } + + /** + * Command strategy provider for error scenarioss. + * + * @return the test data stream + */ + private static Stream provideCommandStrategyResourceDetailsForErrors() { + return Stream.of(Arguments.of("loans/123?command=reject", HttpMethod.POST), + Arguments.of("loans/glimAccount/746?command=approve", HttpMethod.POST), Arguments.of("loans/123", HttpMethod.PUT)); + } + + /** + * Tests {@link CommandStrategyProvider#getCommandStrategy} for error scenarios. + * + * @param url + * the resource URL + * @param httpMethod + * the resource HTTP method + */ + @ParameterizedTest + @MethodSource("provideCommandStrategyResourceDetailsForErrors") + public void testGetCommandStrategyForError(final String url, final String httpMethod) { + final ApplicationContext applicationContext = mock(ApplicationContext.class); + final CommandStrategyProvider commandStrategyProvider = new CommandStrategyProvider(applicationContext); + + final CommandStrategy result = commandStrategyProvider.getCommandStrategy(CommandContext.resource(url).method(httpMethod).build()); + assertEquals(UnknownCommandStrategy.class, result.getClass()); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategyTest.java b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategyTest.java new file mode 100644 index 000000000..a16cea7a2 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetLoanByIdCommandStrategyTest.java @@ -0,0 +1,186 @@ +/** + * 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.batch.command.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import java.util.stream.Stream; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.infrastructure.core.api.MutableUriInfo; +import org.apache.fineract.portfolio.loanaccount.api.LoansApiResource; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class GetLoanByIdCommandStrategyTest { + + private static Stream provideQueryParameters() { + return Stream.of(Arguments.of(null, 0), Arguments.of("associations=all", 1), + Arguments.of("fields=id,principal,annualInterestRate&associations=repaymentSchedule,transactions", 2)); + } + + /** + * Test {@link GetLoanByIdCommandStrategy#execute} happy path scenario. + * + */ + @ParameterizedTest + @MethodSource("provideQueryParameters") + public void testExecuteSuccessScenario(final String queryParameter, final int noOfQueryParams) { + // given + final TestContext testContext = new TestContext(); + + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, queryParameter); + final String responseBody = "{\\\"id\\\":2,\\\"accountNo\\\":\\\"000000002\\\"}"; + + given(testContext.loansApiResource.retrieveLoan(eq(loanId), eq(false), any(UriInfo.class))).willReturn(responseBody); + + // when + final BatchResponse response = testContext.underTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + assertThat(response.getBody()).isEqualTo(responseBody); + + verify(testContext.loansApiResource).retrieveLoan(eq(loanId), eq(false), testContext.uriInfoCaptor.capture()); + MutableUriInfo mutableUriInfo = testContext.uriInfoCaptor.getValue(); + assertThat(mutableUriInfo.getAdditionalQueryParameters()).hasSize(noOfQueryParams); + } + + /** + * Test {@link GetLoanByIdCommandStrategy#execute} for internal server error. + */ + @Test + public void testExecuteForInternalServerError() { + // given + final TestContext testContext = new TestContext(); + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, null); + + given(testContext.loansApiResource.retrieveLoan(eq(loanId), eq(false), any(UriInfo.class))) + .willThrow(new RuntimeException("Some error")); + + // when + final BatchResponse response = testContext.underTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR); + assertThat(response.getBody()).isEqualTo("{\"Exception\": java.lang.RuntimeException: Some error}"); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + } + + /** + * Test {@link GetLoanByIdCommandStrategy#execute} for loan not found exception. + */ + @Test + public void testExecuteForLoanNotFoundException() { + // given + final TestContext testContext = new TestContext(); + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, null); + + given(testContext.loansApiResource.retrieveLoan(eq(loanId), eq(false), any(UriInfo.class))) + .willThrow(new LoanNotFoundException(loanId)); + + // when + final BatchResponse response = testContext.underTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND); + assertThat(response.getBody()).isEqualTo(buildLoanNotFoundError(loanId)); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + } + + private String buildLoanNotFoundError(final Long loanId) { + return String.format( + "{\n \"developerMessage\": \"The requested resource is not available.\",\n" + + " \"httpStatusCode\": \"404\",\n \"defaultUserMessage\": \"The requested resource is not available.\",\n" + + " \"userMessageGlobalisationCode\": \"error.msg.resource.not.found\",\n \"errors\": [\n {\n" + + " \"developerMessage\": \"Loan with identifier %s does not exist\",\n" + + " \"defaultUserMessage\": \"Loan with identifier %s does not exist\",\n" + + " \"userMessageGlobalisationCode\": \"error.msg.loan.id.invalid\",\n \"parameterName\": \"id\",\n" + + " \"args\": [\n {\n \"value\": %s\n }\n ]\n }\n ]\n" + "}", + loanId, loanId, loanId); + } + + /** + * Creates and returns a request with the given loan id. + * + * @param loanId + * the loan id + * @return BatchRequest + */ + private BatchRequest getBatchRequest(final Long loanId, final String queryParamStr) { + + final BatchRequest br = new BatchRequest(); + String relativeUrl = "loans/" + loanId; + if (queryParamStr != null) { + relativeUrl = relativeUrl + "?" + queryParamStr; + } + + br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5))); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5))); + br.setBody("{}"); + + return br; + } + + /** + * Private test context class used since testng runs in parallel to avoid state between tests + */ + private static class TestContext { + + @Mock + private UriInfo uriInfo; + + @Mock + private LoansApiResource loansApiResource; + + @Captor + private ArgumentCaptor uriInfoCaptor; + + private final GetLoanByIdCommandStrategy underTest; + + TestContext() { + MockitoAnnotations.openMocks(this); + underTest = new GetLoanByIdCommandStrategy(loansApiResource); + } + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategyTest.java b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategyTest.java new file mode 100644 index 000000000..bec2cfdc0 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetTransactionByIdCommandStrategyTest.java @@ -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.batch.command.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class GetTransactionByIdCommandStrategyTest { + + /** + * Test {@link GetTransactionByIdCommandStrategy#execute} happy path scenario. + */ + @Test + public void testExecuteSuccessScenario() { + // given + final TestContext testContext = new TestContext(); + + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final Long transactionId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, transactionId); + final String responseBody = "{\"id\":12,\"officeId\":1,\"officeName\":\"Head Office\",\"type\":{\"id\":10,\"code\":" + + "\"loanTransactionType.accrual\",\"value\":\"Accrual\",\"disbursement\":false,\"repaymentAtDisbursement\":false," + + "\"repayment\":false,\"contra\":false,\"waiveInterest\":false,\"waiveCharges\":false,\"accrual\":true,\"writeOff\":false," + + "\"recoveryRepayment\":false,\"initiateTransfer\":false,\"approveTransfer\":false,\"withdrawTransfer\":false," + + "\"rejectTransfer\":false,\"chargePayment\":false,\"refund\":false,\"refundForActiveLoans\":false},\"date\":[2022,3,29]," + + "\"currency\":{\"code\":\"EUR\",\"name\":\"Euro\",\"decimalPlaces\":2,\"inMultiplesOf\":0,\"displaySymbol\":\"€\"," + + "\"nameCode\":\"currency.EUR\",\"displayLabel\":\"Euro (€)\"},\"amount\":0.000000,\"netDisbursalAmount\":200.000000," + + "\"principalPortion\":0,\"interestPortion\":0.000000,\"feeChargesPortion\":0,\"penaltyChargesPortion\":0,\"overpaymentPortion\":0," + + "\"unrecognizedIncomePortion\":0,\"outstandingLoanBalance\":0,\"submittedOnDate\":[2022,3,29],\"manuallyReversed\":false," + + "\"loanChargePaidByList\":[],\"numberOfRepayments\":0}"; + + given(testContext.loanTransactionsApiResource.retrieveTransaction(loanId, transactionId, testContext.uriInfo)) + .willReturn(responseBody); + + // when + final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + assertThat(response.getBody()).isEqualTo(responseBody); + } + + /** + * Test {@link GetTransactionByIdCommandStrategy#execute} for internal server error. + */ + @Test + public void testExecuteForInternalServerError() { + // given + final TestContext testContext = new TestContext(); + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final Long transactionId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, transactionId); + + given(testContext.loanTransactionsApiResource.retrieveTransaction(loanId, transactionId, testContext.uriInfo)) + .willThrow(new RuntimeException("Some error")); + + // when + final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + assertThat(response.getBody()).isEqualTo("{\"Exception\": java.lang.RuntimeException: Some error}"); + } + + /** + * Test {@link GetTransactionByIdCommandStrategy#execute} for loan not found exception. + */ + @Test + public void testExecuteForLoanNotFoundException() { + // given + final TestContext testContext = new TestContext(); + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final Long transactionId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, transactionId); + + given(testContext.loanTransactionsApiResource.retrieveTransaction(loanId, transactionId, testContext.uriInfo)) + .willThrow(new LoanNotFoundException(loanId)); + + // when + final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND); + assertThat(response.getRequestId()).isEqualTo(request.getRequestId()); + assertThat(response.getHeaders()).isEqualTo(request.getHeaders()); + assertThat(response.getBody()).isEqualTo(build404NotFoundError("Loan", loanId)); + } + + /** + * Test {@link GetTransactionByIdCommandStrategy#execute} for transaction not found exception. + */ + @Test + public void testExecuteForTransactionNotFoundException() { + final TestContext testContext = new TestContext(); + final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final Long transactionId = Long.valueOf(RandomStringUtils.randomNumeric(4)); + final BatchRequest request = getBatchRequest(loanId, transactionId); + + when(testContext.loanTransactionsApiResource.retrieveTransaction(loanId, transactionId, testContext.uriInfo)) + .thenThrow(new LoanTransactionNotFoundException(transactionId)); + + final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo); + + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + assertEquals(build404NotFoundError("Transaction", transactionId), response.getBody()); + assertEquals(request.getRequestId(), response.getRequestId()); + assertEquals(request.getHeaders(), response.getHeaders()); + } + + /** + * Creates and returns a request with the given loan id and transaction id. + * + * @param loanId + * the loan id + * @param transactionId + * the transaction id + * @return BatchRequest + */ + private BatchRequest getBatchRequest(final Long loanId, final Long transactionId) { + + final BatchRequest br = new BatchRequest(); + String relativeUrl = "loans/" + loanId + "/transactions/" + transactionId; + + br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5))); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5))); + br.setBody("{}"); + + return br; + } + + /** + * Builds the 404 not found error. + * + * @param field + * the field name + * @param id + * the id + */ + private String build404NotFoundError(final String field, final Long id) { + return String.format("{\n" + " \"developerMessage\": \"The requested resource is not available.\",\n" + + " \"httpStatusCode\": \"404\",\n" + " \"defaultUserMessage\": \"The requested resource is not available.\",\n" + + " \"userMessageGlobalisationCode\": \"error.msg.resource.not.found\",\n" + " \"errors\": [\n" + " {\n" + + " \"developerMessage\": \"%s with identifier %s does not exist\",\n" + + " \"defaultUserMessage\": \"%s with identifier %s does not exist\",\n" + + " \"userMessageGlobalisationCode\": \"error.msg.loan.id.invalid\",\n" + " \"parameterName\": \"id\",\n" + + " \"args\": [\n" + " {\n" + " \"value\": %s\n" + " }\n" + " ]\n" + " }\n" + " ]\n" + + "}", field, id, field, id, id); + } + + /** + * Private test context class used since testng runs in parallel to avoid state between tests + */ + private static class TestContext { + + @Mock + private UriInfo uriInfo; + + @Mock + private LoanTransactionsApiResource loanTransactionsApiResource; + + private final GetTransactionByIdCommandStrategy subjectToTest; + + TestContext() { + MockitoAnnotations.openMocks(this); + subjectToTest = new GetTransactionByIdCommandStrategy(loanTransactionsApiResource); + } + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java new file mode 100644 index 000000000..8b55f176c --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java @@ -0,0 +1,88 @@ +/** + * 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.portfolio.loanaccount.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests {@link Loan}. + */ +public class LoanTest { + + /** + * Tests {@link Loan#getCharges()} with charges. + */ + @Test + public void testGetChargesWithCharges() { + Loan loan = new Loan(); + ReflectionTestUtils.setField(loan, "charges", Collections.singleton(buildLoanCharge())); + + final Collection chargeIds = loan.getCharges(); + + assertEquals(1, chargeIds.size()); + } + + /** + * Tests {@link Loan#getCharges()} with no charges. + */ + @Test + public void testGetChargesWithNoCharges() { + final Loan loan = new Loan(); + + final Collection chargeIds = loan.getCharges(); + + assertEquals(0, chargeIds.size()); + } + + /** + * Tests {@link Loan#getCharges()} with null to make sure NPE is not thrown. + */ + @Test + public void testGetChargesWithNull() { + final Loan loan = new Loan(); + ReflectionTestUtils.setField(loan, "charges", null); + + final Collection chargeIds = loan.getCharges(); + + assertEquals(0, chargeIds.size()); + } + + /** + * Builds a new loan charge. + * + * @return the {@link LoanCharge} + */ + private LoanCharge buildLoanCharge() { + return new LoanCharge(mock(Loan.class), mock(Charge.class), new BigDecimal(100), new BigDecimal(100), + ChargeTimeType.TRANCHE_DISBURSEMENT, ChargeCalculationType.FLAT, DateUtils.getLocalDateOfTenant(), + ChargePaymentMode.REGULAR, 1, new BigDecimal(100)); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java index 022c1e8a9..59d0ce052 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java @@ -38,6 +38,8 @@ import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -115,7 +117,7 @@ public class BatchApiTest { * rolled back. * * @see org.apache.fineract.batch.command.internal.CreateClientCommandStrategy - * @see org.apache.fineract.batch.api.BatchApiResource + * @see org.apache.fineract.batch.api.BatchesApiResource * @see org.apache.fineract.batch.service.BatchApiService */ @Test @@ -426,7 +428,7 @@ public class BatchApiTest { // Create a createClient Request final BatchRequest br1 = BatchHelper.createClientRequest(4726L, ""); - // Create a activateClient Request + // Create an activateClient Request final BatchRequest br2 = BatchHelper.activateClientRequest(4727L, 4726L); final List batchRequests = new ArrayList<>(); @@ -480,13 +482,13 @@ public class BatchApiTest { // Create a activateClient Request final BatchRequest br2 = BatchHelper.activateClientRequest(4731L, 4730L); - // Create a ApplyLoan Request + // Create an ApplyLoan Request final BatchRequest br3 = BatchHelper.applyLoanRequest(4732L, 4731L, productId, clientCollateralId); - // Create a approveLoan Request + // Create an approveLoan Request final BatchRequest br4 = BatchHelper.approveLoanRequest(4733L, 4732L); - // Create a disburseLoan Request + // Create an disburseLoan Request final BatchRequest br5 = BatchHelper.disburseLoanRequest(4734L, 4733L); final List batchRequests = new ArrayList<>(); @@ -540,10 +542,10 @@ public class BatchApiTest { // Create a createClient Request final BatchRequest br1 = BatchHelper.createActiveClientRequest(4740L, ""); - // Create a ApplyLoan Request + // Create an ApplyLoan Request final BatchRequest br2 = BatchHelper.applyLoanRequest(4742L, 4740L, productId, clientCollateralId); - // Create a approveLoan Request + // Create an approveLoan Request final BatchRequest br3 = BatchHelper.approveLoanRequest(4743L, 4742L); // Create a disburseLoan Request @@ -561,4 +563,195 @@ public class BatchApiTest { Assertions.assertEquals(200L, (long) response.get(2).getStatusCode(), "Verify Status Code 200 for approve Loan"); Assertions.assertEquals(200L, (long) response.get(3).getStatusCode(), "Verify Status Code 200 for disburse Loan"); } + + /** + * Test for the successful disbursement and get loan. A '200' status code is expected on successful responses. + * + * @see org.apache.fineract.batch.command.internal.DisburseLoanCommandStrategy + * @see org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy + */ + @Test + public void shouldReturnOkStatusOnSuccessfulDisbursementAndGetTransaction() { + final String loanProductJSON = new LoanProductTestBuilder() // + .withPrincipal("10000000.00") // + .withNumberOfRepayments("24") // + .withRepaymentAfterEvery("1") // + .withRepaymentTypeAsMonth() // + .withinterestRatePerPeriod("2") // + .withInterestRateFrequencyTypeAsMonths() // + .withAmortizationTypeAsEqualPrincipalPayment() // + .withInterestTypeAsDecliningBalance() // + .currencyDetails("0", "100").build(null); + + final Long applyLoanRequestId = 6730L; + final Long approveLoanRequestId = 6731L; + final Long disburseLoanRequestId = 6732L; + final Long getTransactionRequestId = 6733L; + + // Create product + final Integer productId = new LoanTransactionHelper(this.requestSpec, this.responseSpec).getLoanProductId(loanProductJSON); + + // Create client + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientId); + + // Create an ApplyLoan Request + final BatchRequest batchRequest1 = BatchHelper.applyLoanRequestWithClientId(applyLoanRequestId, clientId, productId); + + // Create an approveLoan Request + final BatchRequest batchRequest2 = BatchHelper.approveLoanRequest(approveLoanRequestId, applyLoanRequestId); + + // Create a disbursement Request + final BatchRequest batchRequest3 = BatchHelper.disburseLoanRequest(disburseLoanRequestId, approveLoanRequestId); + + // Create a getTransaction Request + final BatchRequest batchRequest4 = BatchHelper.getTransactionByIdRequest(getTransactionRequestId, disburseLoanRequestId); + + final List batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4); + + final List responses = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec, this.responseSpec, + BatchHelper.toJsonString(batchRequests)); + + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(0).getStatusCode(), "Verify Status Code 200 for Apply Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(1).getStatusCode(), "Verify Status Code 200 for Approve Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(2).getStatusCode(), "Verify Status Code 200 for Disburse Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(3).getStatusCode(), "Verify Status Code 200 for Get Transaction By Id"); + } + + /** + * Test for the successful create client, creat, approve and get loan. A '200' status code is expected on successful + * responses. + * + * @see org.apache.fineract.batch.command.internal.CreateClientCommandStrategy + * @see org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy + * @see org.apache.fineract.batch.command.internal.ApproveLoanCommandStrategy + * @see org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy + */ + @Test + public void shouldReturnOkStatusOnSuccessfulCreateClientCreateApproveAndGetLoan() { + final String loanProductJSON = new LoanProductTestBuilder() // + .withPrincipal("10000000.00") // + .withNumberOfRepayments("24") // + .withRepaymentAfterEvery("1") // + .withRepaymentTypeAsMonth() // + .withinterestRatePerPeriod("2") // + .withInterestRateFrequencyTypeAsMonths() // + .withAmortizationTypeAsEqualPrincipalPayment() // + .withInterestTypeAsDecliningBalance() // + .currencyDetails("0", "100").build(null); + + final Long createActiveClientRequestId = 4730L; + final Long applyLoanRequestId = 4731L; + final Long approveLoanRequestId = 4732L; + final Long getLoanByIdRequestId = 4733L; + + // Create product + final Integer productId = new LoanTransactionHelper(this.requestSpec, this.responseSpec).getLoanProductId(loanProductJSON); + + // Create createClient Request + final BatchRequest batchRequest1 = BatchHelper.createActiveClientRequest(createActiveClientRequestId, ""); + + // Create an ApplyLoan Request + final BatchRequest batchRequest2 = BatchHelper.applyLoanRequest(applyLoanRequestId, createActiveClientRequestId, productId, null); + + // Create an approveLoan Request + final BatchRequest batchRequest3 = BatchHelper.approveLoanRequest(approveLoanRequestId, applyLoanRequestId); + + // Get loan by id Request + final BatchRequest batchRequest4 = BatchHelper.getLoanByIdRequest(getLoanByIdRequestId, applyLoanRequestId, null); + + final List batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4); + + final List responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(this.requestSpec, this.responseSpec, + BatchHelper.toJsonString(batchRequests)); + + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(0).getStatusCode(), "Verify Status Code 200 for Create Client"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(1).getStatusCode(), "Verify Status Code 200 for Apply Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(2).getStatusCode(), "Verify Status Code 200 for Approve Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(3).getStatusCode(), "Verify Status Code 200 for Get Loan By Id"); + + final FromJsonHelper jsonHelper = new FromJsonHelper(); + final Long loanId = jsonHelper.extractLongNamed("loanId", jsonHelper.parse(responses.get(1).getBody()).getAsJsonObject()); + final Long loanIdInGetResponse = jsonHelper.extractLongNamed("id", jsonHelper.parse(responses.get(3).getBody()).getAsJsonObject()); + final JsonObject statusInGetResponse = jsonHelper.parse(responses.get(3).getBody()).getAsJsonObject().get("status") + .getAsJsonObject(); + + Assertions.assertEquals(loanId, loanIdInGetResponse); + Assertions.assertEquals(LoanStatus.APPROVED.getCode(), jsonHelper.extractStringNamed("code", statusInGetResponse)); + Assertions.assertEquals("Approved", jsonHelper.extractStringNamed("value", statusInGetResponse)); + } + + /** + * Test for the successful creat, approve and get loan. A '200' status code is expected on successful responses. + * + * @see org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy + * @see org.apache.fineract.batch.command.internal.ApproveLoanCommandStrategy + * @see org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy + */ + @Test + public void shouldReturnOkStatusOnSuccessfulCreateApproveAndGetLoan() { + final String loanProductJSON = new LoanProductTestBuilder() // + .withPrincipal("10000000.00") // + .withNumberOfRepayments("24") // + .withRepaymentAfterEvery("1") // + .withRepaymentTypeAsMonth() // + .withinterestRatePerPeriod("2") // + .withInterestRateFrequencyTypeAsMonths() // + .withAmortizationTypeAsEqualPrincipalPayment() // + .withInterestTypeAsDecliningBalance() // + .currencyDetails("0", "100").build(null); + + final Long applyLoanRequestId = 5730L; + final Long approveLoanRequestId = 5731L; + final Long getLoanByIdRequestId = 5732L; + final Long getLoanByIdWithQueryParametersRequestId = 5733L; + + // Create product + final Integer productId = new LoanTransactionHelper(this.requestSpec, this.responseSpec).getLoanProductId(loanProductJSON); + + // Create client + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec); + ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientId); + + // Create an ApplyLoan Request + final BatchRequest batchRequest1 = BatchHelper.applyLoanRequestWithClientId(applyLoanRequestId, clientId, productId); + + // Create an approveLoan Request + final BatchRequest batchRequest2 = BatchHelper.approveLoanRequest(approveLoanRequestId, applyLoanRequestId); + + // Get loan by id Request without query param + final BatchRequest batchRequest3 = BatchHelper.getLoanByIdRequest(getLoanByIdRequestId, applyLoanRequestId, null); + + // Get loan by id Request with query param + final BatchRequest batchRequest4 = BatchHelper.getLoanByIdRequest(getLoanByIdWithQueryParametersRequestId, applyLoanRequestId, + "associations=repaymentSchedule,transactions"); + + final List batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4); + + final List responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(this.requestSpec, this.responseSpec, + BatchHelper.toJsonString(batchRequests)); + + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(0).getStatusCode(), "Verify Status Code 200 for Apply Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(1).getStatusCode(), "Verify Status Code 200 for Approve Loan"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(2).getStatusCode(), + "Verify Status Code 200 for Get Loan By Id without query parameter"); + Assertions.assertEquals(HttpStatus.SC_OK, responses.get(3).getStatusCode(), + "Verify Status Code 200 for Get Loan By Id with query parameter"); + + final FromJsonHelper jsonHelper = new FromJsonHelper(); + final Long loanId = jsonHelper.extractLongNamed("loanId", jsonHelper.parse(responses.get(0).getBody()).getAsJsonObject()); + final Long loanIdInGetResponse = jsonHelper.extractLongNamed("id", jsonHelper.parse(responses.get(2).getBody()).getAsJsonObject()); + final JsonObject statusInGetResponse = jsonHelper.parse(responses.get(2).getBody()).getAsJsonObject().get("status") + .getAsJsonObject(); + + Assertions.assertEquals(loanId, loanIdInGetResponse); + Assertions.assertEquals(LoanStatus.APPROVED.getCode(), jsonHelper.extractStringNamed("code", statusInGetResponse)); + Assertions.assertEquals("Approved", jsonHelper.extractStringNamed("value", statusInGetResponse)); + + // Repayment schedule will not be available in the response + Assertions.assertFalse(responses.get(2).getBody().contains("repaymentSchedule")); + + // Repayment schedule information will be available in the response based on the query parameter + Assertions.assertTrue(responses.get(3).getBody().contains("repaymentSchedule")); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java index 7fca25dfa..5b37a620d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java @@ -26,6 +26,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Map; +import javax.ws.rs.HttpMethod; import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.batch.domain.BatchResponse; import org.junit.jupiter.api.Assertions; @@ -236,14 +237,52 @@ public final class BatchHelper { br.setMethod("POST"); br.setReference(reference); - final String body = "{\"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en_GB\", \"clientId\": \"$.clientId\"," + "\"productId\": " + String body = "{\"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en_GB\", \"clientId\": \"$.clientId\"," + "\"productId\": " + productId + ", \"principal\": \"10,000.00\", \"loanTermFrequency\": 10," + "\"loanTermFrequencyType\": 2, \"loanType\": \"individual\", \"numberOfRepayments\": 10," + "\"repaymentEvery\": 1, \"repaymentFrequencyType\": 2, \"interestRatePerPeriod\": 10," + "\"amortizationType\": 1, \"interestType\": 0, \"interestCalculationPeriodType\": 1," + + "\"transactionProcessingStrategyId\": 1, \"expectedDisbursementDate\": \"10 Jun 2013\","; + + if (clientCollateralId != null) { + body = body + "\"collateral\": [{\"clientCollateralId\": \"" + clientCollateralId.toString() + "\", \"quantity\": \"1\"}],"; + } + + body = body + "\"submittedOnDate\": \"10 Jun 2013\"}"; + + br.setBody(body); + + return br; + } + + /** + * Creates and returns a {@link org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy} request with + * given clientId and product id. + * + * @param requestId + * the request id + * @param clientId + * the client id + * @param productId + * the product id + * @return {@link BatchRequest} + */ + public static BatchRequest applyLoanRequestWithClientId(final Long requestId, final Integer clientId, final Integer productId) { + + final BatchRequest br = new BatchRequest(); + + br.setRequestId(requestId); + br.setRelativeUrl("loans"); + br.setMethod("POST"); + + String body = String.format("{\"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en_GB\", \"clientId\": %s, " + + "\"productId\": %s, \"principal\": \"10,000.00\", \"loanTermFrequency\": 10," + + "\"loanTermFrequencyType\": 2, \"loanType\": \"individual\", \"numberOfRepayments\": 10," + + "\"repaymentEvery\": 1, \"repaymentFrequencyType\": 2, \"interestRatePerPeriod\": 10," + + "\"amortizationType\": 1, \"interestType\": 0, \"interestCalculationPeriodType\": 1," + "\"transactionProcessingStrategyId\": 1, \"expectedDisbursementDate\": \"10 Jun 2013\"," - + "\"collateral\": [{\"clientCollateralId\": \"" + clientCollateralId.toString() + "\", \"quantity\": \"1\"}]," - + "\"submittedOnDate\": \"10 Jun 2013\"}"; + + "\"submittedOnDate\": \"10 Jun 2013\"}", clientId, productId); + br.setBody(body); return br; @@ -419,4 +458,57 @@ public final class BatchHelper { final Integer responseRecords = Utils.performServerGet(requestSpec, responseSpec, CLIENT_URL, "totalFilteredRecords"); Assertions.assertEquals((long) 0, (long) responseRecords, "No records found with given externalId"); } + + /** + * Creates and returns a {@link org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy} + * request with given requestId and reference. + * + * @param requestId + * the request id + * @param reference + * the reference + * @return the {@link BatchRequest} + */ + public static BatchRequest getTransactionByIdRequest(final Long requestId, final Long reference) { + + final BatchRequest br = new BatchRequest(); + String relativeUrl = "loans/$.loanId/transactions/$.resourceId"; + + br.setRequestId(requestId); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setReference(reference); + br.setBody("{}"); + + return br; + } + + /** + * Creates and returns a {@link org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy} request with + * given requestId and reference. + * + * @param requestId + * the request id + * @param reference + * the reference + * @param queryParameter + * the query parameters + * @return the {@link BatchRequest} + */ + public static BatchRequest getLoanByIdRequest(final Long requestId, final Long reference, final String queryParameter) { + + final BatchRequest br = new BatchRequest(); + String relativeUrl = "loans/$.loanId"; + if (queryParameter != null) { + relativeUrl = relativeUrl + "?" + queryParameter; + } + + br.setRequestId(requestId); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setReference(reference); + br.setBody("{}"); + + return br; + } }