This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 77fd4ff4c Fine tuning the batch APIs and minor adjustments
77fd4ff4c is described below

commit 77fd4ff4c1fc25cb8d2f9955a8b68c2b5657bfcb
Author: Arnold Galovics <[email protected]>
AuthorDate: Wed May 11 11:30:00 2022 +0200

    Fine tuning the batch APIs and minor adjustments
---
 .../fineract/client/util/FineractClient.java       |   3 +
 .../fineract/batch/api/BatchApiResource.java       |   5 +-
 .../batch/command/CommandStrategyProvider.java     |   5 +
 .../internal/GetLoanByIdCommandStrategy.java       | 138 +++++++++++++
 .../GetTransactionByIdCommandStrategy.java         |  87 ++++++++
 .../populator/AbstractWorkbookPopulator.java       |   2 +-
 .../infrastructure/core/api/MutableUriInfo.java    | 147 +++++++++++++
 .../dataqueries/api/DatatablesApiResource.java     |   4 +-
 .../api/DatatablesApiResourceSwagger.java          |  58 ++----
 .../monetary/api/CurrenciesApiResourceSwagger.java |   4 +-
 .../charge/api/ChargesApiResourceSwagger.java      |   4 +
 .../client/api/ClientsApiResourceSwagger.java      |   4 +
 .../api/LoanTransactionsApiResourceSwagger.java    |  21 +-
 .../loanaccount/api/LoansApiResourceSwagger.java   | 230 ++++++++++++++++++++-
 .../portfolio/loanaccount/domain/Loan.java         |  16 +-
 .../loanschedule/domain/LoanApplicationTerms.java  |  10 +-
 .../service/LoanReadPlatformServiceImpl.java       |   2 +-
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |  11 +-
 .../api/LoanProductsApiResourceSwagger.java        | 121 +++++++++--
 .../api/SelfLoansApiResourceSwagger.java           |   2 +
 .../batch/command/CommandStrategyProviderTest.java | 134 ++++++++++++
 .../internal/GetLoanByIdCommandStrategyTest.java   | 186 +++++++++++++++++
 .../GetTransactionByIdCommandStrategyTest.java     | 205 ++++++++++++++++++
 .../portfolio/loanaccount/domain/LoanTest.java     |  88 ++++++++
 .../fineract/integrationtests/BatchApiTest.java    | 207 ++++++++++++++++++-
 .../integrationtests/common/BatchHelper.java       |  98 ++++++++-
 26 files changed, 1703 insertions(+), 89 deletions(-)

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<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());
+        }
+    }
+}
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<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;
+    }
+
+}
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<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);
+    }
+}
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<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 ")
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<PostClientsDatatable> 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<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;
     }
 }
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<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<>());
+    }
 }
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<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()) //
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<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")
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<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());
+    }
+}
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<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);
+        }
+    }
+}
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<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));
+    }
+}
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<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"));
+    }
 }
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;
+    }
 }

Reply via email to