You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by co...@apache.org on 2019/04/18 14:27:39 UTC

[fineract] 01/02: interoperable - service

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

conradsp pushed a commit to branch marta-jankovics-mojaloop-fineract_1.0
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 6e52d686e63e6f15f598acb9f311d51e72a9e184
Author: Marti <ma...@dpc.hu>
AuthorDate: Fri Mar 8 12:01:34 2019 +0100

    interoperable - service
---
 .../interoperation/api/InteropApiResource.java     | 317 +++++++++++++++
 .../interoperation/api/InteropWrapperBuilder.java  |  82 ++++
 .../interoperation/data/ExtensionData.java         |  71 ++++
 .../fineract/interoperation/data/GeoCodeData.java  |  68 ++++
 .../data/InteropIdentifierRequestData.java         |  86 +++++
 .../data/InteropIdentifierResponseData.java        |  58 +++
 .../data/InteropQuoteRequestData.java              | 117 ++++++
 .../data/InteropQuoteResponseData.java             |  88 +++++
 .../interoperation/data/InteropRequestData.java    | 193 +++++++++
 .../interoperation/data/InteropResponseData.java   | 105 +++++
 .../data/InteropTransactionRequestData.java        |  71 ++++
 .../InteropTransactionRequestResponseData.java     |  64 +++
 .../data/InteropTransactionTypeData.java           |  96 +++++
 .../data/InteropTransferRequestData.java           | 111 ++++++
 .../data/InteropTransferResponseData.java          |  79 ++++
 .../fineract/interoperation/data/MoneyData.java    |  86 +++++
 .../interoperation/domain/InteropActionState.java  |  24 ++
 .../interoperation/domain/InteropAmountType.java   |  24 ++
 .../interoperation/domain/InteropIdentifier.java   | 167 ++++++++
 .../domain/InteropIdentifierRepository.java        |  27 ++
 .../domain/InteropIdentifierType.java              |  31 ++
 .../domain/InteropInitiatorType.java               |  26 ++
 .../domain/InteropTransactionRole.java             |  35 ++
 .../domain/InteropTransactionScenario.java         |  28 ++
 .../domain/InteropTransferActionType.java          |  26 ++
 .../handler/CommitInteropTransferHandler.java      |  49 +++
 .../handler/CreateInteropIdentifierHandler.java    |  56 +++
 .../handler/CreateInteropQuoteHandler.java         |  48 +++
 .../handler/CreateInteropRequestHandler.java       |  49 +++
 .../handler/DeleteInteropRequestHandler.java       |  55 +++
 .../handler/PrepareInteropTransferHandler.java     |  49 +++
 .../serialization/InteropDataValidator.java        | 110 ++++++
 .../interoperation/service/InteropService.java     |  60 +++
 .../interoperation/service/InteropServiceImpl.java | 429 +++++++++++++++++++++
 .../fineract/interoperation/util/InteropUtil.java  |  76 ++++
 .../fineract/interoperation/util/MathUtil.java     | 403 +++++++++++++++++++
 36 files changed, 3464 insertions(+)

diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropApiResource.java
new file mode 100644
index 0000000..1aba6a7
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropApiResource.java
@@ -0,0 +1,317 @@
+/**
+ * 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.interoperation.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.interoperation.data.InteropIdentifierRequestData;
+import org.apache.fineract.interoperation.data.InteropIdentifierResponseData;
+import org.apache.fineract.interoperation.data.InteropQuoteRequestData;
+import org.apache.fineract.interoperation.data.InteropQuoteResponseData;
+import org.apache.fineract.interoperation.data.InteropTransactionRequestData;
+import org.apache.fineract.interoperation.data.InteropTransactionRequestResponseData;
+import org.apache.fineract.interoperation.data.InteropTransferRequestData;
+import org.apache.fineract.interoperation.data.InteropTransferResponseData;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.domain.InteropTransferActionType;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_QUOTE;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_REQUEST;
+import static org.apache.fineract.interoperation.util.InteropUtil.ROOT_PATH;
+
+@Path("/interoperation") //api/v1/
+@Component
+@Scope
+@Api(value = ROOT_PATH, description = "")
+public class InteropApiResource {
+
+    private PlatformSecurityContext context;
+    private ApiRequestParameterHelper apiRequestParameterHelper;
+
+    private DefaultToApiJsonSerializer<CommandProcessingResult> jsonSerializer;
+
+    private InteropService interopService;
+    private PortfolioCommandSourceWritePlatformService commandsSourceService;
+
+    @Autowired
+    public InteropApiResource(PlatformSecurityContext context,
+                              ApiRequestParameterHelper apiRequestParameterHelper,
+                              DefaultToApiJsonSerializer<CommandProcessingResult> defaultToApiJsonSerializer,
+                              InteropService interopService,
+                              PortfolioCommandSourceWritePlatformService portfolioCommandSourceWritePlatformService) {
+        this.context = context;
+        this.apiRequestParameterHelper = apiRequestParameterHelper;
+        this.jsonSerializer = defaultToApiJsonSerializer;
+        this.interopService = interopService;
+        this.commandsSourceService = portfolioCommandSourceWritePlatformService;
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("health")
+    @ApiOperation(value = "Query Interoperation Health Request", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK")})
+    public String health(@Context UriInfo uriInfo) {
+        return "OK";
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}")
+    @ApiOperation(value = "Query Interoperation Account by secondary identifier", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String getAccountByIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                         @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                         @Context UriInfo uriInfo) {
+        InteropIdentifierResponseData result = interopService.getAccountByIdentifier(idType, idValue, null);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}/{subIdOrType}")
+    @ApiOperation(value = "Query Interoperation Account by secondary identifier", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String getAccountByIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                         @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                         @PathParam("subIdOrType") @ApiParam(value = "subIdOrType") String subIdOrType,
+                                         @Context UriInfo uriInfo) {
+        InteropIdentifierResponseData result = interopService.getAccountByIdentifier(idType, idValue, subIdOrType);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}")
+    @ApiOperation(value = "Interoperation Identifier registration", httpMethod = "POST", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropIdentifierRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String registerAccountIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                            @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                            @ApiParam(hidden = true) String identifierJson, @Context UriInfo uriInfo)
+            throws Throwable {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().registerAccountIdentifier(idType, idValue, null).withJson(identifierJson).build();
+
+        InteropIdentifierResponseData result = (InteropIdentifierResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}/{subIdOrType}")
+    @ApiOperation(value = "Interoperation Identifier registration", httpMethod = "POST", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropIdentifierRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String registerAccountIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                            @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                            @PathParam("subIdOrType") @ApiParam(value = "subIdOrType") String subIdOrType,
+                                            @ApiParam(hidden = true) String identifierJson, @Context UriInfo uriInfo)
+            throws Throwable {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().registerAccountIdentifier(idType, idValue, subIdOrType).withJson(identifierJson).build();
+
+        InteropIdentifierResponseData result = (InteropIdentifierResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @DELETE
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}")
+    @ApiOperation(value = "Allow Interoperation Identifier registration", httpMethod = "DELETE", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropIdentifierRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String deleteAccountIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                          @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                          @Context UriInfo uriInfo)
+            throws Throwable {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().deleteAccountIdentifier(idType, idValue, null).build();
+
+        InteropIdentifierResponseData result = (InteropIdentifierResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @DELETE
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("parties/{idType}/{idValue}/{subIdOrType}")
+    @ApiOperation(value = "Allow Interoperation Identifier registration", httpMethod = "DELETE", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropIdentifierRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropIdentifierResponseData.class)})
+    public String deleteAccountIdentifier(@PathParam("idType") @ApiParam(value = "idType") InteropIdentifierType idType,
+                                          @PathParam("idValue") @ApiParam(value = "idValue") String idValue,
+                                          @PathParam("subIdOrType") @ApiParam(value = "subIdOrType") String subIdOrType,
+                                          @Context UriInfo uriInfo)
+            throws Throwable {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().deleteAccountIdentifier(idType, idValue, subIdOrType).build();
+
+        InteropIdentifierResponseData result = (InteropIdentifierResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("transactions/{transactionCode}/requests/{requestCode}")
+    @ApiOperation(value = "Query Interoperation Transaction Request", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropTransactionRequestResponseData.class)})
+    public String getTransactionRequest(@PathParam("transactionCode") @ApiParam(value = "transactionCode") String transactionCode,
+                                        @PathParam("requestCode") @ApiParam(value = "requestCode") String requestCode, @Context UriInfo uriInfo) {
+        context.authenticatedUser().validateHasReadPermission(ENTITY_NAME_REQUEST);
+
+        InteropTransactionRequestResponseData result = interopService.getTransactionRequest(transactionCode, requestCode);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("requests")
+    @ApiOperation(value = "Allow Interoperation Transaction Request", httpMethod = "POST", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropTransactionRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropTransactionRequestResponseData.class)})
+    public String createTransactionRequest(@ApiParam(hidden = true) String quotesJson, @Context UriInfo uriInfo) {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().createTransactionRequest().withJson(quotesJson).build();
+
+        InteropTransactionRequestResponseData result = (InteropTransactionRequestResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("transactions/{transactionCode}/quotes/{quoteCode}")
+    @ApiOperation(value = "Query Interoperation Quote", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropQuoteResponseData.class)})
+    public String getQuote(@PathParam("transactionCode") @ApiParam(value = "transactionCode") String transactionCode,
+                           @PathParam("quoteCode") @ApiParam(value = "quoteCode") String quoteCode,
+                           @Context UriInfo uriInfo) {
+        context.authenticatedUser().validateHasReadPermission(ENTITY_NAME_QUOTE);
+
+        InteropQuoteResponseData result = interopService.getQuote(transactionCode, quoteCode);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return this.jsonSerializer.serialize(settings, result);
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("quotes")
+    @ApiOperation(value = "Calculate Interoperation Quote", httpMethod = "POST", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropQuoteRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropQuoteResponseData.class)})
+    public String createQuote(@ApiParam(hidden = true) String quotesJson, @Context UriInfo uriInfo) {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().createQuotes().withJson(quotesJson).build();
+
+        InteropQuoteResponseData result = (InteropQuoteResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+
+    @GET
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("transactions/{transactionCode}/transfers/{transferCode}")
+    @ApiOperation(value = "Query Interoperation Transfer", httpMethod = "GET", notes = "")
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropTransferResponseData.class)})
+    public String getTransfer(@PathParam("transactionCode") @ApiParam(value = "transactionCode") String transactionCode,
+                              @PathParam("transferCode") @ApiParam(value = "transferCode") String transferCode,
+                              @Context UriInfo uriInfo) {
+        context.authenticatedUser().validateHasReadPermission(ENTITY_NAME_QUOTE);
+
+        InteropTransferResponseData result = interopService.getTransfer(transactionCode, transferCode);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return this.jsonSerializer.serialize(settings, result);
+    }
+
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON})
+    @Produces({MediaType.APPLICATION_JSON})
+    @Path("transfers")
+    @ApiOperation(value = "Prepare Interoperation Transfer", httpMethod = "POST", notes = "")
+    @ApiImplicitParams({@ApiImplicitParam(value = "body", required = true, paramType = "body", dataType = "body", format = "body",
+            dataTypeClass = InteropTransferRequestData.class)})
+    @ApiResponses({@ApiResponse(code = 200, message = "OK", response = InteropTransferResponseData.class)})
+    public String performTransfer(@QueryParam("action") @ApiParam(value = "action") String action,
+                                  @ApiParam(hidden = true) String quotesJson, @Context UriInfo uriInfo) {
+        CommandWrapper commandRequest = new InteropWrapperBuilder().performTransfer(InteropTransferActionType.valueOf(action)).withJson(quotesJson).build();
+
+        InteropTransferResponseData result = (InteropTransferResponseData) commandsSourceService.logCommandSource(commandRequest);
+        ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+
+        return jsonSerializer.serialize(settings, result);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java
new file mode 100644
index 0000000..140591a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/api/InteropWrapperBuilder.java
@@ -0,0 +1,82 @@
+/*
+ * 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.interoperation.api;
+
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.domain.InteropTransferActionType;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_IDENTIFIER;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_QUOTE;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_REQUEST;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_TRANSFER;
+import static org.apache.fineract.interoperation.util.InteropUtil.ROOT_PATH;
+
+public class InteropWrapperBuilder {
+
+    private String actionName;
+    private String entityName;
+    private String href;
+    private String json = "{}";
+
+    public CommandWrapper build() {
+        return new CommandWrapper(null, null, null, null, null, actionName, entityName, null, null, href, json, null, null,
+                null, null, null);
+    }
+
+    public InteropWrapperBuilder withJson(final String json) {
+        this.json = json;
+        return this;
+    }
+
+    public InteropWrapperBuilder registerAccountIdentifier(InteropIdentifierType idType, String idValue, String subIdOrType) {
+        this.actionName = "CREATE";
+        this.entityName = ENTITY_NAME_IDENTIFIER;
+        this.href = "/" + ROOT_PATH + "/parties/" + idType + "/" + idValue + "/" + (subIdOrType == null ? "" : subIdOrType);
+        return this;
+    }
+
+    public InteropWrapperBuilder deleteAccountIdentifier(InteropIdentifierType idType, String idValue, String subIdOrType) {
+        this.actionName = "DELETE";
+        this.entityName = ENTITY_NAME_IDENTIFIER;
+        this.href = "/" + ROOT_PATH + "/parties/" + idType + "/" + idValue + "/" + (subIdOrType == null ? "" : subIdOrType);
+        return this;
+    }
+
+    public InteropWrapperBuilder createTransactionRequest() {
+        this.actionName = "CREATE";
+        this.entityName = ENTITY_NAME_REQUEST;
+        this.href = "/" + ROOT_PATH + "/requests";
+        return this;
+    }
+
+    public InteropWrapperBuilder createQuotes() {
+        this.actionName = "CREATE";
+        this.entityName = ENTITY_NAME_QUOTE;
+        this.href = "/" + ROOT_PATH + "/quotes";
+        return this;
+    }
+
+    public InteropWrapperBuilder performTransfer(InteropTransferActionType action) {
+        this.actionName = action.name();
+        this.entityName = ENTITY_NAME_TRANSFER;
+        this.href = "/" + ROOT_PATH + "/transfers";
+        return this;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/ExtensionData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/ExtensionData.java
new file mode 100644
index 0000000..e2c4992
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/ExtensionData.java
@@ -0,0 +1,71 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_KEY;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_VALUE;
+
+public class ExtensionData {
+
+    public static final String[] PARAMS = {PARAM_KEY, PARAM_VALUE};
+
+    @NotNull
+    private final String key;
+
+    private String value;
+
+    public ExtensionData(String key, String value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public static ExtensionData validateAndParse(DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        String key = jsonHelper.extractStringNamed(PARAM_KEY, element);
+        DataValidatorBuilder  dataValidatorCopy = dataValidator.reset().parameter(PARAM_KEY).value(key).notBlank();
+
+        String value = jsonHelper.extractStringNamed(PARAM_VALUE, element);
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new ExtensionData(key, value);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/GeoCodeData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/GeoCodeData.java
new file mode 100644
index 0000000..5b8eb8f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/GeoCodeData.java
@@ -0,0 +1,68 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_LATITUDE;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_LONGITUDE;
+
+public class GeoCodeData {
+
+    public static final String[] PARAMS = {PARAM_LATITUDE, PARAM_LONGITUDE};
+
+    @NotNull
+    private final String latitude;
+    @NotNull
+    private final String longitude;
+
+    public GeoCodeData(String latitude, String longitude) {
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public String getLatitude() {
+        return latitude;
+    }
+
+    public String getLongitude() {
+        return longitude;
+    }
+
+    public static GeoCodeData validateAndParse(DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        String latitude = jsonHelper.extractStringNamed(PARAM_LATITUDE, element);
+        DataValidatorBuilder  dataValidatorCopy = dataValidator.reset().parameter(PARAM_LATITUDE).value(latitude).notBlank();
+
+        String longitude = jsonHelper.extractStringNamed(PARAM_LONGITUDE, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_LONGITUDE).value(longitude).notBlank();
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new GeoCodeData(latitude, longitude);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierRequestData.java
new file mode 100644
index 0000000..0347b53
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierRequestData.java
@@ -0,0 +1,86 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.bval.constraints.NotEmpty;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.*;
+
+public class InteropIdentifierRequestData {
+
+    static final String[] PARAMS = {PARAM_ACCOUNT_ID};
+
+    @NotEmpty
+    private final InteropIdentifierType idType;
+    @NotEmpty
+    private final String idValue;
+
+    private final String subIdOrType;
+
+    @NotEmpty
+    private final String accountId;
+
+    public InteropIdentifierRequestData(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType, String accountId) {
+        this.idType = idType;
+        this.idValue = idValue;
+        this.subIdOrType = subIdOrType;
+        this.accountId = accountId;
+    }
+
+    public InteropIdentifierType getIdType() {
+        return idType;
+    }
+
+    public String getIdValue() {
+        return idValue;
+    }
+
+    public String getSubIdOrType() {
+        return subIdOrType;
+    }
+
+    public String getAccountId() {
+        return accountId;
+    }
+
+    public static InteropIdentifierRequestData validateAndParse(final DataValidatorBuilder dataValidator, @NotNull InteropIdentifierType idType,
+                                                                @NotNull String idValue, String subIdOrType, JsonObject element,
+                                                                FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        String accountId = jsonHelper.extractStringNamed(PARAM_ACCOUNT_ID, element);
+        DataValidatorBuilder dataValidatorCopy = dataValidator.reset().parameter(PARAM_ACCOUNT_ID).value(accountId).notBlank();
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new InteropIdentifierRequestData(idType, idValue, subIdOrType, accountId);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierResponseData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierResponseData.java
new file mode 100644
index 0000000..1f7b74a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropIdentifierResponseData.java
@@ -0,0 +1,58 @@
+/*
+ * 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.interoperation.data;
+
+import org.apache.bval.constraints.NotEmpty;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+
+public class InteropIdentifierResponseData extends CommandProcessingResult {
+
+    @NotEmpty
+    private String accountId;
+
+
+    protected InteropIdentifierResponseData(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly, @NotNull String accountId) {
+        super(resourceId, officeId, commandId, changesOnly);
+        this.accountId = accountId;
+    }
+
+    protected static InteropIdentifierResponseData build(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly, @NotNull String accountId) {
+        return new InteropIdentifierResponseData(resourceId, officeId, commandId, changesOnly, accountId);
+    }
+
+    protected static InteropIdentifierResponseData build(Long commandId, @NotNull String accountId) {
+        return build(null, null, commandId, null, accountId);
+    }
+
+    public static InteropIdentifierResponseData build(@NotNull String accountId) {
+        return build(null, accountId);
+    }
+
+    @NotNull
+    public String getAccountId() {
+        return accountId;
+    }
+
+    protected void setAccountId(String accountId) {
+        this.accountId = accountId;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteRequestData.java
new file mode 100644
index 0000000..a5a88cb
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteRequestData.java
@@ -0,0 +1,117 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.interoperation.domain.InteropAmountType;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.*;
+
+public class InteropQuoteRequestData extends InteropRequestData {
+
+    static final String[] PARAMS = {PARAM_TRANSACTION_CODE, PARAM_REQUEST_CODE, PARAM_ACCOUNT_ID, PARAM_AMOUNT, PARAM_TRANSACTION_TYPE,
+            PARAM_TRANSACTION_ROLE, PARAM_NOTE, PARAM_GEO_CODE, PARAM_EXPIRATION, PARAM_EXTENSION_LIST, PARAM_QUOTE_CODE,
+            PARAM_AMOUNT_TYPE, PARAM_FEES, PARAM_LOCALE, PARAM_DATE_FORMAT};
+    @NotNull
+    private final String quoteCode;
+    @NotNull
+    private final InteropAmountType amountType;
+
+    private final MoneyData fees; // only for disclosed Payer fees on the Payee side
+
+    public InteropQuoteRequestData(@NotNull String transactionCode, String requestCode, @NotNull String accountId, @NotNull MoneyData amount,
+                                   @NotNull InteropTransactionRole transactionRole, @NotNull InteropTransactionTypeData transactionType,
+                                   String note, GeoCodeData geoCode, LocalDateTime expiration, List<ExtensionData> extensionList,
+                                   @NotNull String quoteCode, @NotNull InteropAmountType amountType, MoneyData fees) {
+        super(transactionCode, requestCode, accountId, amount, transactionRole, transactionType, note, geoCode, expiration, extensionList);
+        this.quoteCode = quoteCode;
+        this.amountType = amountType;
+        this.fees = fees;
+    }
+
+    public InteropQuoteRequestData(@NotNull String transactionCode, @NotNull String accountId, @NotNull InteropAmountType amountType,
+                                   @NotNull MoneyData amount, @NotNull InteropTransactionRole transactionRole, @NotNull InteropTransactionTypeData transactionType,
+                                   @NotNull String quoteCode) {
+        this(transactionCode, null, accountId, amount, transactionRole, transactionType, null, null, null, null, quoteCode,
+                amountType, null);
+    }
+
+    private InteropQuoteRequestData(@NotNull InteropRequestData other, @NotNull String quoteCode, @NotNull InteropAmountType amountType,
+                                    MoneyData fees) {
+        this(other.getTransactionCode(), other.getRequestCode(), other.getAccountId(), other.getAmount(), other.getTransactionRole(),
+                other.getTransactionType(), other.getNote(), other.getGeoCode(), other.getExpiration(), other.getExtensionList(),
+                quoteCode, amountType, fees);
+    }
+
+    public String getQuoteCode() {
+        return quoteCode;
+    }
+
+    public InteropAmountType getAmountType() {
+        return amountType;
+    }
+
+    public MoneyData getFees() {
+        return fees;
+    }
+
+    public void normalizeAmounts(@NotNull MonetaryCurrency currency) {
+        super.normalizeAmounts(currency);
+        if (fees != null)
+            fees.normalizeAmount(currency);
+    }
+
+    public static InteropQuoteRequestData validateAndParse(final DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        InteropRequestData interopRequestData = InteropRequestData.validateAndParse(dataValidator, element, jsonHelper);
+
+        String quoteCode = jsonHelper.extractStringNamed(PARAM_QUOTE_CODE, element);
+        DataValidatorBuilder dataValidatorCopy = dataValidator.reset().parameter(PARAM_QUOTE_CODE).value(quoteCode).notBlank();
+
+        String amountTypeString = jsonHelper.extractStringNamed(PARAM_AMOUNT_TYPE, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_AMOUNT_TYPE).value(amountTypeString).notBlank();
+        InteropAmountType amountType = InteropAmountType.valueOf(amountTypeString);
+
+        JsonObject feesElement = jsonHelper.extractJsonObjectNamed(PARAM_FEES, element);
+        dataValidator.merge(dataValidatorCopy);
+        MoneyData fees = MoneyData.validateAndParse(dataValidator, feesElement, jsonHelper);
+
+        String transactionRoleString = jsonHelper.extractStringNamed(PARAM_TRANSACTION_ROLE, element);
+        dataValidatorCopy = dataValidator.reset().parameter(PARAM_TRANSACTION_ROLE).value(transactionRoleString).notNull();
+
+        JsonObject transactionTypeElement = jsonHelper.extractJsonObjectNamed(PARAM_TRANSACTION_TYPE, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_TRANSACTION_TYPE).value(transactionTypeElement).notNull();
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new InteropQuoteRequestData(interopRequestData, quoteCode, amountType, fees);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteResponseData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteResponseData.java
new file mode 100644
index 0000000..bd2d7b9
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropQuoteResponseData.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.interoperation.data;
+
+import org.apache.fineract.interoperation.domain.InteropActionState;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Map;
+
+public class InteropQuoteResponseData extends InteropResponseData {
+
+    @NotNull
+    private final String quoteCode;
+
+    private MoneyData fspFee;
+
+    private MoneyData fspCommission;
+
+    
+    private InteropQuoteResponseData(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly,
+                                     @NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration,
+                                     List<ExtensionData> extensionList, @NotNull String quoteCode, MoneyData fspFee, MoneyData fspCommission) {
+        super(resourceId, officeId, commandId, changesOnly, transactionCode, state, expiration, extensionList);
+        this.quoteCode = quoteCode;
+        this.fspFee = fspFee;
+        this.fspCommission = fspCommission;
+    }
+
+    public static InteropQuoteResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                 LocalDateTime expiration, List<ExtensionData> extensionList, @NotNull String quoteCode,
+                                                 MoneyData fspFee, MoneyData fspCommission) {
+        return new InteropQuoteResponseData(null, null, commandId, null, transactionCode, state, expiration, extensionList,
+                quoteCode, fspFee, fspCommission);
+    }
+
+    public static InteropQuoteResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration,
+                                                 List<ExtensionData> extensionList, @NotNull String quoteCode, MoneyData fspFee,
+                                                 MoneyData fspCommission) {
+        return build(null, transactionCode, state, expiration, extensionList, quoteCode, fspFee, fspCommission);
+    }
+
+    public static InteropQuoteResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                 @NotNull String quoteCode) {
+        return build(commandId, transactionCode, state, null, null, quoteCode, null, null);
+    }
+
+    public static InteropQuoteResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state, @NotNull String quoteCode) {
+        return build(null, transactionCode, state, quoteCode);
+    }
+
+    public String getQuoteCode() {
+        return quoteCode;
+    }
+
+    public MoneyData getFspFee() {
+        return fspFee;
+    }
+
+    public void setFspFee(MoneyData fspFee) {
+        this.fspFee = fspFee;
+    }
+
+    public MoneyData getFspCommission() {
+        return fspCommission;
+    }
+
+    public void setFspCommission(MoneyData fspCommission) {
+        this.fspCommission = fspCommission;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropRequestData.java
new file mode 100644
index 0000000..e05adbc
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropRequestData.java
@@ -0,0 +1,193 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.*;
+
+public class InteropRequestData {
+
+    @NotNull
+    private final String transactionCode;
+
+    private final String requestCode;
+    @NotNull
+    private final String accountId;
+    @NotNull
+    private final MoneyData amount;
+    @NotNull
+    private final InteropTransactionRole transactionRole;
+
+    private final InteropTransactionTypeData transactionType;
+
+    private String note;
+
+    private GeoCodeData geoCode;
+
+    private LocalDateTime expiration;
+
+    private List<ExtensionData> extensionList;
+
+    protected InteropRequestData(@NotNull String transactionCode, String requestCode, @NotNull String accountId, @NotNull MoneyData amount,
+                                 @NotNull InteropTransactionRole transactionRole, InteropTransactionTypeData transactionType, String note,
+                                 GeoCodeData geoCode, LocalDateTime expiration, List<ExtensionData> extensionList) {
+        this.transactionCode = transactionCode;
+        this.requestCode = requestCode;
+        this.accountId = accountId;
+        this.amount = amount;
+        this.transactionType = transactionType;
+        this.transactionRole = transactionRole;
+        this.note = note;
+        this.geoCode = geoCode;
+        this.expiration = expiration;
+        this.extensionList = extensionList;
+    }
+
+    protected InteropRequestData(@NotNull String transactionCode, @NotNull String accountId, @NotNull MoneyData amount, @NotNull InteropTransactionRole transactionRole) {
+        this(transactionCode, null, accountId, amount, transactionRole, null, null, null, null, null);
+    }
+
+    @NotNull
+    public String getTransactionCode() {
+        return transactionCode;
+    }
+
+    public String getRequestCode() {
+        return requestCode;
+    }
+
+    @NotNull
+    public String getAccountId() {
+        return accountId;
+    }
+
+    @NotNull
+    public MoneyData getAmount() {
+        return amount;
+    }
+
+    public InteropTransactionTypeData getTransactionType() {
+        return transactionType;
+    }
+
+    @NotNull
+    public InteropTransactionRole getTransactionRole() {
+        return transactionRole;
+    }
+
+    public String getNote() {
+        return note;
+    }
+
+    public void setNote(String note) {
+        this.note = note;
+    }
+
+    public GeoCodeData getGeoCode() {
+        return geoCode;
+    }
+
+    public void setGeoCode(GeoCodeData geoCode) {
+        this.geoCode = geoCode;
+    }
+
+    public LocalDateTime getExpiration() {
+        return expiration;
+    }
+
+    public LocalDate getExpirationLocalDate() {
+        return expiration == null ? null : expiration.toLocalDate();
+    }
+
+    public void setExpiration(LocalDateTime expiration) {
+        this.expiration = expiration;
+    }
+
+    public List<ExtensionData> getExtensionList() {
+        return extensionList;
+    }
+
+    public void setExtensionList(List<ExtensionData> extensionList) {
+        this.extensionList = extensionList;
+    }
+
+    public void normalizeAmounts(@NotNull MonetaryCurrency currency) {
+        amount.normalizeAmount(currency);
+    }
+
+    public static InteropRequestData validateAndParse(final DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        String transactionCode = jsonHelper.extractStringNamed(PARAM_TRANSACTION_CODE, element);
+        DataValidatorBuilder  dataValidatorCopy = dataValidator.reset().parameter(PARAM_TRANSACTION_CODE).value(transactionCode).notBlank();
+
+        String requestCode = jsonHelper.extractStringNamed(PARAM_REQUEST_CODE, element);
+
+        String accountId = jsonHelper.extractStringNamed(PARAM_ACCOUNT_ID, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_ACCOUNT_ID).value(accountId).notBlank();
+
+        JsonObject moneyElement = jsonHelper.extractJsonObjectNamed(PARAM_AMOUNT, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_AMOUNT).value(moneyElement).notNull();
+        dataValidator.merge(dataValidatorCopy);
+        MoneyData amount = MoneyData.validateAndParse(dataValidator, moneyElement, jsonHelper);
+
+        JsonObject transactionTypeElement = jsonHelper.extractJsonObjectNamed(PARAM_TRANSACTION_TYPE, element);
+        InteropTransactionTypeData transactionType = InteropTransactionTypeData.validateAndParse(dataValidator, transactionTypeElement, jsonHelper);
+
+        String transactionRoleString = jsonHelper.extractStringNamed(PARAM_TRANSACTION_ROLE, element);
+        InteropTransactionRole transactionRole = transactionRoleString == null ? InteropTransactionRole.PAYER : InteropTransactionRole.valueOf(transactionRoleString);
+
+        String note = jsonHelper.extractStringNamed(PARAM_NOTE, element);
+
+        JsonObject geoCodeElement = jsonHelper.extractJsonObjectNamed(PARAM_GEO_CODE, element);
+        GeoCodeData geoCode = GeoCodeData.validateAndParse(dataValidator, geoCodeElement, jsonHelper);
+
+        String locale = jsonHelper.extractStringNamed(PARAM_LOCALE, element);
+        LocalDateTime expiration = locale == null
+                ? jsonHelper.extractLocalTimeNamed(PARAM_EXPIRATION, element, ISO8601_DATE_TIME_FORMAT, DEFAULT_LOCALE)
+                : jsonHelper.extractLocalTimeNamed(PARAM_EXPIRATION, element); // PARAM_DATE_FORMAT also must be set
+
+        JsonArray extensionArray = jsonHelper.extractJsonArrayNamed(PARAM_EXTENSION_LIST, element);
+        ArrayList<ExtensionData> extensionList = null;
+        if (extensionArray != null) {
+            extensionList = new ArrayList<>(extensionArray.size());
+            for (JsonElement jsonElement : extensionArray) {
+                if (jsonElement.isJsonObject())
+                    extensionList.add(ExtensionData.validateAndParse(dataValidator, jsonElement.getAsJsonObject(), jsonHelper));
+            }
+        }
+
+        return dataValidator.hasError() ? null : new InteropRequestData(transactionCode, requestCode, accountId, amount,
+                transactionRole, transactionType, note, geoCode, expiration, extensionList);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropResponseData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropResponseData.java
new file mode 100644
index 0000000..aecf2a0
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropResponseData.java
@@ -0,0 +1,105 @@
+/*
+ * 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.interoperation.data;
+
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.domain.InteropActionState;
+import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+
+public class InteropResponseData extends CommandProcessingResult {
+
+    public static final String ISO_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ";
+//    public static final SimpleDateFormat ISO_DATE_TIME_FORMATTER = new SimpleDateFormat(ISO_DATE_TIME_PATTERN); // TODO: not synchronized
+    public static DateTimeFormatter ISO_DATE_TIME_FORMATTER = DateTimeFormat.forPattern(ISO_DATE_TIME_PATTERN);
+
+    @NotNull
+    private final String transactionCode;
+
+    @NotNull
+    private final InteropActionState state;
+
+    private final String expiration;
+
+    private final List<ExtensionData> extensionList;
+
+
+    protected InteropResponseData(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly, @NotNull String transactionCode,
+                                  @NotNull InteropActionState state, LocalDateTime expiration, List<ExtensionData> extensionList) {
+        super(resourceId, officeId, commandId, changesOnly);
+        this.transactionCode = transactionCode;
+        this.state = state;
+        this.expiration = format(expiration);
+        this.extensionList = extensionList;
+    }
+
+    protected static InteropResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                               LocalDateTime expiration, List<ExtensionData> extensionList) {
+        return new InteropResponseData(null, null, commandId, null, transactionCode, state, expiration, extensionList);
+    }
+
+    public static InteropResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+                                            LocalDateTime expiration, List<ExtensionData> extensionList) {
+        return build(null, transactionCode, state, expiration, extensionList);
+    }
+
+    public static InteropResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state) {
+        return build(commandId, transactionCode, state, null, null);
+    }
+
+    public static InteropResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state) {
+        return build(null, transactionCode, state);
+    }
+
+    public String getTransactionCode() {
+        return transactionCode;
+    }
+
+    public InteropActionState getState() {
+        return state;
+    }
+
+    public String getExpiration() {
+        return expiration;
+    }
+
+    @Transient
+    public LocalDateTime getExpirationDate() {
+        return parse(expiration);
+    }
+
+    public List<ExtensionData> getExtensionList() {
+        return extensionList;
+    }
+
+    protected static LocalDateTime parse(String date) {
+        return date == null ? null : LocalDateTime.parse(date, ISO_DATE_TIME_FORMATTER);
+    }
+
+    protected static String format(LocalDateTime date) {
+        return date == null ? null : date.toString(ISO_DATE_TIME_FORMATTER);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestData.java
new file mode 100644
index 0000000..1912ce1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestData.java
@@ -0,0 +1,71 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.*;
+
+public class InteropTransactionRequestData extends InteropRequestData {
+
+    static final String[] PARAMS = {PARAM_TRANSACTION_CODE, PARAM_REQUEST_CODE, PARAM_ACCOUNT_ID, PARAM_AMOUNT, PARAM_TRANSACTION_ROLE,
+            PARAM_TRANSACTION_TYPE, PARAM_NOTE, PARAM_GEO_CODE, PARAM_EXPIRATION, PARAM_EXTENSION_LIST, PARAM_LOCALE, PARAM_DATE_FORMAT};
+
+
+    public InteropTransactionRequestData(@NotNull String transactionCode, @NotNull String requestCode, @NotNull String accountId,
+                                         @NotNull MoneyData amount, @NotNull InteropTransactionTypeData transactionType, String note,
+                                         GeoCodeData geoCode, LocalDateTime expiration, List<ExtensionData> extensionList) {
+        super(transactionCode, requestCode, accountId, amount, InteropTransactionRole.PAYER, transactionType, note, geoCode, expiration, extensionList);
+    }
+
+    public InteropTransactionRequestData(@NotNull String transactionCode, @NotNull String requestCode, @NotNull String accountId,
+                                         @NotNull MoneyData amount, @NotNull InteropTransactionTypeData transactionType) {
+        this(transactionCode, requestCode, accountId, amount, transactionType, null, null, null, null);
+    }
+
+    private InteropTransactionRequestData(InteropRequestData other) {
+        this(other.getTransactionCode(), other.getRequestCode(), other.getAccountId(), other.getAmount(), other.getTransactionType(),
+                other.getNote(), other.getGeoCode(), other.getExpiration(), other.getExtensionList());
+    }
+
+    public static InteropTransactionRequestData validateAndParse(final DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        InteropRequestData interopRequestData = InteropRequestData.validateAndParse(dataValidator, element, jsonHelper);
+
+        DataValidatorBuilder dataValidatorCopy = dataValidator.reset().parameter(PARAM_REQUEST_CODE).value(interopRequestData.getRequestCode()).notNull();
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_TRANSACTION_TYPE).value(interopRequestData.getTransactionType()).notNull();
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_TRANSACTION_ROLE).value(interopRequestData.getTransactionRole()).ignoreIfNull()
+                .isOneOfTheseValues(InteropTransactionRole.PAYER);
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new InteropTransactionRequestData(interopRequestData);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestResponseData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestResponseData.java
new file mode 100644
index 0000000..1743aac
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionRequestResponseData.java
@@ -0,0 +1,64 @@
+/*
+ * 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.interoperation.data;
+
+import org.apache.fineract.interoperation.domain.InteropActionState;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Map;
+
+public class InteropTransactionRequestResponseData extends InteropResponseData {
+
+    @NotNull
+    private final String requestCode;
+
+
+    private InteropTransactionRequestResponseData(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly,
+                                                  @NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration,
+                                                  List<ExtensionData> extensionList, @NotNull String requestCode) {
+        super(resourceId, officeId, commandId, changesOnly, transactionCode, state, expiration, extensionList);
+        this.requestCode = requestCode;
+    }
+
+    public static InteropTransactionRequestResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                              LocalDateTime expiration, List<ExtensionData> extensionList, @NotNull String requestCode) {
+        return new InteropTransactionRequestResponseData(null, null, commandId, null, transactionCode, state, expiration, extensionList, requestCode);
+    }
+
+    public static InteropTransactionRequestResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+                                                              LocalDateTime expiration, List<ExtensionData> extensionList, @NotNull String requestCode) {
+        return build(null, transactionCode, state, expiration, extensionList, requestCode);
+    }
+
+    public static InteropTransactionRequestResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                              @NotNull String requestCode) {
+        return build(commandId, transactionCode, state, null, null, requestCode);
+    }
+
+    public static InteropTransactionRequestResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+                                                              @NotNull String requestCode) {
+        return build(null, transactionCode, state, requestCode);
+    }
+
+    public String getRequestCode() {
+        return requestCode;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionTypeData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionTypeData.java
new file mode 100644
index 0000000..2b9f999
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionTypeData.java
@@ -0,0 +1,96 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.interoperation.domain.InteropInitiatorType;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.apache.fineract.interoperation.domain.InteropTransactionScenario;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_INITIATOR;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_INITIATOR_TYPE;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_SCENARIO;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_SUB_SCENARIO;
+
+public class InteropTransactionTypeData {
+
+    public static final String[] PARAMS = {PARAM_SCENARIO, PARAM_SUB_SCENARIO, PARAM_INITIATOR, PARAM_INITIATOR_TYPE};
+
+    @NotNull
+    private final InteropTransactionScenario scenario;
+
+    private final String subScenario;
+    @NotNull
+    private final InteropTransactionRole initiator;
+    @NotNull
+    private final InteropInitiatorType initiatorType;
+
+    public InteropTransactionTypeData(InteropTransactionScenario scenario, String subScenario, InteropTransactionRole initiator, InteropInitiatorType initiatorType) {
+        this.scenario = scenario;
+        this.subScenario = subScenario;
+        this.initiator = initiator;
+        this.initiatorType = initiatorType;
+    }
+
+    public InteropTransactionScenario getScenario() {
+        return scenario;
+    }
+
+    public String getSubScenario() {
+        return subScenario;
+    }
+
+    public InteropTransactionRole getInitiator() {
+        return initiator;
+    }
+
+    public InteropInitiatorType getInitiatorType() {
+        return initiatorType;
+    }
+
+
+    public static InteropTransactionTypeData validateAndParse(DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        String scenarioString = jsonHelper.extractStringNamed(PARAM_SCENARIO, element);
+        DataValidatorBuilder dataValidatorCopy = dataValidator.reset().parameter(PARAM_SCENARIO).value(scenarioString).notBlank();
+        InteropTransactionScenario scenario = InteropTransactionScenario.valueOf(scenarioString);
+
+        String subScenario = jsonHelper.extractStringNamed(PARAM_SUB_SCENARIO, element);
+
+        String initiatorString = jsonHelper.extractStringNamed(PARAM_INITIATOR, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_INITIATOR).value(initiatorString).notBlank();
+        InteropTransactionRole initiator = InteropTransactionRole.valueOf(initiatorString);
+
+        String initiatorTypeString = jsonHelper.extractStringNamed(PARAM_INITIATOR_TYPE, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_INITIATOR_TYPE).value(initiatorTypeString).notBlank();
+        InteropInitiatorType initiatorType = InteropInitiatorType.valueOf(initiatorTypeString);
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new InteropTransactionTypeData(scenario, subScenario, initiator, initiatorType);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferRequestData.java
new file mode 100644
index 0000000..849b69d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferRequestData.java
@@ -0,0 +1,111 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.interoperation.domain.InteropTransactionRole;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.*;
+
+public class InteropTransferRequestData extends InteropRequestData {
+
+    static final String[] PARAMS = {PARAM_TRANSACTION_CODE, PARAM_ACCOUNT_ID, PARAM_AMOUNT, PARAM_TRANSACTION_ROLE, PARAM_TRANSACTION_TYPE,
+            PARAM_NOTE, PARAM_EXPIRATION, PARAM_EXTENSION_LIST, PARAM_TRANSFER_CODE, PARAM_FSP_FEE, PARAM_FSP_COMMISSION,
+            PARAM_LOCALE, PARAM_DATE_FORMAT};
+
+
+    @NotNull
+    private final String transferCode;
+
+    // validation: what was specified in quotes step
+    private MoneyData fspFee;
+
+    private MoneyData fspCommission;
+
+    public InteropTransferRequestData(@NotNull String transactionCode, @NotNull String accountId, @NotNull MoneyData amount,
+                                      @NotNull InteropTransactionRole transactionRole, InteropTransactionTypeData transactionType, String note, LocalDateTime expiration,
+                                      List<ExtensionData> extensionList, @NotNull String transferCode, MoneyData fspFee, MoneyData fspCommission) {
+        super(transactionCode, null, accountId, amount, transactionRole, transactionType, note, null, expiration, extensionList);
+        this.transferCode = transferCode;
+        this.fspFee = fspFee;
+        this.fspCommission = fspCommission;
+    }
+
+    public InteropTransferRequestData(@NotNull String transactionCode, @NotNull String transferCode, @NotNull String accountId,
+                                      @NotNull MoneyData amount, @NotNull InteropTransactionRole transactionRole) {
+        this(transactionCode, accountId, amount, transactionRole, null, null, null, null, transferCode, null, null);
+    }
+
+    private InteropTransferRequestData(InteropRequestData other, @NotNull String transferCode, MoneyData fspFee, MoneyData fspCommission) {
+        this(other.getTransactionCode(), other.getAccountId(), other.getAmount(), other.getTransactionRole(), other.getTransactionType(),
+                other.getNote(), other.getExpiration(), other.getExtensionList(), transferCode, fspFee, fspCommission);
+    }
+
+    public String getTransferCode() {
+        return transferCode;
+    }
+
+    public MoneyData getFspFee() {
+        return fspFee;
+    }
+
+    public MoneyData getFspCommission() {
+        return fspCommission;
+    }
+
+    public void normalizeAmounts(@NotNull MonetaryCurrency currency) {
+        super.normalizeAmounts(currency);
+        if (fspFee != null)
+            fspFee.normalizeAmount(currency);
+    }
+
+    public static InteropTransferRequestData validateAndParse(final DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        InteropRequestData interopRequestData = InteropRequestData.validateAndParse(dataValidator, element, jsonHelper);
+
+        String transferCode = jsonHelper.extractStringNamed(PARAM_TRANSFER_CODE, element);
+        DataValidatorBuilder dataValidatorCopy = dataValidator.reset().parameter(PARAM_TRANSFER_CODE).value(transferCode).notBlank();
+
+        JsonObject fspFeeElement = jsonHelper.extractJsonObjectNamed(PARAM_FSP_FEE, element);
+        dataValidator.merge(dataValidatorCopy);
+        MoneyData fspFee = MoneyData.validateAndParse(dataValidator, fspFeeElement, jsonHelper);
+
+        JsonObject fspCommissionElement = jsonHelper.extractJsonObjectNamed(PARAM_FSP_COMMISSION, element);
+        dataValidator.merge(dataValidatorCopy);
+        MoneyData fspCommission = MoneyData.validateAndParse(dataValidator, fspCommissionElement, jsonHelper);
+
+        String transactionRoleString = jsonHelper.extractStringNamed(PARAM_TRANSACTION_ROLE, element);
+        dataValidatorCopy = dataValidator.reset().parameter(PARAM_TRANSACTION_ROLE).value(transactionRoleString).notNull();
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new InteropTransferRequestData(interopRequestData, transferCode, fspFee, fspCommission);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferResponseData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferResponseData.java
new file mode 100644
index 0000000..bc89172
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransferResponseData.java
@@ -0,0 +1,79 @@
+/*
+ * 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.interoperation.data;
+
+import org.apache.fineract.interoperation.domain.InteropActionState;
+import org.joda.time.LocalDateTime;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+
+public class InteropTransferResponseData extends InteropResponseData {
+
+    @NotNull
+    private final String transferCode;
+
+    private String completedTimestamp;
+
+    private InteropTransferResponseData(Long resourceId, Long officeId, Long commandId, Map<String, Object> changesOnly,
+                                        @NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration,
+                                        List<ExtensionData> extensionList, @NotNull String transferCode, LocalDateTime completedTimestamp) {
+        super(resourceId, officeId, commandId, changesOnly, transactionCode, state, expiration, extensionList);
+        this.transferCode = transferCode;
+        this.completedTimestamp = format(completedTimestamp);
+    }
+
+    public static InteropTransferResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                    LocalDateTime expiration, List<ExtensionData> extensionList, @NotNull String transferCode,
+                                                    LocalDateTime completedTimestamp) {
+        return new InteropTransferResponseData(null, null, commandId, null, transactionCode, state, expiration, extensionList,
+                transferCode, completedTimestamp);
+    }
+
+    public static InteropTransferResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+                                                    List<ExtensionData> extensionList, @NotNull String transferCode,
+                                                    LocalDateTime completedTimestamp) {
+        return build(null, transactionCode, state, null, extensionList, transferCode, completedTimestamp);
+    }
+
+    public static InteropTransferResponseData build(Long commandId, @NotNull String transactionCode, @NotNull InteropActionState state,
+                                                    @NotNull String transferCode) {
+        return build(commandId, transactionCode, state, null, null, transferCode, null);
+    }
+
+    public static InteropTransferResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state, @NotNull String transferCode) {
+        return build(null, transactionCode, state, transferCode);
+    }
+
+    public String getTransferCode() {
+        return transferCode;
+    }
+
+    public String getCompletedTimestamp() {
+        return completedTimestamp;
+    }
+
+    @Transient
+    public LocalDateTime getCompletedTimestampDate() throws ParseException {
+        return parse(completedTimestamp);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java
new file mode 100644
index 0000000..4b48298
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java
@@ -0,0 +1,86 @@
+/*
+ * 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.interoperation.data;
+
+import com.google.gson.JsonObject;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.interoperation.util.MathUtil;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.DEFAULT_LOCALE;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_AMOUNT;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_CURRENCY;
+import static org.apache.fineract.interoperation.util.InteropUtil.PARAM_LOCALE;
+
+public class MoneyData {
+
+    public static final String[] PARAMS = {PARAM_AMOUNT, PARAM_CURRENCY, PARAM_LOCALE};
+
+    @NotNull
+    private final BigDecimal amount;
+    @NotNull
+    private final String currency;
+
+    MoneyData(BigDecimal amount, String currency) {
+        this.amount = amount;
+        this.currency = currency;
+    }
+
+    public static MoneyData build(BigDecimal amount, String currency) {
+        return new MoneyData(amount, currency);
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public String getCurrency() {
+        return currency;
+    }
+
+    public void normalizeAmount(@NotNull MonetaryCurrency currency) {
+        if (!currency.getCode().equals(this.currency))
+            throw new UnsupportedOperationException("Internal error: Invalid currency " + currency.getCode());
+        MathUtil.normalizeAmount(amount, currency);
+    }
+
+    public static MoneyData validateAndParse(DataValidatorBuilder dataValidator, JsonObject element, FromJsonHelper jsonHelper) {
+        if (element == null)
+            return null;
+
+        jsonHelper.checkForUnsupportedParameters(element, Arrays.asList(PARAMS));
+
+        String locale = jsonHelper.extractStringNamed(PARAM_LOCALE, element);
+        BigDecimal amount = locale == null
+                ? jsonHelper.extractBigDecimalNamed(PARAM_AMOUNT, element, DEFAULT_LOCALE)
+                : jsonHelper.extractBigDecimalWithLocaleNamed(PARAM_AMOUNT, element);
+        DataValidatorBuilder  dataValidatorCopy = dataValidator.reset().parameter(PARAM_AMOUNT).value(amount).notBlank().zeroOrPositiveAmount();
+
+        String currency = jsonHelper.extractStringNamed(PARAM_CURRENCY, element);
+        dataValidatorCopy = dataValidatorCopy.reset().parameter(PARAM_CURRENCY).value(currency).notBlank().notExceedingLengthOf(3);
+
+        dataValidator.merge(dataValidatorCopy);
+        return dataValidator.hasError() ? null : new MoneyData(amount, currency);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropActionState.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropActionState.java
new file mode 100644
index 0000000..73b761d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropActionState.java
@@ -0,0 +1,24 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropActionState {
+    ACCEPTED,
+    REJECTED
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropAmountType.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropAmountType.java
new file mode 100644
index 0000000..1c4cab8
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropAmountType.java
@@ -0,0 +1,24 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropAmountType {
+    SEND,
+    RECEIVE
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java
new file mode 100644
index 0000000..b37c179
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifier.java
@@ -0,0 +1,167 @@
+/*
+ * 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.interoperation.domain;
+
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+@Entity
+@Table(name = "interop_identifier", uniqueConstraints = {
+        @UniqueConstraint(name = "uk_hathor_identifier_account", columnNames = {"account_id", "type"}),
+        @UniqueConstraint(name = "uk_hathor_identifier_value", columnNames = {"type", "a_value", "sub_value_or_type"})
+})
+public class InteropIdentifier extends AbstractPersistableCustom<Long> {
+
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "account_id", nullable = false)
+    private SavingsAccount account;
+
+    @Column(name = "type", nullable = false, length = 32)
+    @Enumerated(EnumType.STRING)
+    private InteropIdentifierType type;
+
+    @Column(name = "a_value", nullable = false, length = 128)
+    private String value;
+
+    @Column(name = "sub_value_or_type", length = 128)
+    private String subValueOrType;
+
+    @Column(name = "created_by", nullable = false, length = 32)
+    private String createdBy;
+
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "created_on", nullable = false)
+    private Date createdOn;
+
+    @Column(name = "modified_by", length = 32)
+    private String modifiedBy;
+
+    @Temporal(TemporalType.TIMESTAMP)
+    @Column(name = "modified_on")
+    private Date modifiedOn;
+
+
+    protected InteropIdentifier() {
+    }
+
+    public InteropIdentifier(@NotNull SavingsAccount account, @NotNull InteropIdentifierType type, @NotNull String value,
+                                   String subValueOrType, @NotNull String createdBy, @NotNull Date createdOn) {
+        this.account = account;
+        this.type = type;
+        this.value = value;
+        this.subValueOrType = subValueOrType;
+        this.createdBy = createdBy;
+        this.createdOn = createdOn;
+    }
+
+    public InteropIdentifier(@NotNull SavingsAccount account, @NotNull InteropIdentifierType type, @NotNull String createdBy,
+                                   @NotNull Date createdOn) {
+        this(account, type, null, null, createdBy, createdOn);
+    }
+
+    public SavingsAccount getAccount() {
+        return account;
+    }
+
+    private void setAccount(SavingsAccount account) {
+        this.account = account;
+    }
+
+    public InteropIdentifierType getType() {
+        return type;
+    }
+
+    private void setType(InteropIdentifierType type) {
+        this.type = type;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public String getSubValueOrType() {
+        return subValueOrType;
+    }
+
+    public void setSubValueOrType(String subValueOrType) {
+        this.subValueOrType = subValueOrType;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    private void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    public Date getCreatedOn() {
+        return createdOn;
+    }
+
+    private void setCreatedOn(Date createdOn) {
+        this.createdOn = createdOn;
+    }
+
+    public String geModifiedBy() {
+        return modifiedBy;
+    }
+
+    public void setModifiedBy(String modifiedBy) {
+        this.modifiedBy = modifiedBy;
+    }
+
+    public Date getModifiedOn() {
+        return modifiedOn;
+    }
+
+    public void setModifiedOn(Date modifiedOn) {
+        this.modifiedOn = modifiedOn;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        InteropIdentifier that = (InteropIdentifier) o;
+
+        if (!account.equals(that.account)) return false;
+        if (type != that.type) return false;
+        if (!value.equals(that.value)) return false;
+        return subValueOrType != null ? subValueOrType.equals(that.subValueOrType) : that.subValueOrType == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type.hashCode();
+        result = 31 * result + value.hashCode();
+        result = 31 * result + (subValueOrType != null ? subValueOrType.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java
new file mode 100644
index 0000000..c9f418c
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.interoperation.domain;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface InteropIdentifierRepository extends JpaRepository<InteropIdentifier, Long>, JpaSpecificationExecutor<InteropIdentifier> {
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierType.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierType.java
new file mode 100644
index 0000000..b0e2e98
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropIdentifierType.java
@@ -0,0 +1,31 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropIdentifierType {
+
+    MSISDN,
+    EMAIL,
+    PERSONAL_ID,
+    BUSINESS,
+    DEVICE,
+    ACCOUNT_ID,
+    IBAN,
+    ALIAS
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropInitiatorType.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropInitiatorType.java
new file mode 100644
index 0000000..fb6ff8c
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropInitiatorType.java
@@ -0,0 +1,26 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropInitiatorType {
+    CONSUMER,
+    AGENT,
+    BUSINESS,
+    DEVICE
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionRole.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionRole.java
new file mode 100644
index 0000000..232bf78
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionRole.java
@@ -0,0 +1,35 @@
+/*
+ * 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.interoperation.domain;
+
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+
+public enum InteropTransactionRole {
+    PAYER,
+    PAYEE,
+    ;
+
+    public boolean isWithdraw() {
+        return this == PAYER;
+    }
+
+    public SavingsAccountTransactionType getTransactionType() {
+        return this == PAYER ? SavingsAccountTransactionType.WITHDRAWAL : SavingsAccountTransactionType.DEPOSIT;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionScenario.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionScenario.java
new file mode 100644
index 0000000..e3db1ad
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransactionScenario.java
@@ -0,0 +1,28 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropTransactionScenario {
+
+    DEPOSIT,
+    WITHDRAWAL,
+    TRANSFER,
+    PAYMENT,
+    REFUND
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java
new file mode 100644
index 0000000..df4b05d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/domain/InteropTransferActionType.java
@@ -0,0 +1,26 @@
+/*
+ * 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.interoperation.domain;
+
+public enum InteropTransferActionType {
+
+    PREPARE,
+    CREATE,
+    ;
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CommitInteropTransferHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CommitInteropTransferHandler.java
new file mode 100644
index 0000000..3cf3033
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CommitInteropTransferHandler.java
@@ -0,0 +1,49 @@
+/**
+ * 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.interoperation.handler;
+
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ACTION_TRANSFER_COMMIT;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_TRANSFER;
+
+@Service
+@CommandType(entity = ENTITY_NAME_TRANSFER, action = ACTION_TRANSFER_COMMIT)
+public class CommitInteropTransferHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public CommitInteropTransferHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return this.interopService.commitTransfer(command);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropIdentifierHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropIdentifierHandler.java
new file mode 100644
index 0000000..1b88821
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropIdentifierHandler.java
@@ -0,0 +1,56 @@
+/**
+ * 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.interoperation.handler;
+
+import com.google.common.base.Strings;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_IDENTIFIER;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_REQUEST;
+
+@Service
+@CommandType(entity = ENTITY_NAME_IDENTIFIER, action = "CREATE")
+public class CreateInteropIdentifierHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public CreateInteropIdentifierHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        String[] split = command.getUrl().split("/");
+        int length = split.length;
+        String subIdOrType = Strings.emptyToNull(split[length]);
+        String idValue = split[length - 1];
+        InteropIdentifierType idType = InteropIdentifierType.valueOf(split[length - 2].toUpperCase());
+        return this.interopService.registerAccountIdentifier(idType, idValue, subIdOrType, command);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropQuoteHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropQuoteHandler.java
new file mode 100644
index 0000000..e389d4b
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropQuoteHandler.java
@@ -0,0 +1,48 @@
+/**
+ * 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.interoperation.handler;
+
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_QUOTE;
+
+@Service
+@CommandType(entity = ENTITY_NAME_QUOTE, action = "CREATE")
+public class CreateInteropQuoteHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public CreateInteropQuoteHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return this.interopService.createQuote(command);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropRequestHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropRequestHandler.java
new file mode 100644
index 0000000..98e2e23
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/CreateInteropRequestHandler.java
@@ -0,0 +1,49 @@
+/**
+ * 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.interoperation.handler;
+
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_REQUEST;
+
+@Service
+@CommandType(entity = ENTITY_NAME_REQUEST, action = "CREATE")
+public class CreateInteropRequestHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public CreateInteropRequestHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return this.interopService.createTransactionRequest(command);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/DeleteInteropRequestHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/DeleteInteropRequestHandler.java
new file mode 100644
index 0000000..dda41c4
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/DeleteInteropRequestHandler.java
@@ -0,0 +1,55 @@
+/**
+ * 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.interoperation.handler;
+
+import com.google.common.base.Strings;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_REQUEST;
+
+@Service
+@CommandType(entity = ENTITY_NAME_REQUEST, action = "DELETE")
+public class DeleteInteropRequestHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public DeleteInteropRequestHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        String[] split = command.getUrl().split("/");
+        int length = split.length;
+        String subIdOrType = Strings.emptyToNull(split[length]);
+        String idValue = split[length - 1];
+        InteropIdentifierType idType = InteropIdentifierType.valueOf(split[length - 2].toUpperCase());
+        return this.interopService.deleteAccountIdentifier(idType, idValue, subIdOrType);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/PrepareInteropTransferHandler.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/PrepareInteropTransferHandler.java
new file mode 100644
index 0000000..3994641
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/handler/PrepareInteropTransferHandler.java
@@ -0,0 +1,49 @@
+/**
+ * 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.interoperation.handler;
+
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.interoperation.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.ACTION_TRANSFER_PREPARE;
+import static org.apache.fineract.interoperation.util.InteropUtil.ENTITY_NAME_TRANSFER;
+
+@Service
+@CommandType(entity = ENTITY_NAME_TRANSFER, action = ACTION_TRANSFER_PREPARE)
+public class PrepareInteropTransferHandler implements NewCommandSourceHandler {
+
+    private final InteropService interopService;
+
+    @Autowired
+    public PrepareInteropTransferHandler(InteropService interopService) {
+        this.interopService = interopService;
+    }
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return this.interopService.prepareTransfer(command);
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java
new file mode 100644
index 0000000..8d90d5a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/serialization/InteropDataValidator.java
@@ -0,0 +1,110 @@
+/*
+ * 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.interoperation.serialization;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.apache.commons.lang.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
+import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.interoperation.data.InteropIdentifierRequestData;
+import org.apache.fineract.interoperation.data.InteropQuoteRequestData;
+import org.apache.fineract.interoperation.data.InteropTransactionRequestData;
+import org.apache.fineract.interoperation.data.InteropTransferRequestData;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+
+@Component
+public class InteropDataValidator {
+
+    private final FromJsonHelper jsonHelper;
+
+    @Autowired
+    public InteropDataValidator(final FromJsonHelper fromJsonHelper) {
+        this.jsonHelper = fromJsonHelper;
+    }
+
+    public InteropIdentifierRequestData validateAndParseCreateIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue,
+                                                                          String subIdOrType, JsonCommand command) {
+        final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.identifier");
+        JsonObject element = extractJsonObject(command);
+
+        InteropIdentifierRequestData result = InteropIdentifierRequestData.validateAndParse(dataValidator, idType, idValue, subIdOrType, element, jsonHelper);
+        throwExceptionIfValidationWarningsExist(dataValidator);
+
+        return result;
+    }
+
+    public InteropTransactionRequestData validateAndParseCreateRequest(JsonCommand command) {
+        final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.request");
+        JsonObject element = extractJsonObject(command);
+
+        InteropTransactionRequestData result = InteropTransactionRequestData.validateAndParse(dataValidator, element, jsonHelper);
+        throwExceptionIfValidationWarningsExist(dataValidator);
+
+        return result;
+    }
+
+    public InteropQuoteRequestData validateAndParseCreateQuote(JsonCommand command) {
+        final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.quote");
+        JsonObject element = extractJsonObject(command);
+
+        InteropQuoteRequestData result = InteropQuoteRequestData.validateAndParse(dataValidator, element, jsonHelper);
+        throwExceptionIfValidationWarningsExist(dataValidator);
+
+        return result;
+    }
+
+    public InteropTransferRequestData validateAndParsePrepareTransfer(JsonCommand command) {
+        return validateAndParseCreateTransfer(command);
+    }
+
+    public InteropTransferRequestData validateAndParseCreateTransfer(JsonCommand command) {
+        final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("interoperation.transfer");
+        JsonObject element = extractJsonObject(command);
+
+        InteropTransferRequestData result = InteropTransferRequestData.validateAndParse(dataValidator, element, jsonHelper);
+        throwExceptionIfValidationWarningsExist(dataValidator);
+
+        return result;
+    }
+
+    private JsonObject extractJsonObject(JsonCommand command) {
+        String json = command.json();
+        if (StringUtils.isBlank(json))
+            throw new InvalidJsonException();
+
+        final JsonElement element = jsonHelper.parse(json);
+        return element.getAsJsonObject();
+    }
+
+    private void throwExceptionIfValidationWarningsExist(DataValidatorBuilder dataValidator) {
+        if (dataValidator.hasError()) {
+            throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
+                    "Validation errors exist.", dataValidator.getDataValidationErrors());
+        }
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java
new file mode 100644
index 0000000..e0b0e7f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropService.java
@@ -0,0 +1,60 @@
+/**
+ * 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.interoperation.service;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.interoperation.data.InteropIdentifierResponseData;
+import org.apache.fineract.interoperation.data.InteropQuoteResponseData;
+import org.apache.fineract.interoperation.data.InteropTransactionRequestResponseData;
+import org.apache.fineract.interoperation.data.InteropTransferResponseData;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+
+import javax.validation.constraints.NotNull;
+
+public interface InteropService {
+
+    @NotNull
+    InteropIdentifierResponseData getAccountByIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType);
+
+    @NotNull
+    InteropIdentifierResponseData registerAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue,
+                                                            String subIdOrType, @NotNull JsonCommand command);
+
+    @NotNull
+    InteropIdentifierResponseData deleteAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue,
+                                                          String subIdOrType);
+
+    InteropTransactionRequestResponseData getTransactionRequest(@NotNull String transactionCode, @NotNull String requestCode);
+
+    @NotNull
+    InteropTransactionRequestResponseData createTransactionRequest(@NotNull JsonCommand command);
+
+    InteropQuoteResponseData getQuote(@NotNull String transactionCode, @NotNull String quoteCode);
+
+    @NotNull
+    InteropQuoteResponseData createQuote(@NotNull JsonCommand command);
+
+    InteropTransferResponseData getTransfer(@NotNull String transactionCode, @NotNull String transferCode);
+
+    @NotNull
+    InteropTransferResponseData prepareTransfer(@NotNull JsonCommand command);
+
+    @NotNull
+    InteropTransferResponseData commitTransfer(@NotNull JsonCommand command);
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
new file mode 100644
index 0000000..51c0b25
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
@@ -0,0 +1,429 @@
+/**
+ * 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.interoperation.service;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.interoperation.data.*;
+import org.apache.fineract.interoperation.domain.InteropIdentifier;
+import org.apache.fineract.interoperation.domain.InteropIdentifierRepository;
+import org.apache.fineract.interoperation.domain.InteropIdentifierType;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.interoperation.domain.InteropActionState;
+import org.apache.fineract.interoperation.serialization.InteropDataValidator;
+import org.apache.fineract.interoperation.util.MathUtil;
+import org.apache.fineract.portfolio.note.domain.Note;
+import org.apache.fineract.portfolio.note.domain.NoteRepository;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
+import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
+import org.apache.fineract.portfolio.paymenttype.domain.PaymentType;
+import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository;
+import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountDomainService;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionSummaryWrapper;
+import org.apache.fineract.portfolio.savings.domain.SavingsHelper;
+import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.domain.Specifications;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Path;
+import javax.persistence.criteria.Root;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import static org.apache.fineract.interoperation.util.InteropUtil.DEFAULT_LOCALE;
+import static org.apache.fineract.interoperation.util.InteropUtil.DEFAULT_ROUTING_CODE;
+import static org.apache.fineract.interoperation.util.InteropUtil.ISO8601_DATE_TIME_FORMAT;
+
+@Service
+public class InteropServiceImpl implements InteropService {
+
+    private final static Logger LOG = LoggerFactory.getLogger(InteropServiceImpl.class);
+
+    private final PlatformSecurityContext securityContext;
+    private final InteropDataValidator dataValidator;
+
+    private final SavingsAccountRepository savingsAccountRepository;
+    private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
+    private final ApplicationCurrencyRepository currencyRepository;
+    private final NoteRepository noteRepository;
+    private final PaymentTypeRepository paymentTypeRepository;
+    private final InteropIdentifierRepository identifierRepository;
+
+    private final SavingsHelper savingsHelper;
+    private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper;
+
+    private final SavingsAccountDomainService savingsAccountService;
+    private final PaymentDetailWritePlatformService paymentDetailService;
+
+    @Autowired
+    public InteropServiceImpl(PlatformSecurityContext securityContext,
+                              InteropDataValidator interopDataValidator,
+                              SavingsAccountRepository savingsAccountRepository,
+                              SavingsAccountTransactionRepository savingsAccountTransactionRepository,
+                              ApplicationCurrencyRepository applicationCurrencyRepository,
+                              NoteRepository noteRepository,
+                              PaymentTypeRepository paymentTypeRepository,
+                              InteropIdentifierRepository identifierRepository,
+                              SavingsHelper savingsHelper,
+                              SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
+                              SavingsAccountDomainService savingsAccountService,
+                              PaymentDetailWritePlatformService paymentDetailWritePlatformService) {
+        this.securityContext = securityContext;
+        this.dataValidator = interopDataValidator;
+        this.savingsAccountRepository = savingsAccountRepository;
+        this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
+        this.currencyRepository = applicationCurrencyRepository;
+        this.noteRepository = noteRepository;
+        this.paymentTypeRepository = paymentTypeRepository;
+        this.identifierRepository = identifierRepository;
+        this.savingsHelper = savingsHelper;
+        this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
+        this.savingsAccountService = savingsAccountService;
+        this.paymentDetailService = paymentDetailWritePlatformService;
+    }
+
+    @NotNull
+    @Transactional
+    public InteropIdentifierResponseData getAccountByIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) {
+        InteropIdentifier identifier = findIdentifier(idType, idValue, subIdOrType);
+        if (identifier == null)
+            throw new UnsupportedOperationException("Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType)));
+
+        return InteropIdentifierResponseData.build(identifier.getAccount().getExternalId());
+    }
+
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropIdentifierResponseData registerAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue,
+                                                                   String subIdOrType, @NotNull JsonCommand command) {
+        InteropIdentifierRequestData request = dataValidator.validateAndParseCreateIdentifier(idType, idValue, subIdOrType, command);
+        //TODO: error handling
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request.getAccountId());
+
+        AppUser createdBy = getLoginUser();
+
+        InteropIdentifier identifier = new InteropIdentifier(savingsAccount, request.getIdType(), request.getIdValue(),
+                request.getSubIdOrType(), createdBy.getUsername(), DateUtils.getDateOfTenant());
+
+        identifierRepository.save(identifier);
+
+        return InteropIdentifierResponseData.build(savingsAccount.getExternalId());
+    }
+
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropIdentifierResponseData deleteAccountIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue,
+                                                          String subIdOrType) {
+        InteropIdentifier identifier = findIdentifier(idType, idValue, subIdOrType);
+        if (identifier == null)
+            throw new UnsupportedOperationException("Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType)));
+
+        String accountId = identifier.getAccount().getExternalId();
+
+        identifierRepository.delete(identifier);
+
+        return InteropIdentifierResponseData.build(accountId);
+    }
+
+    @Override
+    public InteropTransactionRequestResponseData getTransactionRequest(@NotNull String transactionCode, @NotNull String requestCode) {
+        // always REJECTED until request info is stored
+        return InteropTransactionRequestResponseData.build(transactionCode, InteropActionState.REJECTED, requestCode);
+    }
+
+    @Override
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropTransactionRequestResponseData createTransactionRequest(@NotNull JsonCommand command) {
+        // only when Payee request transaction from Payer, so here role must be always Payer
+        InteropTransactionRequestData request = dataValidator.validateAndParseCreateRequest(command);
+
+        //TODO: error handling
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request);
+
+        return InteropTransactionRequestResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED,
+                request.getExpiration(), request.getExtensionList(), request.getRequestCode());
+    }
+
+    @Override
+    public InteropQuoteResponseData getQuote(@NotNull String transactionCode, @NotNull String quoteCode) {
+        return null;
+    }
+
+    @Override
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropQuoteResponseData createQuote(@NotNull JsonCommand command) {
+        InteropQuoteRequestData request = dataValidator.validateAndParseCreateQuote(command);
+
+        //TODO: error handling
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request);
+        SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType();
+
+        BigDecimal fee = transactionType.isDebit() ? savingsAccount.calculateWithdrawalFee(request.getAmount().getAmount()) : BigDecimal.ZERO;
+
+        return InteropQuoteResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED,
+                request.getExpiration(), request.getExtensionList(), request.getQuoteCode(), MoneyData.build(fee, savingsAccount.getCurrency().getCode()),
+                null);
+    }
+
+    @Override
+    public InteropTransferResponseData getTransfer(@NotNull String transactionCode, @NotNull String transferCode) {
+        return null;
+    }
+
+    @Override
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropTransferResponseData prepareTransfer(@NotNull JsonCommand command) {
+        InteropTransferRequestData request = dataValidator.validateAndParsePrepareTransfer(command);
+
+        //TODO: error handling
+        //TODO: REVERSE
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request);
+
+        BigDecimal total = validateTransfer(request, savingsAccount);
+
+        String transferCode = request.getTransferCode();
+        LocalDateTime transactionDate = DateUtils.getLocalDateTimeOfTenant();
+        if (MathUtil.isGreaterThanZero(total)) {
+            if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), total)) {
+                throw new UnsupportedOperationException();
+            }
+            if (findTransaction(savingsAccount, transferCode, SavingsAccountTransactionType.AMOUNT_HOLD) != null)
+                throw new UnsupportedOperationException("Transfer amount was already put on hold " + transferCode);
+
+            PaymentDetail paymentDetail = PaymentDetail.instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null);
+            AppUser appUser = getLoginUser();
+            SavingsAccountTransaction transaction = SavingsAccountTransaction.holdAmount(savingsAccount, savingsAccount.office(),
+                    paymentDetail, transactionDate.toLocalDate(), Money.of(savingsAccount.getCurrency(), total), new Date(),
+                    appUser);
+
+            savingsAccount.holdAmount(total);
+            savingsAccount.addTransaction(transaction);
+
+            savingsAccountRepository.save(savingsAccount);
+        }
+
+        return InteropTransferResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, request.getExpiration(),
+                request.getExtensionList(), transferCode, transactionDate);
+    }
+
+    @Override
+    @NotNull
+    @Transactional(propagation = Propagation.MANDATORY)
+    public InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) {
+        InteropTransferRequestData request = dataValidator.validateAndParseCreateTransfer(command);
+        SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType();
+        boolean debit = transactionType.isDebit();
+
+        //TODO: error handling
+        //TODO: REVERSE
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request);
+
+        validateTransfer(request, savingsAccount);
+
+        String transferCode = request.getTransferCode();
+        if (findTransaction(savingsAccount, transferCode, debit ? SavingsAccountTransactionType.WITHDRAWAL : SavingsAccountTransactionType.DEPOSIT) != null)
+            throw new UnsupportedOperationException("Transfer was already committed " + transferCode);
+
+        PaymentDetail paymentDetail = PaymentDetail.instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null);
+
+        LocalDateTime transactionDateTime = DateUtils.getLocalDateTimeOfTenant();
+        LocalDate transactionDate = transactionDateTime.toLocalDate();
+        Date createdDate = new Date();
+
+        SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, transferCode, SavingsAccountTransactionType.AMOUNT_HOLD);
+        if (holdTransaction != null && holdTransaction.getReleaseIdOfHoldAmountTransaction() == null) {
+            AppUser appUser = getLoginUser();
+
+            SavingsAccountTransaction releaseTransaction = SavingsAccountTransaction.releaseAmount(holdTransaction, transactionDate, createdDate, appUser);
+            releaseTransaction = savingsAccountTransactionRepository.saveAndFlush(releaseTransaction);
+            holdTransaction.updateReleaseId(releaseTransaction.getId());
+
+            savingsAccount.releaseAmount(holdTransaction.getAmount());
+            savingsAccount.addTransaction(releaseTransaction);
+
+            savingsAccountRepository.save(savingsAccount);
+        }
+
+        BigDecimal amount = request.getAmount().getAmount();
+        DateTimeFormatter fmt = getDateTimeFormatter(command);
+
+        SavingsAccountTransaction transaction;
+        if (debit) {
+            SavingsTransactionBooleanValues transactionValues = new SavingsTransactionBooleanValues(false, true, true, false, false);
+            transaction = savingsAccountService.handleWithdrawal(savingsAccount, fmt, transactionDate, amount,
+                    paymentDetail, transactionValues);
+        }
+        else {
+            transaction = savingsAccountService.handleDeposit(savingsAccount, fmt, transactionDate, amount,
+                    paymentDetail, false, true);
+        }
+
+        String note = request.getNote();
+        if (!StringUtils.isBlank(note)) {
+            noteRepository.save(Note.savingsTransactionNote(savingsAccount, transaction, note));
+        }
+
+        return InteropTransferResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED,
+                request.getExpiration(), request.getExtensionList(), request.getTransferCode(), transactionDateTime);
+    }
+
+    // Util
+
+    private SavingsAccount validateAndGetSavingAccount(String accountId) {
+        SavingsAccount savingsAccount = savingsAccountRepository.findByExternalId(accountId);
+        if (savingsAccount == null)
+            throw new SavingsAccountNotFoundException(accountId);
+        return savingsAccount;
+    }
+
+    private SavingsAccount validateAndGetSavingAccount(@NotNull InteropRequestData request) {
+        //TODO: error handling
+        SavingsAccount savingsAccount = validateAndGetSavingAccount(request.getAccountId());
+        savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper);
+
+        ApplicationCurrency currency = currencyRepository.findOneByCode(request.getAmount().getCurrency());
+        if (!savingsAccount.getCurrency().getCode().equals(currency.getCode()))
+            throw new UnsupportedOperationException();
+
+        SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType();
+        if (!savingsAccount.isTransactionAllowed(transactionType, request.getExpirationLocalDate()))
+            throw new UnsupportedOperationException();
+
+        request.normalizeAmounts(savingsAccount.getCurrency());
+        if (transactionType.isDebit() && MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), request.getAmount().getAmount()))
+            throw new UnsupportedOperationException();
+
+        return savingsAccount;
+    }
+
+    private BigDecimal validateTransfer(@NotNull InteropTransferRequestData request, @NotNull SavingsAccount savingsAccount) {
+        BigDecimal amount = request.getAmount().getAmount();
+        SavingsAccountTransactionType transactionType = request.getTransactionRole().getTransactionType();
+
+        BigDecimal total = transactionType.isDebit() ? amount : MathUtil.negate(amount);
+        MoneyData fspFee = request.getFspFee();
+        if (fspFee != null) {
+            if (!savingsAccount.getCurrency().getCode().equals(fspFee.getCurrency()))
+                throw new UnsupportedOperationException();
+            //TODO: compare with calculated quote fee
+            total = MathUtil.add(total, fspFee.getAmount());
+        }
+        MoneyData fspCommission = request.getFspCommission();
+        if (fspCommission != null) {
+            if (!savingsAccount.getCurrency().getCode().equals(fspCommission.getCurrency()))
+                throw new UnsupportedOperationException();
+            //TODO: compare with calculated quote commission
+            total = MathUtil.subtractToZero(total, fspCommission.getAmount());
+        }
+        return total;
+    }
+
+    private DateTimeFormatter getDateTimeFormatter(@NotNull JsonCommand command) {
+        Locale locale = command.extractLocale();
+        if (locale == null)
+            locale = DEFAULT_LOCALE;
+        String dateFormat = command.dateFormat();
+        if (StringUtils.isEmpty(dateFormat))
+            dateFormat = "yyyy-MM-dd HH:mm:ss.SSS";
+
+        return DateTimeFormat.forPattern(dateFormat).withLocale(locale);
+    }
+
+    PaymentType findPaymentType() {
+        List<PaymentType> paymentTypes = paymentTypeRepository.findAll();
+        for (PaymentType paymentType : paymentTypes) {
+            if (!paymentType.isCashPayment())
+                return paymentType;
+            //TODO: for now first not cash is retured:
+            // 1. must be added as initial setup,
+            // 2. if more than one non-cashe type added then update this code
+        }
+        return null;
+    }
+
+    SavingsAccountTransaction findTransaction(@NotNull SavingsAccount savingsAccount, @NotNull String transactionCode, SavingsAccountTransactionType transactionType) {
+        String routingCode = getRoutingCode();
+        for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
+            if (transactionType != null && !transactionType.getValue().equals(transaction.getTypeOf()))
+                continue;
+
+            PaymentDetail detail = transaction.getPaymentDetail();
+            if (detail != null && routingCode.equals(detail.getRoutingCode()) && transactionCode.equals(detail.getReceiptNumber()))
+                return transaction;
+        }
+        return null;
+    }
+
+    public InteropIdentifier findIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) {
+        return identifierRepository.findOne(Specifications.where(idTypeEqual(idType)).and(idValueEqual(idValue)).and(subIdOrTypeEqual(subIdOrType)));
+    }
+
+    public static Specification<InteropIdentifier> idTypeEqual(@NotNull InteropIdentifierType idType) {
+        return (Root<InteropIdentifier> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("type"), idType));
+    }
+
+    public static Specification<InteropIdentifier> idValueEqual(@NotNull String idValue) {
+        return (Root<InteropIdentifier> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("value"), idValue));
+    }
+
+    public static Specification<InteropIdentifier> subIdOrTypeEqual(String subIdOrType) {
+        return (Root<InteropIdentifier> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
+            Path<Object> path = root.get("subValueOrType");
+            return cb.and(subIdOrType == null ? cb.isNull(path) : cb.equal(path, subIdOrType));
+        };
+    }
+
+    private AppUser getLoginUser() {
+        return securityContext.getAuthenticatedUserIfPresent();
+    }
+
+    @NotNull
+    String getRoutingCode() {
+        return DEFAULT_ROUTING_CODE;
+    }
+}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/InteropUtil.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/InteropUtil.java
new file mode 100644
index 0000000..b33adeb
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/InteropUtil.java
@@ -0,0 +1,76 @@
+/*
+ * 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.interoperation.util;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import serp.util.Strings;
+
+import java.util.Locale;
+
+public class InteropUtil {
+
+    public static final String ISO8601_DATE_TIME_FORMAT = "yyyy-MM-ddTHH:mm:ss.SSS[-HH:MM]";
+    public static final String ISO8601_DATE_FORMAT = "yyyy-MM-dd";
+
+    public static final Locale DEFAULT_LOCALE = Locale.US;
+
+    public static final String ROOT_PATH = "interoperation";
+    public static final String DEFAULT_ROUTING_CODE = "INTEROPERATION";
+
+    public static final String ENTITY_NAME_IDENTIFIER = "INTERID";
+    public static final String ENTITY_NAME_REQUEST = "INTERREQUEST";
+    public static final String ENTITY_NAME_QUOTE = "INTERQUOTE";
+    public static final String ENTITY_NAME_TRANSFER = "INTERTRANSFER";
+
+    public static final String ACTION_TRANSFER_PREPARE = "PREPARE";
+    public static final String ACTION_TRANSFER_COMMIT = "CREATE";
+
+    public static final String PARAM_LOCALE = "locale";
+    public static final String PARAM_DATE_FORMAT = "dateFormat";
+
+    public static final String PARAM_TRANSACTION_CODE = "transactionCode";
+    public static final String PARAM_REQUEST_CODE = "requestCode";
+    public static final String PARAM_QUOTE_CODE = "quoteCode";
+    public static final String PARAM_TRANSFER_CODE = "transferCode";
+    public static final String PARAM_ACCOUNT_ID = "accountId";
+    public static final String PARAM_AMOUNT_TYPE = "amountType";
+    public static final String PARAM_AMOUNT = "amount";
+    public static final String PARAM_FEES = "fees";
+    public static final String PARAM_FSP_FEE = "fspFee";
+    public static final String PARAM_FSP_COMMISSION = "fspCommission";
+    public static final String PARAM_TRANSACTION_TYPE = "transactionType";
+    public static final String PARAM_TRANSACTION_ROLE = "transactionRole";
+    public static final String PARAM_NOTE = "note";
+    public static final String PARAM_GEO_CODE = "geoCode";
+
+    public static final String PARAM_CURRENCY = "currency";
+
+    public static final String PARAM_SCENARIO = "scenario";
+    public static final String PARAM_SUB_SCENARIO = "subScenario";
+    public static final String PARAM_INITIATOR = "initiator";
+    public static final String PARAM_INITIATOR_TYPE = "initiatorType";
+    public static final String PARAM_EXPIRATION = "expiration";
+
+    public static final String PARAM_LATITUDE = "latitude";
+    public static final String PARAM_LONGITUDE = "latitude";
+
+    public static final String PARAM_EXTENSION_LIST = "extensionList";
+    public static final String PARAM_KEY = "key";
+    public static final String PARAM_VALUE = "value";
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java
new file mode 100644
index 0000000..a5c7a78
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java
@@ -0,0 +1,403 @@
+/*
+ * 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.interoperation.util;
+
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.math.MathContext;
+
+public class MathUtil {
+
+    public static Long nullToZero(Long value) {
+        return nullToDefault(value, 0L);
+    }
+
+    public static Long nullToDefault(Long value, Long def) {
+        return value == null ? def : value;
+    }
+
+    public static Long zeroToNull(Long value) {
+        return isEmpty(value) ? null : value;
+    }
+
+    /** @return parameter value or ZERO if it is negative */
+    public static Long negativeToZero(Long value) {
+        return isGreaterThanZero(value) ? value : 0L;
+    }
+
+    public static boolean isEmpty(Long value) {
+        return value == null || value.equals(0L);
+    }
+
+    public static boolean isGreaterThanZero(Long value) {
+        return value != null && value > 0L;
+    }
+
+    public static boolean isLessThanZero(Long value) {
+        return value != null && value < 0L;
+    }
+
+    public static boolean isZero(Long value) {
+        return value != null && value.equals(0L);
+    }
+
+    public static boolean isEqualTo(Long first, Long second) {
+        return nullToZero(first).equals(nullToZero(second));
+    }
+
+    public static boolean isGreaterThan(Long first, Long second) {
+        return nullToZero(first) > nullToZero(second);
+    }
+
+    public static boolean isLessThan(Long first, Long second) {
+        return nullToZero(first) < nullToZero(second);
+    }
+
+    public static boolean isGreaterThanOrEqualTo(Long first, Long second) {
+        return nullToZero(first) >= nullToZero(second);
+    }
+
+    public static boolean isLessThanOrEqualZero(Long value) {
+        return nullToZero(value) <= 0L;
+    }
+
+    /** @return parameter value or negated value to positive */
+    public static Long abs(Long value) {
+        return value == null ? 0L : Math.abs(value);
+    }
+
+    /** @return calculates minimum of the two values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static Long min(Long first, Long second, boolean notNull) {
+        return first == null
+                ? (notNull ? second : null)
+                : second == null ? (notNull ? first : null) : Math.min(first, second);
+    }
+
+    /** @return calculates minimum of the values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static Long min(Long first, Long second, Long third, boolean notNull) {
+        return min(min(first, second, notNull), third, notNull);
+    }
+
+    /** @return sum the two values considering null values */
+    public static Long add(Long first, Long second) {
+        return first == null
+                ? second
+                : second == null ? first : Math.addExact(first, second);
+    }
+
+    /** @return sum the values considering null values */
+    public static Long add(Long first, Long second, Long third) {
+        return add(add(first, second), third);
+    }
+
+    /** @return sum the values considering null values */
+    public static Long add(Long first, Long second, Long third, Long fourth) {
+        return add(add(add(first, second), third), fourth);
+    }
+
+    /** @return sum the values considering null values */
+    public static Long add(Long first, Long second, Long third, Long fourth, Long fifth) {
+        return add(add(add(add(first, second), third), fourth), fifth);
+    }
+
+    /** @return first minus second considering null values, maybe negative */
+    public static Long subtract(Long first, Long second) {
+        return first == null
+                ? null
+                : second == null ? first : Math.subtractExact(first, second);
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static Long subtractToZero(Long first, Long second, Long third) {
+        return subtractToZero(subtract(first, second), third);
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static Long subtractToZero(Long first, Long second, Long third, Long fourth) {
+        return subtractToZero(subtract(subtract(first, second), third), fourth);
+    }
+
+    /** @return NONE negative first minus second considering null values */
+    public static Long subtractToZero(Long first, Long second) {
+        return negativeToZero(subtract(first, second));
+    }
+
+    /** @return BigDecimal null safe negate */
+    public static Long negate(Long amount) {
+        return isEmpty(amount) ? amount : Math.negateExact(amount);
+    }
+
+
+    // ----------------- BigDecimal -----------------
+
+    public static BigDecimal nullToZero(BigDecimal value) {
+        return nullToDefault(value, BigDecimal.ZERO);
+    }
+
+    public static BigDecimal nullToDefault(BigDecimal value, BigDecimal def) {
+        return value == null ? def : value;
+    }
+
+    public static BigDecimal zeroToNull(BigDecimal value) {
+        return isEmpty(value) ? null : value;
+    }
+
+    /** @return parameter value or ZERO if it is negative */
+    public static BigDecimal negativeToZero(BigDecimal value) {
+        return isGreaterThanZero(value) ? value : BigDecimal.ZERO;
+    }
+
+    public static boolean isEmpty(BigDecimal value) {
+        return value == null || BigDecimal.ZERO.compareTo(value) == 0;
+    }
+
+    public static boolean isGreaterThanZero(BigDecimal value) {
+        return value != null && value.compareTo(BigDecimal.ZERO) > 0;
+    }
+
+    public static boolean isLessThanZero(BigDecimal value) {
+        return value != null && value.compareTo(BigDecimal.ZERO) < 0;
+    }
+
+    public static boolean isZero(BigDecimal value) {
+        return value != null && value.compareTo(BigDecimal.ZERO) == 0;
+    }
+
+    public static boolean isEqualTo(BigDecimal first, BigDecimal second) {
+        return nullToZero(first).compareTo(nullToZero(second)) == 0;
+    }
+
+    public static boolean isGreaterThan(BigDecimal first, BigDecimal second) {
+        return nullToZero(first).compareTo(nullToZero(second)) > 0;
+    }
+
+    public static boolean isLessThan(BigDecimal first, BigDecimal second) {
+        return nullToZero(first).compareTo(nullToZero(second)) < 0;
+    }
+
+    public static boolean isGreaterThanOrEqualTo(BigDecimal first, BigDecimal second) {
+        return nullToZero(first).compareTo(nullToZero(second)) >= 0;
+    }
+
+    public static boolean isLessThanOrEqualZero(BigDecimal value) {
+        return nullToZero(value).compareTo(BigDecimal.ZERO) <= 0;
+    }
+
+    /** @return parameter value or negated value to positive */
+    public static BigDecimal abs(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value.abs();
+    }
+
+    /** @return calculates minimum of the two values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static BigDecimal min(BigDecimal first, BigDecimal second, boolean notNull) {
+        return notNull
+                ? first == null
+                ? second
+                : second == null ? first : min(first, second, false)
+                : isLessThan(first, second) ? first : second;
+    }
+
+    /** @return calculates minimum of the values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static BigDecimal min(BigDecimal first, BigDecimal second, BigDecimal third, boolean notNull) {
+        return min(min(first, second, notNull), third, notNull);
+    }
+
+    /** @return sum the two values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second) {
+        return add(first, second, MoneyHelper.getMathContext());
+    }
+
+    /** @return sum the two values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, MathContext mc) {
+        return first == null
+                ? second
+                : second == null ? first : first.add(second, mc);
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third) {
+        return add(first, second, third, MoneyHelper.getMathContext());
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third, MathContext mc) {
+        return add(add(first, second, mc), third, mc);
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third, BigDecimal fourth) {
+        return add(first, second, third, fourth, MoneyHelper.getMathContext());
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third, BigDecimal fourth, MathContext mc) {
+        return add(add(add(first, second, mc), third, mc), fourth, mc);
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third, BigDecimal fourth, BigDecimal fifth) {
+        return add(first, second, third, fourth, fifth, MoneyHelper.getMathContext());
+    }
+
+    /** @return sum the values considering null values */
+    public static BigDecimal add(BigDecimal first, BigDecimal second, BigDecimal third, BigDecimal fourth, BigDecimal fifth, MathContext mc) {
+        return add(add(add(add(first, second, mc), third, mc), fourth, mc), fifth, mc);
+    }
+
+    /** @return first minus second considering null values, maybe negative */
+    public static BigDecimal subtract(BigDecimal first, BigDecimal second) {
+        return first == null
+                ? null
+                : second == null ? first : first.subtract(second, MoneyHelper.getMathContext());
+    }
+
+    /** @return NONE negative first minus second considering null values */
+    public static BigDecimal subtractToZero(BigDecimal first, BigDecimal second) {
+        return negativeToZero(subtract(first, second));
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static BigDecimal subtractToZero(BigDecimal first, BigDecimal second, BigDecimal third) {
+        MathContext mc = MoneyHelper.getMathContext();
+        return subtractToZero(subtract(first, second), third);
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static BigDecimal subtractToZero(BigDecimal first, BigDecimal second, BigDecimal third, BigDecimal fourth) {
+        return subtractToZero(subtract(subtract(first, second), third), fourth);
+    }
+
+    /** @return BigDecimal with scale set to the 'digitsAfterDecimal' of the parameter currency */
+    public static BigDecimal normalizeAmount(BigDecimal amount, @NotNull MonetaryCurrency currency) {
+        return amount == null ? null : amount.setScale(currency.getDigitsAfterDecimal(), MoneyHelper.getRoundingMode());
+    }
+
+    /** @return BigDecimal null safe negate */
+    public static BigDecimal negate(BigDecimal amount) {
+        return negate(amount, MoneyHelper.getMathContext());
+    }
+
+    /** @return BigDecimal null safe negate */
+    public static BigDecimal negate(BigDecimal amount, MathContext mc) {
+        return isEmpty(amount) ? amount : amount.negate(mc);
+    }
+
+
+    // ----------------- Money -----------------
+
+    public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency) {
+        return nullToDefault(value, Money.zero(currency));
+    }
+
+    public static Money nullToDefault(Money value, Money def) {
+        return value == null ? def : value;
+    }
+
+    public static Money zeroToNull(Money value) {
+        return isEmpty(value) ? null : value;
+    }
+
+    /** @return parameter value or ZERO if it is negative */
+    public static Money negativeToZero(Money value) {
+        return value == null || isGreaterThanZero(value) ? value : Money.zero(value.getCurrency());
+    }
+
+    public static boolean isEmpty(Money value) {
+        return value == null || value.isZero();
+    }
+
+    public static boolean isGreaterThanZero(Money value) {
+        return value != null && value.isGreaterThanZero();
+    }
+
+    public static boolean isLessThanZero(Money value) {
+        return value != null && value.isLessThanZero();
+    }
+
+    public static boolean isEqualTo(Money first, Money second) {
+        return first == null ? second == null : (second != null && first.isEqualTo(second));
+    }
+
+    public static boolean isGreaterThan(Money first, Money second) {
+        return second == null || (first != null && first.isGreaterThan(second));
+    }
+
+    public static boolean isLessThan(Money first, Money second) {
+        return first == null || (second != null && first.isLessThan(second));
+    }
+
+    public static Money plus(Money first, Money second) {
+        return first == null
+                ? second
+                : second == null ? first : first.plus(second);
+    }
+
+    public static Money plus(Money first, Money second, Money third) {
+        return plus(plus(first, second), third);
+    }
+
+    public static Money plus(Money first, Money second, Money third, Money fourth) {
+        return plus(plus(plus(first, second), third), fourth);
+    }
+
+    public static Money minus(Money first, Money second) {
+        return first == null
+                ? null
+                : second == null ? first : first.minus(second);
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static Money minusToZero(Money first, Money second, Money third) {
+        return minusToZero(minus(first, second), third);
+    }
+
+    /** @return first minus the others considering null values, maybe negative */
+    public static Money minusToZero(Money first, Money second, Money third, Money fourth) {
+        return minusToZero(minus(minus(first, second), third), fourth);
+    }
+
+    /** @return NONE negative first minus second considering null values */
+    public static Money minusToZero(Money first, Money second) {
+        return negativeToZero(minus(first, second));
+    }
+
+    /** @return calculates minimum of the two values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static Money min(Money first, Money second, boolean notNull) {
+        return notNull
+                ? first == null
+                ? second
+                : second == null ? first : min(first, second, false)
+                : isLessThan(first, second) ? first : second;
+    }
+
+    /** @return calculates minimum of the values considering null values
+     * @param notNull if true then null parameter is omitted, otherwise returns null */
+    public static Money min(Money first, Money second, Money third, boolean notNull) {
+        return min(min(first, second, notNull), third, notNull);
+    }
+}