Fine tuning the batch APIs and minor adjustments

This commit is contained in:
Arnold Galovics
2022-05-12 11:04:01 +02:00
committed by Arnold Gálovics
parent a89fe53937
commit 77fd4ff4c1
26 changed files with 1703 additions and 89 deletions
@@ -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);
@@ -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) {
@@ -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");
}
}
@@ -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<String, String> 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<String, String> getQueryParameters(final String relativeUrl) {
final String queryParameterStr = StringUtils.substringAfter(relativeUrl, "?");
final String[] queryParametersArray = StringUtils.split(queryParameterStr, "&");
final Map<String, String> 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<String, String> queryParameters) {
for (Map.Entry<String, String> entry : queryParameters.entrySet()) {
uriInfo.addAdditionalQueryParameter(entry.getKey(), entry.getValue());
}
}
}
@@ -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<String> 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;
}
}
@@ -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) {
@@ -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<String, String> additionalQueryParameters = new MultivaluedHashMap<>();
@Override
public MultivaluedMap<String, String> getQueryParameters() {
return fillAdditionalQueryParameters(delegate.getQueryParameters());
}
@Override
public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
return fillAdditionalQueryParameters(delegate.getQueryParameters(decode));
}
private MultivaluedMap<String, String> fillAdditionalQueryParameters(MultivaluedMap<String, String> queryParameters) {
MultivaluedMap<String, String> 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<String> 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<PathSegment> getPathSegments() {
return delegate.getPathSegments();
}
@Override
public List<PathSegment> 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<String, String> getPathParameters() {
return delegate.getPathParameters();
}
@Override
public MultivaluedMap<String, String> getPathParameters(boolean decode) {
return delegate.getPathParameters(decode);
}
@Override
public List<String> getMatchedURIs() {
return delegate.getMatchedURIs();
}
@Override
public List<String> getMatchedURIs(boolean decode) {
return delegate.getMatchedURIs(decode);
}
@Override
public List<Object> getMatchedResources() {
return delegate.getMatchedResources();
}
@Override
public URI resolve(URI uri) {
return delegate.resolve(uri);
}
@Override
public URI relativize(URI uri) {
return delegate.relativize(uri);
}
}
@@ -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,
@@ -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<ResultsetRowData> 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<String, Object> 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<String, Object> changes;
}
@Schema(description = "DeleteDataTablesDatatableAppTableIdResponse ")
@@ -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;
}
}
@@ -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")
@@ -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<PostClientsDatatable> datatables;
@Schema(description = "Address requests")
@@ -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.")
@@ -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<GetLoansLoanIdRepaymentPeriod> 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<PostLoansDisbursementData> disbursementData;
}
@Schema(description = "PostLoansResponse")
@@ -583,6 +727,14 @@ final class LoansApiResourceSwagger {
@Schema(example = "0")
public Long totalOutstanding;
public Set<PostLoansRepaymentSchedulePeriods> 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<PostLoansLoanIdDisbursementData> 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;
}
}
@@ -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<LoanCharge> 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<Long> fetchLoanTrancheChargeIds() {
List<Long> 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<LoanCharge> 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<>());
}
}
@@ -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;
@@ -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));
@@ -546,7 +546,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
createStandingInstruction(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
final Set<LoanCharge> 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()) //
@@ -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<Integer> principalVariationsForBorrowerCycle;
@Schema(example = "[]")
public List<Integer> interestRateVariationsForBorrowerCycle;
@Schema(example = "[]")
public List<Integer> 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<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public Set<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
public Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
public AllowAttributeOverrides allowAttributeOverrides;
public List<ChargeId> charges;
public List<RateData> 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")
@@ -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 {
@@ -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<Arguments> 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<Arguments> 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());
}
}
@@ -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<Arguments> 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<MutableUriInfo> uriInfoCaptor;
private final GetLoanByIdCommandStrategy underTest;
TestContext() {
MockitoAnnotations.openMocks(this);
underTest = new GetLoanByIdCommandStrategy(loansApiResource);
}
}
}
@@ -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);
}
}
}
@@ -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<LoanCharge> 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<LoanCharge> 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<LoanCharge> 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));
}
}
@@ -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<BatchRequest> 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<BatchRequest> 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<BatchRequest> batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4);
final List<BatchResponse> 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<BatchRequest> batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4);
final List<BatchResponse> 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<BatchRequest> batchRequests = Arrays.asList(batchRequest1, batchRequest2, batchRequest3, batchRequest4);
final List<BatchResponse> 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"));
}
}
@@ -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;
}
}