interoperable - service
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1b77db
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+.gradle
+.idea
+**/build/
+**/target/
+**/out/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+*.iml
+
+*.log
+
+*.toDelete
\ No newline at end of file
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..90705e0
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,16 @@
+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.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed 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.
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..822eedb
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache Fineract CN Provisioner
+Copyright [2017-2018] The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
\ No newline at end of file
diff --git a/README.md b/README.md
index 07a4dd0..93c01b6 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,26 @@
-fineract-cn-interoperation
+# Apache Fineract CN Provisioner
+
+This service provisions services for tenants of an Apache Fineract CN installation.
+
+## Abstract
+Apache Fineract CN is an application framework for digital financial services, a system to support nationwide and cross-national financial transactions and help to level and speed the creation of an inclusive, interconnected digital economy for every nation in the world.
+
+## Versioning
+The version numbers follow the [Semantic Versioning](http://semver.org/) scheme.
+
+In addition to MAJOR.MINOR.PATCH the following postfixes are used to indicate the development state.
+
+* BUILD-SNAPSHOT - A release currently in development.
+* M - A _milestone_ release include specific sets of functions and are released as soon as the functionality is complete.
+* RC - A _release candidate_ is a version with potential to be a final product, considered _code complete_.
+* RELEASE - indicates that this release is the best available version and is recommended for all usage.
+
+The versioning layout is {MAJOR}.{MINOR}.{PATCH}-{INDICATOR}[.{PATCH}]. Only milestones and release candidates can have patch versions. Some examples:
+
+1.2.3-BUILD-SNAPSHOT
+1.3.5-M.1
+1.5.7-RC.2
+2.0.0-RELEASE
+
+## License
+See [LICENSE](LICENSE) file.
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..883c7ca
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE'
+ }
+}
+
+plugins {
+ id "com.github.hierynomus.license" version "0.13.1"
+ id("org.nosphere.apache.rat") version "0.3.1"
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+ compile(
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
+ [group: 'org.apache.fineract.cn', name: 'api', version: versions.frameworkapi],
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
+ [group: 'org.hibernate', name: 'hibernate-validator-annotation-processor', version: versions.validator],
+ [group: 'org.apache.fineract.cn.deposit-account-management', name: 'api', version: versions.frameworkdeposit]
+ )
+}
+
+publishing {
+ publications {
+ api(MavenPublication) {
+ from components.java
+ groupId project.group
+ artifactId project.name
+ version project.version
+ }
+ }
+}
diff --git a/api/settings.gradle b/api/settings.gradle
new file mode 100644
index 0000000..491f745
--- /dev/null
+++ b/api/settings.gradle
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+rootProject.name = 'api'
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/EventConstants.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/EventConstants.java
new file mode 100644
index 0000000..d2d3faf
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/EventConstants.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.cn.interoperation.api.v1;
+
+@SuppressWarnings("unused")
+public interface EventConstants {
+
+ String DESTINATION = "interoperation-v1";
+ String SELECTOR_NAME = "action";
+
+ String OPERATION_HEADER = "operation";
+ String INITIALIZE = "initialize";
+ String SELECTOR_INITIALIZE = OPERATION_HEADER + " = '" + INITIALIZE + "'";
+
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/PermittableGroupIds.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/PermittableGroupIds.java
new file mode 100644
index 0000000..bfee891
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/PermittableGroupIds.java
@@ -0,0 +1,29 @@
+/*
+ * 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.cn.interoperation.api.v1;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public interface PermittableGroupIds {
+
+ String INTEROPERATION_SINGLE = "interoperation__v1__single";
+ String INTEROPERATION_BULK = "interoperation__v1__bulk";
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/client/InteroperationManager.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/client/InteroperationManager.java
new file mode 100644
index 0000000..0692273
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/client/InteroperationManager.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cn.interoperation.api.v1.client;
+
+import org.apache.fineract.cn.api.util.CustomFeignClientsConfiguration;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+@SuppressWarnings("unused")
+@FeignClient(value = "interoperation-v1", path = "/interoperation/v1", configuration = CustomFeignClientsConfiguration.class)
+public interface InteroperationManager {
+
+ @RequestMapping(
+ value = "/health",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ void fetchActions();
+
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropActionState.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropActionState.java
new file mode 100644
index 0000000..4bb4001
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/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.cn.interoperation.api.v1.domain;
+
+public enum InteropActionState {
+ ACCEPTED,
+ REJECTED
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropActionType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropActionType.java
new file mode 100644
index 0000000..7e72a73
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropActionType.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.cn.interoperation.api.v1.domain;
+
+public enum InteropActionType {
+
+ REQUEST,
+ QUOTE,
+ PREPARE,
+ COMMIT,
+ ABORT,
+ ;
+
+ public static InteropActionType[] VALUES = values();
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropAmountType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropAmountType.java
new file mode 100644
index 0000000..02dafe4
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropAmountType.java
@@ -0,0 +1,25 @@
+/*
+ * 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.cn.interoperation.api.v1.domain;
+
+public enum InteropAmountType {
+
+ SEND,
+ RECEIVE
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropIdentifierType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropIdentifierType.java
new file mode 100644
index 0000000..08780f4
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/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.cn.interoperation.api.v1.domain;
+
+public enum InteropIdentifierType {
+
+ MSISDN,
+ EMAIL,
+ PERSONAL_ID,
+ BUSINESS,
+ DEVICE,
+ ACCOUNT_ID,
+ IBAN,
+ ALIAS
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropInitiatorType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropInitiatorType.java
new file mode 100644
index 0000000..376a571
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropInitiatorType.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.cn.interoperation.api.v1.domain;
+
+public enum InteropInitiatorType {
+
+ CONSUMER,
+ AGENT,
+ BUSINESS,
+ DEVICE
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropState.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropState.java
new file mode 100644
index 0000000..3cc7505
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropState.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.cn.interoperation.api.v1.domain;
+
+public enum InteropState {
+
+ REQUESTED,
+ QUOTED,
+ PREPARED,
+ COMMITTED,
+ ABORTED,
+ ;
+
+ public static InteropState[] VALUES = values();
+
+
+ public static InteropState forAction(InteropActionType action) {
+ switch (action) {
+ case REQUEST:
+ return REQUESTED;
+ case QUOTE:
+ return QUOTED;
+ case PREPARE:
+ return PREPARED;
+ case COMMIT:
+ return COMMITTED;
+ case ABORT:
+ return ABORTED;
+ default:
+ return null; // TODO fail
+ }
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropStateMachine.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropStateMachine.java
new file mode 100644
index 0000000..e5bc409
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropStateMachine.java
@@ -0,0 +1,57 @@
+/*
+ * 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.cn.interoperation.api.v1.domain;
+
+import org.apache.fineract.cn.lang.ServiceException;
+
+import javax.validation.constraints.NotNull;
+
+public class InteropStateMachine {
+
+ private InteropStateMachine() {
+
+ }
+
+ /**
+ * Executes the given transition from one status to another when the given transition (actualEvent + action) is valid.
+ *
+ * @param currentState actual {@link InteropState}
+ * @param action the {@link InteropActionType}
+ * @return the new {@link InteropState} for the tag if the given transition is valid
+ *
+ */
+ public static InteropState handleTransition(InteropState currentState, @NotNull InteropActionType action) {
+ InteropState transitionState = getTransitionState(currentState, action);
+ if (transitionState == null) {
+ throw new ServiceException("State transition is not valid: ({0}) and action: ({1})", currentState, action);
+ }
+ return transitionState;
+ }
+
+ public static boolean isValidAction(InteropState currentState, @NotNull InteropActionType action) {
+ if (currentState == null || action == InteropActionType.ABORT)
+ return true;
+ InteropState transitionState = getTransitionState(currentState, action);
+ return transitionState != null && transitionState.ordinal() > currentState.ordinal();
+ }
+
+ private static InteropState getTransitionState(InteropState currentState, @NotNull InteropActionType action) {
+ return InteropState.forAction(action);
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransactionRole.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransactionRole.java
new file mode 100644
index 0000000..2a0b800
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransactionRole.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cn.interoperation.api.v1.domain;
+
+public enum InteropTransactionRole {
+
+ PAYER,
+ PAYEE,
+ ;
+
+ public boolean isWithdraw() {
+ return this == PAYER;
+ }
+
+ public TransactionType getTransactionType() {
+ return this == PAYER ? TransactionType.CURRENCY_WITHDRAWAL : TransactionType.CURRENCY_DEPOSIT;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransactionScenario.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransactionScenario.java
new file mode 100644
index 0000000..de5b776
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/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.cn.interoperation.api.v1.domain;
+
+public enum InteropTransactionScenario {
+
+ DEPOSIT,
+ WITHDRAWAL,
+ TRANSFER,
+ PAYMENT,
+ REFUND
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransferActionType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/InteropTransferActionType.java
new file mode 100644
index 0000000..3f48ecb
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/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.cn.interoperation.api.v1.domain;
+
+public enum InteropTransferActionType {
+
+ PREPARE,
+ CREATE,
+ ;
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/TransactionType.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/TransactionType.java
new file mode 100644
index 0000000..3ecf30e
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/TransactionType.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cn.interoperation.api.v1.domain;
+
+/**
+ * @code org.apache.fineract.cn.accounting.service.internal.repository.AccountEntryEntity.type
+ */
+public enum TransactionType {
+
+ ACCOUNT_OPENING("ACCO"),
+ ACCOUNT_CLOSING("ACCC"),
+ ACCOUNT_TRANSFER("ACCT"),
+
+ ACH_CREDIT("ACDT"),
+ ACH_DEBIT("ADBT"),
+ ACH_ADJUSTMENTS("ADJT"),
+ ACH_PREAUTHORISED("APAC"),
+ ACH_RETURN("ARET"),
+ ACH_REVERSAL("AREV"),
+ ACH_SETTLEMENT("ASET"),
+ ACH_TRANSACTION("ATXN"),
+
+ ARP_DEBIT("ARPD"),
+ CURRENCY_DEPOSIT("FCDP"),
+ CURRENCY_WITHDRAWAL("FCWD"),
+
+ BRANCH_TRANSFER("BACT"),
+ BRANCH_DEPOSIT("BCDP"),
+ BRANCH_WITHDRAWAL("BCWD"),
+
+ CASH_DEPOSIT("CDPT"),
+ CASH_WITHDRAWAL("CWDL"),
+ CASH_LETTER("CASH"),
+
+ CHEQUE("CCHQ"),
+ BRANCH_CHEQUE("BCHQ"),
+ CERTIFIED_CHEQUE("CCCH"),
+ CROSSED_CHEQUE("CRCQ"),
+ CHEQUE_REVERSAL("CQRV"),
+ CHEQUE_OPEN("OPCQ"),
+ CHEQUE_ORDER("ORCQ"),
+
+ CHARGES_PAYMENT("CHRG"),
+ FEES_PAYMENT("FEES"),
+ TAXES_PAYMENT("TAXE"),
+ PRINCIPAL_PAYMENT("PPAY"),
+ INTEREST_PAYMENT("INTR"),
+
+ DIRECTDEBIT_PAYMENT("PMDD"),
+ DIRECTDEBIT_PREAUTHORISED("PADD"),
+ BRANCH_DIRECTDEBIT("BBDD"),
+
+ SMARTCARD_PAYMENT("SMRT"),
+ POINTOFSALE_CREDITCARD("POSC"),
+ POINTOFSALE_DEBITCARD("POSD"),
+ POINTOFSALE_PAYMENT("POSP"),
+
+ DOMESTIC_CREDIT("DMCT"),
+ INTRACOMPANY_TRANSFER("ICCT"),
+
+ MIXED_DEPOSIT("MIXD"),
+ MISCELLANEOUS_DEPOSIT("MSCD"),
+ OTHER("OTHR"),
+
+ CONTROLLED_DISBURSEMENT("CDIS"),
+ CONTROLLED_DISBURSEMENT2("DSBR"),
+ CREDIT_ADJUSTMENT("CAJT"),
+ DEBIT_ADJUSTMENT("DAJT"),
+ EXCHANGERATE_ADJUSTMENT("ERTA"),
+ YID_ADJUSTMENT("YTDA"),
+ REIMBURSEMENT("RIMB"),
+ DRAWDOWN("DDWN"),
+
+ ERROR_NOTAVAILABLE("NTAV"),
+ ERROR_POSTING("PSTE"),
+ ERROR_CANCELLATION("RCDD"),
+ ERROR_CANCELLATION2("RPCR"),
+ ERROR_ZEROBALANCE("ZABA"),
+ ;
+
+ private final String code;
+
+ TransactionType(String code) {
+ this.code = code;
+ }
+
+ public String getCode() {
+ return code;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/ExtensionData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/ExtensionData.java
new file mode 100644
index 0000000..7cd7e3f
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/ExtensionData.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.cn.interoperation.api.v1.domain.data;
+
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotNull;
+
+public class ExtensionData {
+
+ @NotNull
+ @Length(min = 1, max = 128)
+ private String key;
+
+ @NotNull
+ @Length(min = 1, max = 128)
+ private String value;
+
+ protected ExtensionData() {
+ }
+
+ public ExtensionData(@NotNull String key, @NotNull String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ protected void setKey(String key) {
+ this.key = key;
+ }
+
+ protected void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/GeoCodeData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/GeoCodeData.java
new file mode 100644
index 0000000..ca862db
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/GeoCodeData.java
@@ -0,0 +1,57 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+public class GeoCodeData {
+
+ @NotNull
+ @Pattern(regexp = "^(\\+|-)?(?:90(?:(?:\\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\\.[0-9]{1,6})?))$")
+ private String latitude;
+
+ @NotNull
+ @Pattern(regexp = "^(\\+|-)?(?:180(?:(?:\\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\\.[0-9]{1,6})?))$")
+ private String longitude;
+
+ protected GeoCodeData() {
+ }
+
+ public GeoCodeData(String latitude, String longitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ public String getLatitude() {
+ return latitude;
+ }
+
+ public String getLongitude() {
+ return longitude;
+ }
+
+ protected void setLatitude(String latitude) {
+ this.latitude = latitude;
+ }
+
+ protected void setLongitude(String longitude) {
+ this.longitude = longitude;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierCommand.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierCommand.java
new file mode 100644
index 0000000..ed66827
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierCommand.java
@@ -0,0 +1,61 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropIdentifierType;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.constraints.NotNull;
+
+public class InteropIdentifierCommand extends InteropIdentifierData {
+
+ @NotNull
+ private InteropIdentifierType idType;
+ @NotEmpty
+ @Length(max = 128)
+ private String idValue;
+ @Length(max = 128)
+ private String subIdOrType;
+
+
+ protected InteropIdentifierCommand() {
+ }
+
+ public InteropIdentifierCommand(@NotNull InteropIdentifierData requestData, @NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) {
+ super(requestData.getAccountId());
+ this.idType = idType;
+ this.idValue = idValue;
+ this.subIdOrType = subIdOrType;
+ }
+
+ @NotNull
+ public InteropIdentifierType getIdType() {
+ return idType;
+ }
+
+ @NotNull
+ public String getIdValue() {
+ return idValue;
+ }
+
+ public String getSubIdOrType() {
+ return subIdOrType;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierData.java
new file mode 100644
index 0000000..1872135
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierData.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.cn.interoperation.api.v1.domain.data;
+
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.constraints.NotNull;
+
+public class InteropIdentifierData {
+
+ @NotEmpty
+ @Length(max = 32)
+ private String accountId;
+
+
+ protected InteropIdentifierData() {
+ }
+
+ public InteropIdentifierData(@NotNull String accountId) {
+ this.accountId = accountId;
+ }
+
+ @NotNull
+ public String getAccountId() {
+ return accountId;
+ }
+
+ protected void setAccountId(String accountId) {
+ this.accountId = accountId;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierDeleteCommand.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierDeleteCommand.java
new file mode 100644
index 0000000..a98294f
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropIdentifierDeleteCommand.java
@@ -0,0 +1,59 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropIdentifierType;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.constraints.NotNull;
+
+public class InteropIdentifierDeleteCommand {
+
+ @NotNull
+ private InteropIdentifierType idType;
+ @NotEmpty
+ @Length(max = 128)
+ private String idValue;
+ @Length(max = 128)
+ private String subIdOrType;
+
+
+ protected InteropIdentifierDeleteCommand() {
+ }
+
+ public InteropIdentifierDeleteCommand(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) {
+ this.idType = idType;
+ this.idValue = idValue;
+ this.subIdOrType = subIdOrType;
+ }
+
+ @NotNull
+ public InteropIdentifierType getIdType() {
+ return idType;
+ }
+
+ @NotNull
+ public String getIdValue() {
+ return idValue;
+ }
+
+ public String getSubIdOrType() {
+ return subIdOrType;
+ }}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropQuoteRequestData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropQuoteRequestData.java
new file mode 100644
index 0000000..4eaa853
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropAmountType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionRole;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.beans.Transient;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+public class InteropQuoteRequestData extends InteropRequestData {
+
+ @NotNull
+ @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
+ private String quoteCode;
+
+ @NotNull
+ private InteropAmountType amountType;
+
+ @Valid
+ private MoneyData fees; // only for disclosed Payer fees on the Payee side
+
+ private InteropQuoteRequestData() {
+ super();
+ }
+
+ 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 Currency currency) {
+ super.normalizeAmounts(currency);
+ if (fees != null)
+ fees.normalizeAmount(currency);
+ }
+
+ protected void setQuoteCode(String quoteCode) {
+ this.quoteCode = quoteCode;
+ }
+
+ protected void setAmountType(InteropAmountType amountType) {
+ this.amountType = amountType;
+ }
+
+ protected void setFees(MoneyData fees) {
+ this.fees = fees;
+ }
+
+ @NotNull
+ @Transient
+ @Override
+ public InteropActionType getActionType() {
+ return InteropActionType.QUOTE;
+ }
+
+ @Transient
+ @NotNull
+ @Override
+ public String getIdentifier() {
+ return quoteCode;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropQuoteResponseData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropQuoteResponseData.java
new file mode 100644
index 0000000..f572b8d
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropQuoteResponseData.java
@@ -0,0 +1,69 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InteropQuoteResponseData extends InteropResponseData {
+
+ @NotNull
+ private final String quoteCode;
+
+ private MoneyData fspFee;
+
+ private MoneyData fspCommission;
+
+ private InteropQuoteResponseData(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration, List<ExtensionData> extensionList, @NotNull String quoteCode,
+ MoneyData fspFee, MoneyData fspCommission) {
+ super(transactionCode, state, expiration, extensionList);
+ this.quoteCode = quoteCode;
+ this.fspFee = fspFee;
+ this.fspCommission = fspCommission;
+ }
+
+ public static InteropQuoteResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration, List<ExtensionData> extensionList,
+ @NotNull String quoteCode, MoneyData fspFee, MoneyData fspCommission) {
+ return new InteropQuoteResponseData(transactionCode, state, expiration, extensionList, quoteCode, fspFee, fspCommission);
+ }
+
+ public static InteropQuoteResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration, @NotNull String quoteCode,
+ MoneyData fspFee, MoneyData fspCommission) {
+ return build(transactionCode, state, expiration, null, quoteCode, fspFee, fspCommission);
+ }
+
+ public String getQuoteCode() {
+ return quoteCode;
+ }
+
+ public MoneyData getFspFee() {
+ return fspFee;
+ }
+
+ public MoneyData getFspCommission() {
+ return fspCommission;
+ }
+
+ public void setFspCommission(MoneyData fspCommission) {
+ this.fspCommission = fspCommission;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropRequestData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropRequestData.java
new file mode 100644
index 0000000..c8a5444
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropRequestData.java
@@ -0,0 +1,195 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionRole;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.beans.Transient;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public abstract class InteropRequestData {
+
+ public static final String IDENTIFIER_SEPARATOR = "_";
+
+ @NotNull
+ @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
+ private String transactionCode;
+
+ @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
+ private String requestCode;
+
+ @NotEmpty
+ @Length(max = 32)
+ private String accountId;
+
+ @NotNull
+ @Valid
+ private MoneyData amount;
+
+ @NotNull
+ private InteropTransactionRole transactionRole;
+
+ @Valid
+ private InteropTransactionTypeData transactionType;
+
+ @Length(max = 128)
+ private String note;
+
+ @Valid
+ private GeoCodeData geoCode;
+
+ private LocalDateTime expiration;
+
+ @Size(max = 16)
+ @Valid
+ private List<ExtensionData> extensionList;
+
+ protected InteropRequestData() {
+ }
+
+ 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 Currency currency) {
+ amount.normalizeAmount(currency);
+ }
+
+ protected void setTransactionCode(String transactionCode) {
+ this.transactionCode = transactionCode;
+ }
+
+ protected void setRequestCode(String requestCode) {
+ this.requestCode = requestCode;
+ }
+
+ protected void setAccountId(String accountId) {
+ this.accountId = accountId;
+ }
+
+ protected void setAmount(MoneyData amount) {
+ this.amount = amount;
+ }
+
+ protected void setTransactionRole(InteropTransactionRole transactionRole) {
+ this.transactionRole = transactionRole;
+ }
+
+ protected void setTransactionType(InteropTransactionTypeData transactionType) {
+ this.transactionType = transactionType;
+ }
+
+ @Transient
+ public abstract InteropActionType getActionType();
+
+ @Transient
+ @NotNull
+ public String getIdentifier() {
+ return transactionCode;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropResponseData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropResponseData.java
new file mode 100644
index 0000000..d8f7cfd
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropResponseData.java
@@ -0,0 +1,77 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+public abstract class InteropResponseData {
+
+ @NotNull
+ private final String transactionCode;
+
+ @NotNull
+ private final InteropActionState state;
+
+ private final String expiration;
+
+ private final List<ExtensionData> extensionList;
+
+
+ protected InteropResponseData(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration, List<ExtensionData> extensionList) {
+ this.transactionCode = transactionCode;
+ this.state = state;
+ this.expiration = format(expiration);
+ this.extensionList = extensionList;
+ }
+
+ 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, DateTimeFormatter.ISO_DATE_TIME);
+ }
+
+ protected static String format(LocalDateTime date) {
+ return date == null ? null : date.format(DateTimeFormatter.ISO_DATE_TIME);
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestData.java
new file mode 100644
index 0000000..b150abb
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestData.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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionRole;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InteropTransactionRequestData extends InteropRequestData {
+
+ protected InteropTransactionRequestData() {
+ super();
+ }
+
+ 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());
+ }
+
+ @NotNull
+ @Transient
+ @Override
+ public InteropActionType getActionType() {
+ return InteropActionType.REQUEST;
+ }
+
+ @Transient
+ @NotNull
+ @Override
+ public String getIdentifier() {
+ return getRequestCode();
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestResponseData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestResponseData.java
new file mode 100644
index 0000000..9ea80ca
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionRequestResponseData.java
@@ -0,0 +1,54 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InteropTransactionRequestResponseData extends InteropResponseData {
+
+ @NotNull
+ @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
+ private final String requestCode;
+
+ private InteropTransactionRequestResponseData(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration, List<ExtensionData> extensionList,
+ @NotNull String requestCode) {
+ super(transactionCode, state, expiration, extensionList);
+ this.requestCode = requestCode;
+ }
+
+ public static InteropTransactionRequestResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+ LocalDateTime expiration, List<ExtensionData> extensionList,
+ @NotNull String requestCode) {
+ return new InteropTransactionRequestResponseData(transactionCode, state, expiration, extensionList, requestCode);
+ }
+
+ public static InteropTransactionRequestResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+ LocalDateTime expiration, @NotNull String requestCode) {
+ return build(transactionCode, state, expiration, null, requestCode);
+ }
+
+ public String getRequestCode() {
+ return requestCode;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionTypeData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionTypeData.java
new file mode 100644
index 0000000..19047b5
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransactionTypeData.java
@@ -0,0 +1,83 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropInitiatorType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionRole;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionScenario;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+public class InteropTransactionTypeData {
+
+ @NotNull
+ private InteropTransactionScenario scenario;
+
+ @Pattern(regexp = "^[A-Z_]{1,32}$")
+ private String subScenario;
+
+ @NotNull
+ private InteropTransactionRole initiator;
+
+ @NotNull
+ private InteropInitiatorType initiatorType;
+
+ protected InteropTransactionTypeData() {
+ }
+
+ 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;
+ }
+
+ protected void setScenario(InteropTransactionScenario scenario) {
+ this.scenario = scenario;
+ }
+
+ protected void setSubScenario(String subScenario) {
+ this.subScenario = subScenario;
+ }
+
+ protected void setInitiator(InteropTransactionRole initiator) {
+ this.initiator = initiator;
+ }
+
+ protected void setInitiatorType(InteropInitiatorType initiatorType) {
+ this.initiatorType = initiatorType;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferCommand.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferCommand.java
new file mode 100644
index 0000000..15a75cb
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferCommand.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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransferActionType;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+
+public class InteropTransferCommand extends InteropTransferRequestData {
+
+ @NotNull
+ private final InteropTransferActionType action;
+
+ public InteropTransferCommand(@NotNull InteropTransferRequestData requestData, @NotNull InteropTransferActionType action) {
+ super(requestData.getTransactionCode(), requestData.getAccountId(), requestData.getAmount(), requestData.getTransactionRole(),
+ requestData.getNote(), requestData.getExpiration(), requestData.getExtensionList(), requestData.getTransferCode(),
+ requestData.getFspFee(), requestData.getFspCommission());
+ this.action = action;
+ }
+
+ public InteropTransferActionType getAction() {
+ return action;
+ }
+
+ @NotNull
+ @Transient
+ @Override
+ public InteropActionType getActionType() {
+ return action == InteropTransferActionType.PREPARE ? InteropActionType.PREPARE : InteropActionType.COMMIT;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferRequestData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferRequestData.java
new file mode 100644
index 0000000..08b1539
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferRequestData.java
@@ -0,0 +1,97 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransactionRole;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.beans.Transient;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InteropTransferRequestData extends InteropRequestData {
+
+ @NotNull
+ @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
+ private String transferCode;
+
+ @Valid
+ private MoneyData fspFee;
+
+ @Valid
+ private MoneyData fspCommission;
+
+ protected InteropTransferRequestData() {
+ super();
+ }
+
+ public InteropTransferRequestData(@NotNull String transactionCode, @NotNull String accountId, @NotNull MoneyData amount,
+ @NotNull InteropTransactionRole transactionRole, String note, LocalDateTime expiration,
+ List<ExtensionData> extensionList, @NotNull String transferCode, MoneyData fspFee, MoneyData fspCommission) {
+ super(transactionCode, null, accountId, amount, transactionRole, null, note, null, expiration, extensionList);
+ this.transferCode = transferCode;
+ this.fspFee = fspFee;
+ this.fspCommission = fspCommission;
+ }
+
+ public InteropTransferRequestData(@NotNull String transactionCode, @NotNull String accountId, @NotNull MoneyData amount,
+ @NotNull InteropTransactionRole transactionRole, @NotNull String transferCode) {
+ this(transactionCode, accountId, amount, transactionRole, 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.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 Currency currency) {
+ super.normalizeAmounts(currency);
+ if (fspFee != null)
+ fspFee.normalizeAmount(currency);
+ }
+
+ @Transient
+ @Override
+ public InteropActionType getActionType() {
+ return null;
+ }
+
+ @Transient
+ @NotNull
+ @Override
+ public String getIdentifier() {
+ return transferCode;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferResponseData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferResponseData.java
new file mode 100644
index 0000000..2e24a3e
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/InteropTransferResponseData.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+
+import javax.validation.constraints.NotNull;
+import java.beans.Transient;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InteropTransferResponseData extends InteropResponseData {
+
+ @NotNull
+ private final String transferCode;
+
+ private String completedTimestamp;
+
+ private InteropTransferResponseData(@NotNull String transactionCode, @NotNull InteropActionState state, LocalDateTime expiration,
+ List<ExtensionData> extensionList, @NotNull String transferCode, LocalDateTime completedTimestamp) {
+ super(transactionCode, state, expiration, extensionList);
+ this.transferCode = transferCode;
+ this.completedTimestamp = format(completedTimestamp);
+ }
+
+ public static InteropTransferResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+ LocalDateTime expiration, List<ExtensionData> extensionList,
+ @NotNull String transferCode, LocalDateTime completedTimestamp) {
+ return new InteropTransferResponseData(transactionCode, state, expiration, extensionList, transferCode, completedTimestamp);
+ }
+
+ public static InteropTransferResponseData build(@NotNull String transactionCode, @NotNull InteropActionState state,
+ LocalDateTime expiration, @NotNull String transferCode, LocalDateTime completedTimestamp) {
+ return build(transactionCode, state, expiration, transferCode, completedTimestamp);
+ }
+
+ public String getTransferCode() {
+ return transferCode;
+ }
+
+ public String getCompletedTimestamp() {
+ return completedTimestamp;
+ }
+
+ @Transient
+ public LocalDateTime getCompletedTimestampDate() {
+ return parse(completedTimestamp);
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/MoneyData.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/MoneyData.java
new file mode 100644
index 0000000..972d838
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/data/MoneyData.java
@@ -0,0 +1,78 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.data;
+
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+import org.apache.fineract.cn.interoperation.api.v1.util.MathUtil;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+public class MoneyData {
+
+ @NotNull
+ @Min(0)
+ @Digits(integer = 15, fraction = 4) // interoperation schema allows integer = 18, AccountEntity amount allows fraction = 5
+ private BigDecimal amount;
+
+ @NotNull
+ @Length(min = 3, max = 3)
+ private String currency;
+
+ protected MoneyData() {
+ }
+
+ MoneyData(BigDecimal amount, String currency) {
+ this.amount = amount;
+ this.currency = currency;
+ }
+
+ public static MoneyData build(BigDecimal amount, String currency) {
+ return amount == null ? null : new MoneyData(amount, currency);
+ }
+
+ public static MoneyData build(BigDecimal amount, Currency currency) {
+ return amount == null ? null : build(amount, currency.getCode());
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public void normalizeAmount(@NotNull Currency currency) {
+ if (!currency.getCode().equals(this.currency))
+ throw new UnsupportedOperationException("Internal error: Invalid currency " + currency.getCode());
+ MathUtil.normalize(amount, currency);
+ }
+
+ protected void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ protected void setCurrency(String currency) {
+ this.currency = currency;
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/validation/InteroperationDataValidator.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/validation/InteroperationDataValidator.java
new file mode 100644
index 0000000..2931ecf
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/domain/validation/InteroperationDataValidator.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cn.interoperation.api.v1.domain.validation;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropIdentifierCommand;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropIdentifierDeleteCommand;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropQuoteRequestData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransactionRequestData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransferCommand;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class InteroperationDataValidator {
+
+ public InteroperationDataValidator() {
+ }
+
+ public InteropIdentifierCommand registerAccountIdentifier(InteropIdentifierCommand requestData) {
+ return requestData;
+ }
+
+ public InteropIdentifierDeleteCommand deleteAccountIdentifier(InteropIdentifierDeleteCommand requestData) {
+ return requestData;
+ }
+
+ public InteropTransactionRequestData validateCreateRequest(InteropTransactionRequestData requestData) {
+ return requestData;
+ }
+
+ public InteropQuoteRequestData validateCreateQuote(InteropQuoteRequestData requestData) {
+ return requestData;
+ }
+
+ public InteropTransferCommand validatePrepareTransfer(InteropTransferCommand requestData) {
+ return validateCommitTransfer(requestData);
+ }
+
+ public InteropTransferCommand validateCommitTransfer(InteropTransferCommand requestData) {
+ return requestData;
+ }
+
+ private void throwExceptionIfValidationWarningsExist(List<String> errors) {
+ if (errors != null && !errors.isEmpty()) {
+ throw new UnsupportedOperationException(String.join(", ", errors)); // TODO
+ }
+ }
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/InteroperationUtil.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/InteroperationUtil.java
new file mode 100644
index 0000000..b24d5a7
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/InteroperationUtil.java
@@ -0,0 +1,23 @@
+/*
+ * 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.cn.interoperation.api.v1.util;
+
+public class InteroperationUtil {
+
+}
diff --git a/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/MathUtil.java b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/MathUtil.java
new file mode 100644
index 0000000..abfecfc
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/interoperation/api/v1/util/MathUtil.java
@@ -0,0 +1,325 @@
+/*
+ * 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.cn.interoperation.api.v1.util;
+
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+public class MathUtil {
+
+ public static final MathContext DEFAULT_MATH_CONTEXT = new MathContext(2, RoundingMode.HALF_EVEN);
+ public static final MathContext CALCULATION_MATH_CONTEXT = new MathContext(5, RoundingMode.HALF_EVEN);
+
+ public static Double nullToZero(Double value) {
+ return nullToDefault(value, 0D);
+ }
+
+ public static Double nullToDefault(Double value, Double def) {
+ return value == null ? def : value;
+ }
+
+ public static Double zeroToNull(Double value) {
+ return isEmpty(value) ? null : value;
+ }
+
+ /** @return parameter value or ZERO if it is negative */
+ public static Double negativeToZero(Double value) {
+ return isGreaterThanZero(value) ? value : 0D;
+ }
+
+ public static boolean isEmpty(Double value) {
+ return value == null || value.equals(0D);
+ }
+
+ public static boolean isGreaterThanZero(Double value) {
+ return value != null && value > 0D;
+ }
+
+ public static boolean isLessThanZero(Double value) {
+ return value != null && value < 0D;
+ }
+
+ public static boolean isZero(Double value) {
+ return value != null && value.equals(0D);
+ }
+
+ public static boolean isEqualTo(Double first, Double second) {
+ return nullToZero(first).equals(nullToZero(second));
+ }
+
+ public static boolean isGreaterThan(Double first, Double second) {
+ return nullToZero(first) > nullToZero(second);
+ }
+
+ public static boolean isLessThan(Double first, Double second) {
+ return nullToZero(first) < nullToZero(second);
+ }
+
+ public static boolean isGreaterThanOrEqualTo(Double first, Double second) {
+ return nullToZero(first) >= nullToZero(second);
+ }
+
+ public static boolean isLessThanOrEqualZero(Double value) {
+ return nullToZero(value) <= 0D;
+ }
+
+ /** @return parameter value or negated value to positive */
+ public static Double abs(Double value) {
+ return value == null ? 0D : 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 Double min(Double first, Double 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 Double min(Double first, Double second, Double third, boolean notNull) {
+ return min(min(first, second, notNull), third, notNull);
+ }
+
+ /** @return sum the two values considering null values */
+ public static Double add(Double first, Double second) {
+ return first == null
+ ? second
+ : second == null ? first : first + second;
+ }
+
+ /** @return sum the values considering null values */
+ public static Double add(Double first, Double second, Double third) {
+ return add(add(first, second), third);
+ }
+
+ /** @return sum the values considering null values */
+ public static Double add(Double first, Double second, Double third, Double fourth) {
+ return add(add(add(first, second), third), fourth);
+ }
+
+ /** @return sum the values considering null values */
+ public static Double add(Double first, Double second, Double third, Double fourth, Double fifth) {
+ return add(add(add(add(first, second), third), fourth), fifth);
+ }
+
+ /** @return first minus second considering null values, maybe negative */
+ public static Double subtract(Double first, Double second) {
+ return first == null
+ ? null
+ : second == null ? first : first - second;
+ }
+
+ /** @return first minus the others considering null values, maybe negative */
+ public static Double subtractToZero(Double first, Double second, Double third) {
+ return subtractToZero(subtract(first, second), third);
+ }
+
+ /** @return first minus the others considering null values, maybe negative */
+ public static Double subtractToZero(Double first, Double second, Double third, Double fourth) {
+ return subtractToZero(subtract(subtract(first, second), third), fourth);
+ }
+
+ /** @return NONE negative first minus second considering null values */
+ public static Double subtractToZero(Double first, Double second) {
+ return negativeToZero(subtract(first, second));
+ }
+
+ /** @return BigDecimal with scale set to the 'digitsAfterDecimal' of the parameter currency */
+ public static Double normalize(Double amount, @NotNull Currency currency) {
+ return amount == null ? null : normalize(BigDecimal.valueOf(amount), currency).doubleValue();
+ }
+
+ /** @return BigDecimal with scale set to the 'digitsAfterDecimal' of the parameter currency */
+ public static Double normalize(Double amount, @NotNull MathContext mc) {
+ return amount == null ? null : normalize(BigDecimal.valueOf(amount), mc).doubleValue();
+ }
+
+ /** @return BigDecimal null safe negate */
+ public static Double negate(Double amount) {
+ return isEmpty(amount) ? amount : amount * -1;
+ }
+
+
+ // ----------------- 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, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @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, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @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, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @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, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @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, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @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 = CALCULATION_MATH_CONTEXT;
+ 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 normalize(BigDecimal amount, @NotNull Currency currency) {
+ return amount == null ? null : amount.setScale(currency.getScale(), CALCULATION_MATH_CONTEXT.getRoundingMode());
+ }
+
+ /** @return BigDecimal with scale set to the 'digitsAfterDecimal' of the parameter currency */
+ public static BigDecimal normalize(BigDecimal amount, @NotNull MathContext mc) {
+ return amount == null ? null : amount.setScale(mc.getPrecision(), mc.getRoundingMode());
+ }
+
+ /** @return BigDecimal null safe negate */
+ public static BigDecimal negate(BigDecimal amount) {
+ return negate(amount, CALCULATION_MATH_CONTEXT);
+ }
+
+ /** @return BigDecimal null safe negate */
+ public static BigDecimal negate(BigDecimal amount, MathContext mc) {
+ return isEmpty(amount) ? amount : amount.negate(mc);
+ }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..949f5bb
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+group 'org.apache.fineract.cn'
+
+task publishApiToMavenLocal {
+ dependsOn gradle.includedBuild('api').task(':publishToMavenLocal')
+}
+
+task publishServiceToMavenLocal {
+ mustRunAfter publishApiToMavenLocal
+ dependsOn gradle.includedBuild('service').task(':publishToMavenLocal')
+}
+
+task publishToMavenLocal {
+ group 'all'
+ dependsOn publishApiToMavenLocal
+ dependsOn publishServiceToMavenLocal
+}
+
+task licenseFormat {
+ group 'all'
+ dependsOn gradle.includedBuild('api').task(':licenseFormat')
+ dependsOn gradle.includedBuild('service').task(':licenseFormat')
+}
+
+task rat {
+ group 'all'
+ dependsOn gradle.includedBuild('api').task(':rat')
+ dependsOn gradle.includedBuild('service').task(':rat')
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..811409c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e2f405e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Apr 14 20:59:05 CEST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..f6cb21f
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,173 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+JAVA_OPTS="-Xms512m -Xmx2048m"
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/service/build.gradle b/service/build.gradle
new file mode 100644
index 0000000..5035f25
--- /dev/null
+++ b/service/build.gradle
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+buildscript {
+ ext {
+ springBootVersion = '1.4.1.RELEASE'
+ }
+
+ repositories {
+ jcenter()
+ }
+
+ dependencies {
+ classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+}
+
+plugins {
+ id "com.github.hierynomus.license" version "0.13.1"
+ id("org.nosphere.apache.rat") version "0.3.1"
+}
+
+apply from: '../shared.gradle'
+
+apply plugin: 'spring-boot'
+
+springBoot {
+ executable = true
+ classifier = 'boot'
+}
+
+dependencies {
+ compile(
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
+ [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
+ [group: 'com.google.code.gson', name: 'gson'],
+ [group: 'org.apache.fineract.cn', name: 'lang', version: versions.frameworklang],
+ [group: 'org.apache.fineract.cn', name: 'async', version: versions.frameworkasync],
+ [group: 'org.apache.fineract.cn', name: 'cassandra', version: versions.frameworkcassandra],
+ [group: 'org.apache.fineract.cn', name: 'mariadb', version: versions.frameworkmariadb],
+ [group: 'org.apache.fineract.cn', name: 'command', version: versions.frameworkcommand],
+ [group: 'org.apache.fineract.cn.interoperation', name: 'api', version: versions.frameworkinter],
+ [group: 'org.apache.fineract.cn.anubis', name: 'library', version: versions.frameworkanubis],
+ [group: 'org.apache.fineract.cn.accounting', name: 'api', version: versions.frameworkledger],
+// [group: 'org.apache.fineract.cn.teller', name: 'service', version: versions.frameworkteller],
+ )
+}
+
+publishToMavenLocal.dependsOn bootRepackage
+
+publishing {
+ publications {
+ service(MavenPublication) {
+ from components.java
+ groupId project.group
+ artifactId project.name
+ version project.version
+ }
+ bootService(MavenPublication) {
+ // "boot" jar
+ artifact("$buildDir/libs/$project.name-$version-boot.jar")
+ groupId project.group
+ artifactId("$project.name-boot")
+ version project.version
+ }
+ }
+}
diff --git a/service/settings.gradle b/service/settings.gradle
new file mode 100644
index 0000000..1a4c2fd
--- /dev/null
+++ b/service/settings.gradle
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+rootProject.name = 'service'
\ No newline at end of file
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropApplication.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropApplication.java
new file mode 100644
index 0000000..aaa559a
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropApplication.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cn.interoperation.service;
+
+import org.springframework.boot.SpringApplication;
+
+public class InteropApplication {
+
+ public InteropApplication() {
+ super();
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(InteropServiceConfiguration.class, args);
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropServiceConfiguration.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropServiceConfiguration.java
new file mode 100644
index 0000000..899d938
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/InteropServiceConfiguration.java
@@ -0,0 +1,83 @@
+/*
+ * 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.cn.interoperation.service;
+
+import com.google.gson.Gson;
+import org.apache.fineract.cn.accounting.api.v1.client.LedgerManager;
+import org.apache.fineract.cn.anubis.config.EnableAnubis;
+import org.apache.fineract.cn.async.config.EnableAsync;
+import org.apache.fineract.cn.cassandra.config.EnableCassandra;
+import org.apache.fineract.cn.command.config.EnableCommandProcessing;
+import org.apache.fineract.cn.deposit.api.v1.client.DepositAccountManager;
+import org.apache.fineract.cn.lang.config.EnableServiceException;
+import org.apache.fineract.cn.lang.config.EnableTenantContext;
+import org.apache.fineract.cn.mariadb.config.EnableMariaDB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+@SuppressWarnings("WeakerAccess")
+@Configuration
+@EnableAutoConfiguration
+@EnableDiscoveryClient
+@EnableAsync
+@EnableTenantContext
+@EnableCassandra
+@EnableMariaDB
+@EnableCommandProcessing
+@EnableAnubis
+@EnableServiceException
+@EnableFeignClients(clients = {
+ LedgerManager.class,
+ DepositAccountManager.class
+})
+@ComponentScan({
+ "org.apache.fineract.cn.interoperation.service.rest",
+ "org.apache.fineract.cn.interoperation.service.internal.service",
+ "org.apache.fineract.cn.interoperation.service.internal.repository",
+ "org.apache.fineract.cn.interoperation.service.internal.command.handler",
+ "org.apache.fineract.cn.interoperation.api.v1.domain.validation",
+ "org.apache.fineract.cn.interoperation.internal.command.handler"
+})
+@EnableJpaRepositories(basePackages = "org.apache.fineract.cn.interoperation.service.internal.repository")
+@EntityScan(basePackages = "org.apache.fineract.cn.interoperation.service.internal.repository")
+public class InteropServiceConfiguration extends WebMvcConfigurerAdapter {
+
+ public InteropServiceConfiguration() {
+ super();
+ }
+
+ @Bean(name = ServiceConstants.LOGGER_NAME)
+ public Logger logger() {
+ return LoggerFactory.getLogger(ServiceConstants.LOGGER_NAME);
+ }
+
+ @Bean(name = ServiceConstants.GSON_NAME)
+ public Gson gson() {
+ return new Gson();
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/ServiceConstants.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/ServiceConstants.java
new file mode 100644
index 0000000..9df33c5
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/ServiceConstants.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.cn.interoperation.service;
+
+public interface ServiceConstants {
+
+ String LOGGER_NAME = "interoperation-logger";
+ String GSON_NAME = "interoperation-gson";
+
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/InitializeServiceCommand.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/InitializeServiceCommand.java
new file mode 100644
index 0000000..f8acab6
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/InitializeServiceCommand.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.cn.interoperation.service.internal.command;
+
+public class InitializeServiceCommand {
+
+ public InitializeServiceCommand() {
+ super();
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/InteropHandler.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/InteropHandler.java
new file mode 100644
index 0000000..f35d281
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/InteropHandler.java
@@ -0,0 +1,112 @@
+/*
+ * 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.cn.interoperation.service.internal.command.handler;
+
+import org.apache.fineract.cn.command.annotation.Aggregate;
+import org.apache.fineract.cn.command.annotation.CommandHandler;
+import org.apache.fineract.cn.command.annotation.CommandLogLevel;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropIdentifierCommand;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropIdentifierData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropIdentifierDeleteCommand;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropQuoteRequestData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropQuoteResponseData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransactionRequestData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransactionRequestResponseData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransferCommand;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.InteropTransferResponseData;
+import org.apache.fineract.cn.interoperation.api.v1.domain.validation.InteroperationDataValidator;
+import org.apache.fineract.cn.interoperation.service.ServiceConstants;
+import org.apache.fineract.cn.interoperation.service.internal.service.InteropService;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.validation.constraints.NotNull;
+
+@SuppressWarnings("unused")
+@Aggregate
+public class InteropHandler {
+
+ private final Logger logger;
+
+ private final InteroperationDataValidator dataValidator;
+
+ private final InteropService interopService;
+
+ @Autowired
+ public InteropHandler(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+ InteroperationDataValidator interoperationDataValidator,
+ InteropService interopService) {
+ this.logger = logger;
+ this.dataValidator = interoperationDataValidator;
+ this.interopService = interopService;
+ }
+
+ @NotNull
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ public InteropIdentifierData registerAccountIdentifier(@NotNull InteropIdentifierCommand command) {
+ command = dataValidator.registerAccountIdentifier(command);
+ return interopService.registerAccountIdentifier(command);
+ }
+
+ @NotNull
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ public InteropIdentifierData deleteAccountIdentifier(@NotNull InteropIdentifierDeleteCommand command) {
+ command = dataValidator.deleteAccountIdentifier(command);
+ return interopService.deleteAccountIdentifier(command);
+ }
+
+ @NotNull
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ public InteropTransactionRequestResponseData createTransactionRequest(@NotNull InteropTransactionRequestData command) {
+ // only when Payee request transaction from Payer, so here role must be always Payer
+ command = dataValidator.validateCreateRequest(command);
+ return interopService.createTransactionRequest(command);
+ }
+
+ @NotNull
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ public InteropQuoteResponseData createQuote(@NotNull InteropQuoteRequestData command) {
+ command = dataValidator.validateCreateQuote(command);
+ return interopService.createQuote(command);
+ }
+
+ @NotNull
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ public InteropTransferResponseData performTransfer(@NotNull InteropTransferCommand command) {
+ switch (command.getAction()) {
+ case PREPARE: {
+ command = dataValidator.validatePrepareTransfer(command);
+ return interopService.prepareTransfer(command);
+ }
+ case CREATE: {
+ command = dataValidator.validateCommitTransfer(command);
+ return interopService.commitTransfer(command);
+ }
+ default:
+ return null;
+ }
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/MigrationAggregate.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/MigrationAggregate.java
new file mode 100644
index 0000000..f60bdbc
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/command/handler/MigrationAggregate.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.cn.interoperation.service.internal.command.handler;
+
+import org.apache.fineract.cn.command.annotation.Aggregate;
+import org.apache.fineract.cn.command.annotation.CommandHandler;
+import org.apache.fineract.cn.command.annotation.EventEmitter;
+import org.apache.fineract.cn.interoperation.api.v1.EventConstants;
+import org.apache.fineract.cn.interoperation.service.ServiceConstants;
+import org.apache.fineract.cn.interoperation.service.internal.command.InitializeServiceCommand;
+import org.apache.fineract.cn.lang.ApplicationName;
+import org.apache.fineract.cn.mariadb.domain.FlywayFactoryBean;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+
+@Aggregate
+public class MigrationAggregate {
+
+ private final Logger logger;
+ private final DataSource dataSource;
+ private final FlywayFactoryBean flywayFactoryBean;
+ private final ApplicationName applicationName;
+
+ @Autowired
+ public MigrationAggregate(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+ final DataSource dataSource,
+ final FlywayFactoryBean flywayFactoryBean,
+ final ApplicationName applicationName) {
+ super();
+ this.logger = logger;
+ this.dataSource = dataSource;
+ this.flywayFactoryBean = flywayFactoryBean;
+ this.applicationName = applicationName;
+ }
+
+ @CommandHandler
+ @EventEmitter(selectorName = EventConstants.OPERATION_HEADER, selectorValue = EventConstants.INITIALIZE)
+ @Transactional
+ public String process(final InitializeServiceCommand initializeCommand) {
+ this.logger.info("Starting migration for interoperation version: {}.", applicationName.getVersionString());
+ this.flywayFactoryBean.create(this.dataSource).migrate();
+ return EventConstants.INITIALIZE;
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionEntity.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionEntity.java
new file mode 100644
index 0000000..e4157ab
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionEntity.java
@@ -0,0 +1,250 @@
+/*
+ * 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.cn.interoperation.service.internal.repository;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "hathor_actions", uniqueConstraints = {
+ @UniqueConstraint(name = "uk_hathor_actions_id", columnNames = {"identifier"}),
+ @UniqueConstraint(name = "uk_hathor_actions_type", columnNames = {"transaction_id", "action_type"}),
+ @UniqueConstraint(name = "uk_hathor_actions_seq", columnNames = {"transaction_id", "seq_no"})
+})
+public class InteropActionEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "identifier", nullable = false, length = 64)
+ private String identifier;
+
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ @JoinColumn(name = "transaction_id", referencedColumnName="id", nullable = false)
+ private InteropTransactionEntity transaction;
+
+ @Column(name = "action_type", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private InteropActionType actionType;
+
+ @Column(name = "seq_no", nullable = false)
+ private int seqNo;
+
+ @Column(name = "state", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private InteropActionState state;
+
+ @Column(name = "amount", nullable = false)
+ private BigDecimal amount;
+
+ @Column(name = "fee")
+ private BigDecimal fee;
+
+ @Column(name = "commission")
+ private BigDecimal commission;
+//
+// @Column(name = "charges", nullable = false, length = 1024)
+// private String charges;
+//
+// @Column(name = "ledgers", nullable = false, length = 1024)
+// private String ledgers;
+
+ @Column(name = "error_code", length = 4)
+ private String errorCode;
+
+ @Column(name = "error_msg", length = 128)
+ private String errorMsg;
+
+ @Column(name = "expiration_date")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime expirationDate;
+
+ @Column(name = "created_by", nullable = false, length = 32)
+ private String createdBy;
+
+ @Column(name = "created_on", nullable = false)
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime createdOn;
+
+
+ protected InteropActionEntity() {
+ }
+
+ public InteropActionEntity(@NotNull String identifier, @NotNull InteropTransactionEntity transaction, @NotNull InteropActionType actionType,
+ int seqNo, @NotNull String createdBy, @NotNull LocalDateTime createdOn) {
+ this.identifier = identifier;
+ this.transaction = transaction;
+ this.actionType = actionType;
+ this.seqNo = seqNo;
+ this.createdBy = createdBy;
+ this.createdOn = createdOn;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ private void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ private void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public InteropTransactionEntity getTransaction() {
+ return transaction;
+ }
+
+ private void setTransaction(InteropTransactionEntity transaction) {
+ this.transaction = transaction;
+ }
+
+ public InteropActionType getActionType() {
+ return actionType;
+ }
+
+ private void setActionType(InteropActionType actionType) {
+ this.actionType = actionType;
+ }
+
+ public int getSeqNo() {
+ return seqNo;
+ }
+
+ private void setSeqNo(int seqNo) {
+ this.seqNo = seqNo;
+ }
+
+ public InteropActionState getState() {
+ return state;
+ }
+
+ public void setState(InteropActionState state) {
+ this.state = state;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public BigDecimal getFee() {
+ return fee;
+ }
+
+ public void setFee(BigDecimal fee) {
+ this.fee = fee;
+ }
+
+ public BigDecimal getCommission() {
+ return commission;
+ }
+
+ public void setCommission(BigDecimal commission) {
+ this.commission = commission;
+ }
+//
+// public String getCharges() {
+// return charges;
+// }
+//
+// public void setCharges(String charges) {
+// this.charges = charges;
+// }
+//
+// public String getLedgers() {
+// return ledgers;
+// }
+//
+// public void setLedgers(String ledgers) {
+// this.ledgers = ledgers;
+// }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public void setErrorCode(String errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+
+ public void setErrorMsg(String errorMsg) {
+ this.errorMsg = errorMsg;
+ }
+
+ public LocalDateTime getExpirationDate() {
+ return expirationDate;
+ }
+
+ public void setExpirationDate(LocalDateTime expirationDate) {
+ this.expirationDate = expirationDate;
+ }
+
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ private void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ public LocalDateTime getCreatedOn() {
+ return createdOn;
+ }
+
+ private void setCreatedOn(LocalDateTime createdOn) {
+ this.createdOn = createdOn;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InteropActionEntity that = (InteropActionEntity) o;
+
+ return identifier.equals(that.identifier);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return identifier.hashCode();
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionRepository.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionRepository.java
new file mode 100644
index 0000000..de5d46d
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropActionRepository.java
@@ -0,0 +1,30 @@
+/*
+ * 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.cn.interoperation.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import javax.validation.constraints.NotNull;
+
+@Repository
+public interface InteropActionRepository extends JpaRepository<InteropActionEntity, Long> {
+
+ InteropActionEntity findByIdentifier(@NotNull String identifier);
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropIdentifierEntity.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropIdentifierEntity.java
new file mode 100644
index 0000000..7af5745
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropIdentifierEntity.java
@@ -0,0 +1,177 @@
+/*
+ * 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.cn.interoperation.service.internal.repository;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropIdentifierType;
+import org.apache.fineract.cn.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "hathor_identifiers", uniqueConstraints = {
+ @UniqueConstraint(name = "uk_hathor_identifiers_value", columnNames = {"type", "a_value", "sub_value_or_type"})
+})
+public class InteropIdentifierEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "customer_account_identifier", nullable = false, length = 32)
+ private String customerAccountIdentifier;
+
+ @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;
+
+ @Column(name = "created_on", nullable = false)
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime createdOn;
+
+ @Column(name = "last_modified_by", length = 32)
+ private String lastModifiedBy;
+
+ @Column(name = "last_modified_on")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime lastModifiedOn;
+
+
+ protected InteropIdentifierEntity() {
+ }
+
+ public InteropIdentifierEntity(@NotNull String customerAccountIdentifier, @NotNull InteropIdentifierType type, @NotNull String value,
+ String subValueOrType, @NotNull String createdBy, @NotNull LocalDateTime createdOn) {
+ this.customerAccountIdentifier = customerAccountIdentifier;
+ this.type = type;
+ this.value = value;
+ this.subValueOrType = subValueOrType;
+ this.createdBy = createdBy;
+ this.createdOn = createdOn;
+ }
+
+ public InteropIdentifierEntity(@NotNull String customerAccountIdentifier, @NotNull InteropIdentifierType type, @NotNull String createdBy,
+ @NotNull LocalDateTime createdOn) {
+ this(customerAccountIdentifier, type, null, null, createdBy, createdOn);
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ private void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getCustomerAccountIdentifier() {
+ return customerAccountIdentifier;
+ }
+
+ private void setCustomerAccountIdentifier(String customerAccountIdentifier) {
+ this.customerAccountIdentifier = customerAccountIdentifier;
+ }
+
+ 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 LocalDateTime getCreatedOn() {
+ return createdOn;
+ }
+
+ private void setCreatedOn(LocalDateTime createdOn) {
+ this.createdOn = createdOn;
+ }
+
+ public String getLastModifiedBy() {
+ return lastModifiedBy;
+ }
+
+ public void setLastModifiedBy(String lastModifiedBy) {
+ this.lastModifiedBy = lastModifiedBy;
+ }
+
+ public LocalDateTime getLastModifiedOn() {
+ return lastModifiedOn;
+ }
+
+ public void setLastModifiedOn(LocalDateTime lastModifiedOn) {
+ this.lastModifiedOn = lastModifiedOn;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InteropIdentifierEntity that = (InteropIdentifierEntity) o;
+
+ if (!customerAccountIdentifier.equals(that.customerAccountIdentifier)) 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/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropIdentifierRepository.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropIdentifierRepository.java
new file mode 100644
index 0000000..8686e3b
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/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.cn.interoperation.service.internal.repository;
+
+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<InteropIdentifierEntity, Long>, JpaSpecificationExecutor<InteropIdentifierEntity> {
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionEntity.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionEntity.java
new file mode 100644
index 0000000..217f589
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionEntity.java
@@ -0,0 +1,258 @@
+/*
+ * 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.cn.interoperation.service.internal.repository;
+
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropState;
+import org.apache.fineract.cn.interoperation.api.v1.domain.TransactionType;
+import org.apache.fineract.cn.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "hathor_transactions", uniqueConstraints = {@UniqueConstraint(name = "uk_hathor_transactions_id", columnNames = {"identifier"})})
+public class InteropTransactionEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "identifier", nullable = false, length = 36)
+ private String identifier;
+
+ @Column(name = "a_name", length = 256)
+ private String name;
+
+ @Column(name = "description", length = 1024)
+ private String description;
+
+ @Column(name = "transaction_type", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private TransactionType transactionType;
+
+ @Column(name = "amount", nullable = false)
+ private BigDecimal amount;
+
+ @Column(name = "state", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private InteropState state;
+
+ @Column(name = "customer_account_identifier", nullable = false, length = 32)
+ private String customerAccountIdentifier;
+
+ @Column(name = "payable_account_identifier", length = 32)
+ private String prepareAccountIdentifier;
+
+ @Column(name = "nostro_account_identifier", nullable = false, length = 32)
+ private String nostroAccountIdentifier;
+
+ @Column(name = "transaction_date")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime transactionDate;
+
+ @Column(name = "expiration_date")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime expirationDate;
+
+ @Column(name = "created_by", nullable = false, length = 32)
+ private String createdBy;
+
+ @Column(name = "created_on", nullable = false)
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime createdOn;
+
+ @Column(name = "last_modified_by", length = 32)
+ private String lastModifiedBy;
+
+ @Column(name = "last_modified_on")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime lastModifiedOn;
+
+ @OneToMany(cascade = CascadeType.ALL, mappedBy = "transaction", orphanRemoval = true, fetch=FetchType.LAZY)
+ @OrderBy("seqNo")
+ private List<InteropActionEntity> actions = new ArrayList<>();
+
+
+ protected InteropTransactionEntity() {
+ }
+
+ public InteropTransactionEntity(@NotNull String identifier, @NotNull String customerAccountIdentifier, @NotNull String createdBy,
+ @NotNull LocalDateTime createdOn) {
+ this.identifier = identifier;
+ this.customerAccountIdentifier = customerAccountIdentifier;
+ this.createdBy = createdBy;
+ this.createdOn = createdOn;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ private void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ private void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public TransactionType getTransactionType() {
+ return transactionType;
+ }
+
+ public void setTransactionType(TransactionType transactionType) {
+ this.transactionType = transactionType;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public InteropState getState() {
+ return state;
+ }
+
+ public void setState(InteropState state) {
+ this.state = state;
+ }
+
+ public String getCustomerAccountIdentifier() {
+ return customerAccountIdentifier;
+ }
+
+ private void setCustomerAccountIdentifier(String customerAccountIdentifier) {
+ this.customerAccountIdentifier = customerAccountIdentifier;
+ }
+
+ public String getPrepareAccountIdentifier() {
+ return prepareAccountIdentifier;
+ }
+
+ public void setPrepareAccountIdentifier(String prepareAccountIdentifier) {
+ this.prepareAccountIdentifier = prepareAccountIdentifier;
+ }
+
+ public String getNostroAccountIdentifier() {
+ return nostroAccountIdentifier;
+ }
+
+ public void setNostroAccountIdentifier(String nostroAccountIdentifier) {
+ this.nostroAccountIdentifier = nostroAccountIdentifier;
+ }
+
+ public LocalDateTime getTransactionDate() {
+ return transactionDate;
+ }
+
+ public void setTransactionDate(LocalDateTime transactionDate) {
+ this.transactionDate = transactionDate;
+ }
+
+ public LocalDateTime getExpirationDate() {
+ return expirationDate;
+ }
+
+ public void setExpirationDate(LocalDateTime expirationDate) {
+ this.expirationDate = expirationDate;
+ }
+
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ private void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ public LocalDateTime getCreatedOn() {
+ return createdOn;
+ }
+
+ private void setCreatedOn(LocalDateTime createdOn) {
+ this.createdOn = createdOn;
+ }
+
+ public String getLastModifiedBy() {
+ return lastModifiedBy;
+ }
+
+ public void setLastModifiedBy(String lastModifiedBy) {
+ this.lastModifiedBy = lastModifiedBy;
+ }
+
+ public LocalDateTime getLastModifiedOn() {
+ return lastModifiedOn;
+ }
+
+ public void setLastModifiedOn(LocalDateTime lastModifiedOn) {
+ this.lastModifiedOn = lastModifiedOn;
+ }
+
+ public List<InteropActionEntity> getActions() {
+ return actions;
+ }
+
+ public void setActions(List<InteropActionEntity> actions) {
+ this.actions = actions;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InteropTransactionEntity that = (InteropTransactionEntity) o;
+
+ return identifier.equals(that.identifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return identifier.hashCode();
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionRepository.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionRepository.java
new file mode 100644
index 0000000..d02023d
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/repository/InteropTransactionRepository.java
@@ -0,0 +1,30 @@
+/*
+ * 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.cn.interoperation.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import javax.validation.constraints.NotNull;
+
+@Repository
+public interface InteropTransactionRepository extends JpaRepository<InteropTransactionEntity, Long> {
+
+ InteropTransactionEntity findOneByIdentifier(@NotNull String identifier);
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/InteropService.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/InteropService.java
new file mode 100644
index 0000000..ae9f119
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/InteropService.java
@@ -0,0 +1,847 @@
+/*
+ * 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.cn.interoperation.service.internal.service;
+
+import org.apache.fineract.cn.accounting.api.v1.domain.Account;
+import org.apache.fineract.cn.accounting.api.v1.domain.AccountType;
+import org.apache.fineract.cn.accounting.api.v1.domain.Creditor;
+import org.apache.fineract.cn.accounting.api.v1.domain.Debtor;
+import org.apache.fineract.cn.accounting.api.v1.domain.JournalEntry;
+import org.apache.fineract.cn.api.util.UserContextHolder;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Charge;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Currency;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.ProductDefinition;
+import org.apache.fineract.cn.deposit.api.v1.instance.domain.ProductInstance;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionState;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropIdentifierType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropState;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropStateMachine;
+import org.apache.fineract.cn.interoperation.api.v1.domain.TransactionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.*;
+import org.apache.fineract.cn.interoperation.api.v1.util.MathUtil;
+import org.apache.fineract.cn.interoperation.service.ServiceConstants;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropActionEntity;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropActionRepository;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropIdentifierEntity;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropIdentifierRepository;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropTransactionEntity;
+import org.apache.fineract.cn.interoperation.service.internal.repository.InteropTransactionRepository;
+import org.apache.fineract.cn.interoperation.service.internal.service.helper.InteropAccountingService;
+import org.apache.fineract.cn.interoperation.service.internal.service.helper.InteropDepositService;
+import org.apache.fineract.cn.lang.DateConverter;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+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.math.MathContext;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+//import static org.apache.fineract.cn.interoperation.api.v1.util.InteroperationUtil.DEFAULT_ROUTING_CODE;
+
+@Service
+public class InteropService {
+
+ public static final String ACCOUNT_NAME_NOSTRO = "Interoperation NOSTRO";
+
+ private final Logger logger;
+
+ private final InteropIdentifierRepository identifierRepository;
+ private final InteropTransactionRepository transactionRepository;
+ private final InteropActionRepository actionRepository;
+
+ private final InteropDepositService depositService;
+ private final InteropAccountingService accountingService;
+
+
+ @Autowired
+ public InteropService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+ InteropIdentifierRepository interopIdentifierRepository,
+ InteropTransactionRepository interopTransactionRepository,
+ InteropActionRepository interopActionRepository,
+ InteropDepositService interopDepositService,
+ InteropAccountingService interopAccountingService) {
+ this.logger = logger;
+ this.identifierRepository = interopIdentifierRepository;
+ this.transactionRepository = interopTransactionRepository;
+ this.actionRepository = interopActionRepository;
+ this.depositService = interopDepositService;
+ this.accountingService = interopAccountingService;
+ }
+
+ @NotNull
+ public InteropIdentifierData getAccountByIdentifier(@NotNull InteropIdentifierType idType, @NotNull String idValue, String subIdOrType) {
+ InteropIdentifierEntity identifier = findIdentifier(idType, idValue, subIdOrType);
+ if (identifier == null)
+ throw new UnsupportedOperationException("Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType)));
+
+ return new InteropIdentifierData(identifier.getCustomerAccountIdentifier());
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropIdentifierData registerAccountIdentifier(@NotNull InteropIdentifierCommand request) {
+ //TODO: error handling
+ String accountId = request.getAccountId();
+ validateAndGetAccount(accountId);
+
+ String createdBy = getLoginUser();
+ LocalDateTime createdOn = getNow();
+
+ InteropIdentifierEntity identifier = new InteropIdentifierEntity(accountId, request.getIdType(), request.getIdValue(),
+ request.getSubIdOrType(), createdBy, createdOn);
+
+ identifierRepository.save(identifier);
+
+ return new InteropIdentifierData(accountId);
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropIdentifierData deleteAccountIdentifier(@NotNull InteropIdentifierDeleteCommand request) {
+ InteropIdentifierType idType = request.getIdType();
+ String idValue = request.getIdValue();
+ String subIdOrType = request.getSubIdOrType();
+
+ InteropIdentifierEntity identifier = findIdentifier(idType, idValue, subIdOrType);
+ if (identifier == null)
+ throw new UnsupportedOperationException("Account not found for identifier " + idType + "/" + idValue + (subIdOrType == null ? "" : ("/" + subIdOrType)));
+
+ String customerAccountIdentifier = identifier.getCustomerAccountIdentifier();
+
+ identifierRepository.delete(identifier);
+
+ return new InteropIdentifierData(customerAccountIdentifier);
+ }
+
+ public InteropTransactionRequestResponseData getTransactionRequest(@NotNull String transactionCode, @NotNull String requestCode) {
+ InteropActionEntity action = validateAndGetAction(transactionCode, calcActionIdentifier(requestCode, InteropActionType.REQUEST),
+ InteropActionType.REQUEST);
+ return InteropTransactionRequestResponseData.build(transactionCode, action.getState(), action.getExpirationDate(), requestCode);
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropTransactionRequestResponseData createTransactionRequest(@NotNull InteropTransactionRequestData request) {
+ // only when Payee request transaction from Payer, so here role must be always Payer
+ //TODO: error handling
+ AccountWrapper accountWrapper = validateAndGetAccount(request);
+ //TODO: transaction expiration separated from action expiration
+ InteropTransactionEntity transaction = validateAndGetTransaction(request, accountWrapper);
+ InteropActionEntity action = addAction(transaction, request);
+
+ transactionRepository.save(transaction);
+
+ return InteropTransactionRequestResponseData.build(request.getTransactionCode(), action.getState(), action.getExpirationDate(),
+ request.getExtensionList(), request.getRequestCode());
+ }
+
+ public InteropQuoteResponseData getQuote(@NotNull String transactionCode, @NotNull String quoteCode) {
+ InteropActionEntity action = validateAndGetAction(transactionCode, calcActionIdentifier(quoteCode, InteropActionType.QUOTE),
+ InteropActionType.QUOTE);
+
+ Currency currency = getCurrency(action);
+
+ return InteropQuoteResponseData.build(transactionCode, action.getState(), action.getExpirationDate(), quoteCode,
+ MoneyData.build(action.getFee(), currency), MoneyData.build(action.getCommission(), currency));
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropQuoteResponseData createQuote(@NotNull InteropQuoteRequestData request) {
+ //TODO: error handling
+ AccountWrapper accountWrapper = validateAndGetAccount(request);
+ //TODO: transaction expiration separated from action expiration
+ InteropTransactionEntity transaction = validateAndGetTransaction(request, accountWrapper);
+
+ TransactionType transactionType = request.getTransactionRole().getTransactionType();
+ String accountId = accountWrapper.account.getIdentifier();
+ List<Charge> charges = depositService.getCharges(accountId, transactionType);
+
+ BigDecimal amount = request.getAmount().getAmount();
+ BigDecimal fee = MathUtil.normalize(calcTotalCharges(charges, amount), MathUtil.DEFAULT_MATH_CONTEXT);
+
+ Double withdrawableBalance = getWithdrawableBalance(accountWrapper.account, accountWrapper.productDefinition);
+ boolean withdraw = request.getTransactionRole().isWithdraw();
+
+ BigDecimal total = MathUtil.nullToZero(withdraw ? MathUtil.add(amount, fee) : fee);
+ if (withdraw && withdrawableBalance < total.doubleValue())
+ throw new UnsupportedOperationException("Account balance is not enough to pay the fee " + accountId);
+
+ // TODO add action and set the status to failed in separated transaction
+ InteropActionEntity action = addAction(transaction, request);
+ action.setFee(fee);
+ // TODO: extend Charge with a property that could be stored in charges
+
+ transactionRepository.save(transaction);
+
+ InteropQuoteResponseData build = InteropQuoteResponseData.build(request.getTransactionCode(), action.getState(),
+ action.getExpirationDate(), request.getExtensionList(), request.getQuoteCode(),
+ MoneyData.build(fee, accountWrapper.productDefinition.getCurrency()), null);
+ return build;
+ }
+
+ public InteropTransferResponseData getTransfer(@NotNull String transactionCode, @NotNull String transferCode) {
+ InteropActionEntity action = validateAndGetAction(transactionCode, calcActionIdentifier(transferCode, InteropActionType.PREPARE),
+ InteropActionType.PREPARE, false);
+ if (action == null)
+ action = validateAndGetAction(transactionCode, calcActionIdentifier(transferCode, InteropActionType.COMMIT),
+ InteropActionType.COMMIT);
+
+ return InteropTransferResponseData.build(transactionCode, action.getState(), action.getExpirationDate(), transferCode, action.getCreatedOn());
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropTransferResponseData prepareTransfer(@NotNull InteropTransferCommand request) {
+ //TODO: error handling
+ //TODO: ABORT
+ AccountWrapper accountWrapper = validateAndGetAccount(request);
+
+ LocalDateTime transactionDate = getNow();
+ //TODO: transaction expiration separated from action expiration
+ InteropTransactionEntity transaction = validateAndGetTransaction(request, accountWrapper, transactionDate, true);
+
+ validateTransfer(request, accountWrapper);
+
+ TransactionType transactionType = request.getTransactionRole().getTransactionType();
+ List<Charge> charges = depositService.getCharges(accountWrapper.account.getIdentifier(), transactionType);
+
+ // TODO add action and set the status to failed in separated transaction
+ InteropActionEntity action = addAction(transaction, request, transactionDate);
+ MoneyData fee = request.getFspFee();
+ action.setFee(fee == null ? null : fee.getAmount());
+ // TODO: extend Charge with a property that could be stored in charges
+
+ prepareTransfer(request, accountWrapper, action, charges, transactionDate);
+
+ transactionRepository.save(transaction);
+
+ return InteropTransferResponseData.build(request.getTransferCode(), action.getState(), action.getExpirationDate(),
+ request.getExtensionList(), request.getTransferCode(), transactionDate);
+ }
+
+ @NotNull
+ @Transactional(propagation = Propagation.MANDATORY)
+ public InteropTransferResponseData commitTransfer(@NotNull InteropTransferCommand request) {
+ //TODO: error handling
+ //TODO: ABORT
+ AccountWrapper accountWrapper = validateAndGetAccount(request);
+
+ LocalDateTime transactionDate = getNow();
+ //TODO: transaction expiration separated from action expiration
+ InteropTransactionEntity transaction = validateAndGetTransaction(request, accountWrapper, transactionDate, true);
+ transaction.setTransactionDate(transactionDate);
+
+ validateTransfer(request, accountWrapper);
+
+ TransactionType transactionType = request.getTransactionRole().getTransactionType();
+ List<Charge> charges = depositService.getCharges(accountWrapper.account.getIdentifier(), transactionType);
+
+ // TODO add action and set the status to failed in separated transaction
+ InteropActionEntity action = addAction(transaction, request, transactionDate);
+ MoneyData fee = request.getFspFee();
+ action.setFee(fee == null ? null : fee.getAmount());
+ // TODO: extend Charge with a property that could be stored in charges
+
+ bookTransfer(request, accountWrapper, action, charges, transactionDate);
+
+ transactionRepository.save(transaction);
+
+ return InteropTransferResponseData.build(request.getTransferCode(), action.getState(), action.getExpirationDate(),
+ request.getExtensionList(), request.getTransferCode(), transactionDate);
+ }
+
+ Double getWithdrawableBalance(Account account, ProductDefinition productDefinition) {
+ // on-hold amount, if any, is subtracted to payable account
+ return MathUtil.subtractToZero(account.getBalance(), productDefinition.getMinimumBalance());
+ }
+
+ private void prepareTransfer(@NotNull InteropTransferCommand request, @NotNull AccountWrapper accountWrapper, @NotNull InteropActionEntity action,
+ List<Charge> charges, LocalDateTime transactionDate) {
+ BigDecimal amount = request.getAmount().getAmount();
+ // TODO: validate amount with quote amount
+ boolean isDebit = request.getTransactionRole().isWithdraw();
+ if (!isDebit)
+ return;
+
+ String prepareAccountId = action.getTransaction().getPrepareAccountIdentifier();
+ Account payableAccount = prepareAccountId == null ? null : validateAndGetAccount(request, prepareAccountId);
+ if (payableAccount == null) {
+ logger.warn("Can not prepare transfer: Payable account was not found for " + accountWrapper.account.getIdentifier());
+ return;
+ }
+
+ final JournalEntry journalEntry = createJournalEntry(action.getIdentifier(), TransactionType.CURRENCY_WITHDRAWAL.getCode(),
+ DateConverter.toIsoString(transactionDate), request.getNote(), getLoginUser());
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ addCreditor(accountWrapper.account.getIdentifier(), amount.doubleValue(), creditors);
+ addDebtor(payableAccount.getIdentifier(), amount.doubleValue(), debtors);
+
+ prepareCharges(request, accountWrapper, action, charges, payableAccount, debtors, creditors);
+
+ if (debtors.isEmpty()) // must be same size as creditors
+ return;
+
+ journalEntry.setDebtors(debtors);
+ journalEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(journalEntry);
+ }
+
+ private void prepareCharges(@NotNull InteropTransferCommand request, @NotNull AccountWrapper accountWrapper, @NotNull InteropActionEntity action,
+ @NotNull List<Charge> charges, Account payableAccount, HashSet<Debtor> debtors, HashSet<Creditor> creditors) {
+ MoneyData fspFee = request.getFspFee(); // TODO compare with calculated and with quote
+
+ BigDecimal amount = request.getAmount().getAmount();
+ Currency currency = accountWrapper.productDefinition.getCurrency();
+
+ BigDecimal total = MathUtil.normalize(calcTotalCharges(charges, amount), currency);
+
+ if (MathUtil.isEmpty(total)) {
+ return;
+ }
+
+ if (creditors == null) {
+ creditors = new HashSet<>(1);
+ }
+ if (debtors == null) {
+ debtors = new HashSet<>(charges.size());
+ }
+ addCreditor(accountWrapper.account.getIdentifier(), total.doubleValue(), creditors);
+ addDebtor(payableAccount.getIdentifier(), total.doubleValue(), debtors);
+ }
+
+ private void bookTransfer(@NotNull InteropTransferCommand request, @NotNull AccountWrapper accountWrapper, @NotNull InteropActionEntity action,
+ List<Charge> charges, LocalDateTime transactionDate) {
+ boolean isDebit = request.getTransactionRole().isWithdraw();
+ String accountId = accountWrapper.account.getIdentifier();
+ String message = request.getNote();
+ double doubleAmount = request.getAmount().getAmount().doubleValue();
+
+ String loginUser = getLoginUser();
+ String transactionTypeCode = (isDebit ? TransactionType.CURRENCY_WITHDRAWAL : TransactionType.CURRENCY_DEPOSIT).getCode();
+ String transactionDateString = DateConverter.toIsoString(transactionDate);
+
+ InteropTransactionEntity transaction = action.getTransaction();
+ Account nostroAccount = validateAndGetAccount(request, transaction.getNostroAccountIdentifier());
+ Account payableAccount = null;
+
+ double preparedAmount = 0d;
+ double accountNostroAmount = doubleAmount;
+
+ if (isDebit) {
+ InteropActionEntity prepareAction = findAction(transaction, InteropActionType.PREPARE);
+ if (prepareAction != null) {
+ JournalEntry prepareJournal = accountingService.findJournalEntry(prepareAction.getIdentifier());
+ if (prepareJournal == null)
+ throw new UnsupportedOperationException("Can not find prepare result for " + action.getActionType() +
+ "/" + request.getIdentifier());
+
+ payableAccount = validateAndGetAccount(request, transaction.getPrepareAccountIdentifier());
+ preparedAmount = prepareJournal.getDebtors().stream().mapToDouble(d -> Double.valueOf(d.getAmount())).sum();
+ if (preparedAmount < doubleAmount)
+ throw new UnsupportedOperationException("Prepared amount " + preparedAmount + " is less than transfer amount " +
+ doubleAmount + " for " + request.getIdentifier());
+
+ // now fails if prepared is not enough
+ accountNostroAmount = preparedAmount >= doubleAmount ? 0d : doubleAmount - preparedAmount;
+
+ double fromPrepareToNostroAmount = doubleAmount - accountNostroAmount;
+ preparedAmount -= fromPrepareToNostroAmount;
+
+ if (fromPrepareToNostroAmount > 0) {
+ final JournalEntry fromPrepareToNostroEntry = createJournalEntry(action.getIdentifier(),
+ transactionTypeCode, transactionDateString, message + " #commit", loginUser);
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ addCreditor(payableAccount.getIdentifier(), fromPrepareToNostroAmount, creditors);
+ addDebtor(nostroAccount.getIdentifier(), fromPrepareToNostroAmount, debtors);
+
+ fromPrepareToNostroEntry.setDebtors(debtors);
+ fromPrepareToNostroEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(fromPrepareToNostroEntry);
+ }
+ }
+ }
+ if (accountNostroAmount > 0) {
+ // can not happen that prepared amount is less than requested transfer amount (identifier and message can be default)
+ final JournalEntry journalEntry = createJournalEntry(action.getIdentifier(), transactionTypeCode,
+ transactionDateString, message/* + (payableAccount == null ? "" : " #difference")*/, loginUser);
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ addCreditor(isDebit ? accountId : nostroAccount.getIdentifier(), accountNostroAmount, creditors);
+ addDebtor(isDebit ? nostroAccount.getIdentifier() : accountId, accountNostroAmount, debtors);
+
+ journalEntry.setDebtors(debtors);
+ journalEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(journalEntry);
+ }
+
+ preparedAmount = bookCharges(request, accountWrapper, action, charges, payableAccount, preparedAmount, transactionDate);
+
+ if (preparedAmount > 0) {
+// throw new UnsupportedOperationException("Prepared amount differs from transfer amount " + doubleAmount + " for " + request.getIdentifier());
+ // transfer back remaining prepared amount TODO: JM maybe fail this case?
+
+ final JournalEntry fromPrepareToAccountEntry = createJournalEntry(action.getIdentifier() + InteropRequestData.IDENTIFIER_SEPARATOR + "diff",
+ transactionTypeCode, transactionDateString, message + " #release difference", loginUser);
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ addCreditor(payableAccount.getIdentifier(), preparedAmount, creditors);
+ addDebtor(accountId, preparedAmount, debtors);
+
+ fromPrepareToAccountEntry.setDebtors(debtors);
+ fromPrepareToAccountEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(fromPrepareToAccountEntry);
+ }
+ }
+
+ private double bookCharges(@NotNull InteropTransferCommand request, @NotNull AccountWrapper accountWrapper, @NotNull InteropActionEntity action,
+ @NotNull List<Charge> charges, Account payableAccount, double preparedAmount, LocalDateTime transactionDate) {
+ boolean isDebit = request.getTransactionRole().isWithdraw();
+ String accountId = accountWrapper.account.getIdentifier();
+ String message = request.getNote();
+ BigDecimal amount = request.getAmount().getAmount();
+ Currency currency = accountWrapper.productDefinition.getCurrency();
+
+ BigDecimal calcFee = MathUtil.normalize(calcTotalCharges(charges, amount), currency);
+ BigDecimal requestFee = request.getFspFee().getAmount();
+ if (!MathUtil.isEqualTo(calcFee, requestFee))
+ throw new UnsupportedOperationException("Quote fee " + requestFee + " differs from transfer fee " + calcFee);
+
+ if (MathUtil.isEmpty(calcFee)) {
+ return preparedAmount;
+ }
+
+ String loginUser = getLoginUser();
+ String transactionTypeCode = (isDebit ? TransactionType.CURRENCY_WITHDRAWAL : TransactionType.CURRENCY_DEPOSIT).getCode();
+ String transactionDateString = DateConverter.toIsoString(transactionDate);
+
+ ArrayList<Charge> unpaidCharges = new ArrayList<>(charges);
+ if (preparedAmount > 0) {
+ InteropActionEntity prepareAction = findAction(action.getTransaction(), InteropActionType.PREPARE);
+ if (prepareAction != null) {
+ final JournalEntry fromPrepareToRevenueEntry = createJournalEntry(action.getIdentifier() + InteropRequestData.IDENTIFIER_SEPARATOR + "fee",
+ transactionTypeCode, transactionDateString, message + " #commit fee", loginUser);
+
+ double payedAmount = 0d;
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ for (Charge charge : charges) {
+ BigDecimal value = calcChargeAmount(amount, charge, currency, true);
+ if (value == null)
+ continue;
+
+ double doubleValue = value.doubleValue();
+ if (doubleValue > preparedAmount) {
+ break;
+ }
+ unpaidCharges.remove(charge);
+ preparedAmount -= doubleValue;
+ payedAmount += doubleValue;
+
+ addDebtor(charge.getIncomeAccountIdentifier(), doubleValue, debtors);
+ }
+ if (!unpaidCharges.isEmpty())
+ throw new UnsupportedOperationException("Prepared amount " + preparedAmount + " is less than transfer fee amount for " +
+ request.getIdentifier());
+
+ if (payedAmount > 0) {
+ addCreditor(payableAccount.getIdentifier(), payedAmount, creditors);
+
+ fromPrepareToRevenueEntry.setDebtors(debtors);
+ fromPrepareToRevenueEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(fromPrepareToRevenueEntry);
+ }
+ }
+ }
+ if (!unpaidCharges.isEmpty()) {
+ // can not happen that prepared amount is more or less than requested transfer amount (identifier and message can be default)
+ final JournalEntry journalEntry = createJournalEntry(action.getIdentifier() + InteropRequestData.IDENTIFIER_SEPARATOR + "fee",
+ transactionTypeCode, transactionDateString, message + " #fee", loginUser);
+
+ HashSet<Debtor> debtors = new HashSet<>(1);
+ HashSet<Creditor> creditors = new HashSet<>(1);
+
+ double payedAmount = 0d;
+ for (Charge charge : charges) {
+ BigDecimal value = calcChargeAmount(amount, charge, currency, true);
+ if (value == null)
+ continue;
+
+ double doubleValue = value.doubleValue();
+ payedAmount += doubleValue;
+
+ addDebtor(charge.getIncomeAccountIdentifier(), doubleValue, debtors);
+ }
+
+ if (payedAmount > 0) {
+ addCreditor(accountId, payedAmount, creditors);
+
+ journalEntry.setDebtors(debtors);
+ journalEntry.setCreditors(creditors);
+ accountingService.createJournalEntry(journalEntry);
+ }
+ }
+
+ return preparedAmount;
+ }
+
+ // Util
+
+ private JournalEntry createJournalEntry(String actionIdentifier, String transactionType, String transactionDate, String message, String loginUser) {
+ final JournalEntry fromPrepareToNostroEntry = new JournalEntry();
+ fromPrepareToNostroEntry.setTransactionIdentifier(actionIdentifier);
+ fromPrepareToNostroEntry.setTransactionType(transactionType);
+ fromPrepareToNostroEntry.setTransactionDate(transactionDate);
+ fromPrepareToNostroEntry.setMessage(message);
+ fromPrepareToNostroEntry.setClerk(loginUser);
+ return fromPrepareToNostroEntry;
+ }
+
+ private void addCreditor(String accountNumber, double amount, HashSet<Creditor> creditors) {
+ Creditor creditor = new Creditor();
+ creditor.setAccountNumber(accountNumber);
+ creditor.setAmount(Double.toString(amount));
+ creditors.add(creditor);
+ }
+
+ private void addDebtor(String accountNumber, double amount, HashSet<Debtor> debtors) {
+ Debtor debtor = new Debtor();
+ debtor.setAccountNumber(accountNumber);
+ debtor.setAmount(Double.toString(amount));
+ debtors.add(debtor);
+ }
+
+ private BigDecimal calcChargeAmount(BigDecimal amount, Charge charge) {
+ return calcChargeAmount(amount, charge, null, false);
+ }
+
+ private BigDecimal calcChargeAmount(@NotNull BigDecimal amount, @NotNull Charge charge, Currency currency, boolean norm) {
+ Double value = charge.getAmount();
+ if (value == null)
+ return null;
+
+ BigDecimal portion = BigDecimal.valueOf(100.00d);
+ MathContext mc = MathUtil.CALCULATION_MATH_CONTEXT;
+ BigDecimal feeAmount = BigDecimal.valueOf(MathUtil.nullToZero(charge.getAmount()));
+ BigDecimal result = charge.getProportional()
+ ? amount.multiply(feeAmount.divide(portion, mc), mc)
+ : feeAmount;
+ return norm ? MathUtil.normalize(result, currency) : result;
+ }
+
+ @NotNull
+ private BigDecimal calcTotalCharges(@NotNull List<Charge> charges, BigDecimal amount) {
+ return charges.stream().map(charge -> calcChargeAmount(amount, charge)).reduce(MathUtil::add).orElse(BigDecimal.ZERO);
+ }
+
+ private Account validateAndGetAccount(@NotNull String accountId) {
+ //TODO: error handling
+ Account account = accountingService.findAccount(accountId);
+ validateAccount(account);
+
+ return account;
+ }
+
+ private AccountWrapper validateAndGetAccount(@NotNull InteropRequestData request) {
+ //TODO: error handling
+ String accountId = request.getAccountId();
+ Account account = accountingService.findAccount(accountId);
+ validateAccount(request, account);
+
+ ProductInstance product = depositService.findProductInstance(accountId);
+ ProductDefinition productDefinition = depositService.findProductDefinition(product.getProductIdentifier());
+
+ Currency currency = productDefinition.getCurrency();
+ if (!currency.getCode().equals(request.getAmount().getCurrency()))
+ throw new UnsupportedOperationException();
+
+ request.normalizeAmounts(currency);
+
+ Double withdrawableBalance = getWithdrawableBalance(account, productDefinition);
+ if (request.getTransactionRole().isWithdraw() && withdrawableBalance < request.getAmount().getAmount().doubleValue())
+ throw new UnsupportedOperationException();
+
+ return new AccountWrapper(account, product, productDefinition, withdrawableBalance);
+ }
+
+ private Account validateAndGetPayableAccount(@NotNull InteropRequestData request, @NotNull AccountWrapper wrapper) {
+ String referenceId = wrapper.account.getReferenceAccount();
+ if (referenceId == null)
+ return null;
+
+ return validateAndGetAccount(request, referenceId);
+ }
+
+ @NotNull
+ private Account validateAndGetNostroAccount(@NotNull InteropRequestData request) {
+ //TODO: error handling
+ List<Account> nostros = fetchAccounts(false, ACCOUNT_NAME_NOSTRO, AccountType.ASSET.name(), false, null, null, null, null);
+ int size = nostros.size();
+ if (size != 1)
+ throw new UnsupportedOperationException("NOSTRO Account " + (size == 0 ? "not found" : "is ambigous"));
+
+ Account nostro = nostros.get(0);
+ validateAccount(request, nostro);
+ return nostro;
+ }
+
+ @NotNull
+ private Account validateAndGetAccount(@NotNull InteropRequestData request, @NotNull String accountId) {
+ //TODO: error handling
+ Account account = accountingService.findAccount(accountId);
+
+ validateAccount(request, account);
+ return account;
+ }
+
+ private void validateAccount(Account account) {
+ if (account == null)
+ throw new UnsupportedOperationException("Account not found");
+ if (!account.getState().equals(Account.State.OPEN.name()))
+ throw new UnsupportedOperationException("Account is in state " + account.getState());
+ }
+
+ private void validateAccount(@NotNull InteropRequestData request, Account account) {
+ validateAccount(account);
+
+ String accountId = account.getIdentifier();
+
+ if (account.getHolders() != null) { // customer account
+ ProductInstance product = depositService.findProductInstance(accountId);
+ ProductDefinition productDefinition = depositService.findProductDefinition(product.getProductIdentifier());
+ if (!Boolean.TRUE.equals(productDefinition.getActive()))
+ throw new UnsupportedOperationException("NOSTRO Product Definition is inactive");
+
+ Currency currency = productDefinition.getCurrency();
+ if (!currency.getCode().equals(request.getAmount().getCurrency()))
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private BigDecimal validateTransfer(@NotNull InteropTransferRequestData request, @NotNull AccountWrapper accountWrapper) {
+ BigDecimal amount = request.getAmount().getAmount();
+
+ boolean isDebit = request.getTransactionRole().isWithdraw();
+ Currency currency = accountWrapper.productDefinition.getCurrency();
+
+ BigDecimal total = isDebit ? amount : MathUtil.negate(amount);
+ MoneyData fspFee = request.getFspFee();
+ if (fspFee != null) {
+ if (!currency.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 (!currency.getCode().equals(fspCommission.getCurrency()))
+ throw new UnsupportedOperationException();
+ //TODO: compare with calculated quote commission
+ total = MathUtil.subtractToZero(total, fspCommission.getAmount());
+ }
+ if (isDebit && accountWrapper.withdrawableBalance < request.getAmount().getAmount().doubleValue())
+ throw new UnsupportedOperationException();
+ return total;
+ }
+
+ public List<Account> fetchAccounts(boolean includeClosed, String term, String type, boolean includeCustomerAccounts,
+ Integer pageIndex, Integer size, String sortColumn, String sortDirection) {
+ return accountingService.fetchAccounts(includeClosed, term, type, includeCustomerAccounts, pageIndex, size, sortColumn, sortDirection);
+ }
+
+ public InteropIdentifierEntity 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<InteropIdentifierEntity> idTypeEqual(@NotNull InteropIdentifierType idType) {
+ return (Root<InteropIdentifierEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("type"), idType));
+ }
+
+ public static Specification<InteropIdentifierEntity> idValueEqual(@NotNull String idValue) {
+ return (Root<InteropIdentifierEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> cb.and(cb.equal(root.get("value"), idValue));
+ }
+
+ public static Specification<InteropIdentifierEntity> subIdOrTypeEqual(String subIdOrType) {
+ return (Root<InteropIdentifierEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
+ Path<Object> path = root.get("subValueOrType");
+ return cb.and(subIdOrType == null ? cb.isNull(path) : cb.equal(path, subIdOrType));
+ };
+ }
+
+ @NotNull
+ private String calcActionIdentifier(@NotNull String actionCode, @NotNull InteropActionType actionType) {
+ return actionType == InteropActionType.PREPARE || actionType == InteropActionType.COMMIT ? actionType + InteropRequestData.IDENTIFIER_SEPARATOR + actionCode : actionCode;
+ }
+
+ private InteropActionEntity validateAndGetAction(@NotNull String transactionCode, @NotNull String actionIdentifier, @NotNull InteropActionType actionType) {
+ return validateAndGetAction(transactionCode, actionIdentifier, actionType, true);
+ }
+
+ private InteropActionEntity validateAndGetAction(@NotNull String transactionCode, @NotNull String actionIdentifier, @NotNull InteropActionType actionType, boolean one) {
+ //TODO: error handling
+ InteropActionEntity action = actionRepository.findByIdentifier(actionIdentifier);
+ if (action == null) {
+ if (one)
+ throw new UnsupportedOperationException("Interperation action " + actionType + '/' + actionIdentifier + " was not found for this transaction " + transactionCode);
+ return null;
+ }
+ if (!action.getTransaction().getIdentifier().equals(transactionCode))
+ throw new UnsupportedOperationException("Interperation action " + actionType + '/' + actionIdentifier + " does not exist in this transaction " + transactionCode);
+ if (action.getActionType() != actionType)
+ throw new UnsupportedOperationException("Interperation action " + actionIdentifier + " is not this type " + actionType);
+
+ return action;
+ }
+
+ @NotNull
+ private InteropTransactionEntity validateAndGetTransaction(@NotNull InteropRequestData request, @NotNull AccountWrapper accountWrapper) {
+ return validateAndGetTransaction(request, accountWrapper, true);
+ }
+
+ @NotNull
+ private InteropTransactionEntity validateAndGetTransaction(@NotNull InteropRequestData request, @NotNull AccountWrapper accountWrapper, boolean create) {
+ return validateAndGetTransaction(request, accountWrapper, getNow(), create);
+ }
+
+ @NotNull
+ private InteropTransactionEntity validateAndGetTransaction(@NotNull InteropRequestData request, @NotNull AccountWrapper accountWrapper,
+ @NotNull LocalDateTime createdOn, boolean create) {
+ //TODO: error handling
+ String transactionCode = request.getTransactionCode();
+ InteropTransactionEntity transaction = transactionRepository.findOneByIdentifier(request.getTransactionCode());
+ InteropState state = InteropStateMachine.handleTransition(transaction == null ? null : transaction.getState(), request.getActionType());
+ LocalDateTime now = getNow();
+ if (transaction == null) {
+ if (!create)
+ throw new UnsupportedOperationException("Interperation transaction " + request.getTransactionCode() + " does not exist ");
+ transaction = new InteropTransactionEntity(transactionCode, accountWrapper.account.getIdentifier(), getLoginUser(), now);
+ transaction.setState(state);
+ transaction.setAmount(request.getAmount().getAmount());
+ transaction.setName(request.getNote());
+ transaction.setTransactionType(request.getTransactionRole().getTransactionType());
+ Account payableAccount = validateAndGetPayableAccount(request, accountWrapper);
+ transaction.setPrepareAccountIdentifier(payableAccount == null ? null : payableAccount.getIdentifier());
+ transaction.setNostroAccountIdentifier(validateAndGetNostroAccount(request).getIdentifier());
+ transaction.setExpirationDate(request.getExpiration());
+ }
+ LocalDateTime expirationDate = transaction.getExpirationDate();
+ if (expirationDate != null && expirationDate.isBefore(now))
+ throw new UnsupportedOperationException("Interperation transaction expired on " + expirationDate);
+ return transaction;
+ }
+
+ private Currency getCurrency(InteropActionEntity action) {
+ ProductInstance product = depositService.findProductInstance(action.getTransaction().getCustomerAccountIdentifier());
+ ProductDefinition productDefinition = depositService.findProductDefinition(product.getProductIdentifier());
+ return productDefinition.getCurrency();
+ }
+
+ private InteropActionEntity addAction(@NotNull InteropTransactionEntity transaction, @NotNull InteropRequestData request) {
+ return addAction(transaction, request, getNow());
+ }
+
+ private InteropActionEntity addAction(@NotNull InteropTransactionEntity transaction, @NotNull InteropRequestData request,
+ @NotNull LocalDateTime createdOn) {
+ InteropActionEntity lastAction = getLastAction(transaction);
+
+ InteropActionType actionType = request.getActionType();
+ String actionIdentifier = calcActionIdentifier(request.getIdentifier(), request.getActionType());
+ InteropActionEntity action = new InteropActionEntity(actionIdentifier, transaction, request.getActionType(),
+ (lastAction == null ? 0 : lastAction.getSeqNo() + 1), getLoginUser(),
+ (lastAction == null ? transaction.getCreatedOn() : createdOn));
+ action.setState(InteropActionState.ACCEPTED);
+ action.setAmount(request.getAmount().getAmount());
+ action.setExpirationDate(request.getExpiration());
+ transaction.getActions().add(action);
+
+ InteropState currentState = transaction.getState();
+ if (transaction.getId() != null || InteropStateMachine.isValidAction(currentState, actionType)) // newly created was already set
+ transaction.setState(InteropStateMachine.handleTransition(currentState, actionType));
+ return action;
+ }
+
+ private InteropActionEntity getLastAction(InteropTransactionEntity transaction) {
+ List<InteropActionEntity> actions = transaction.getActions();
+ int size = actions.size();
+ return size == 0 ? null : actions.get(size - 1);
+ }
+
+ private InteropActionEntity findAction(InteropTransactionEntity transaction, InteropActionType actionType) {
+ List<InteropActionEntity> actions = transaction.getActions();
+ for (InteropActionEntity action : actions) {
+ if (action.getActionType() == actionType)
+ return action;
+ }
+ return null;
+ }
+
+ private LocalDateTime getNow() {
+ return LocalDateTime.now(Clock.systemUTC());
+ }
+
+ private String getLoginUser() {
+ return UserContextHolder.checkedGetUser();
+ }
+
+ public static class AccountWrapper {
+ @NotNull
+ private final Account account;
+ @NotNull
+ private final ProductInstance product;
+ @NotNull
+ private final ProductDefinition productDefinition;
+ @NotNull
+ private final Double withdrawableBalance;
+
+ public AccountWrapper(Account account, ProductInstance product, ProductDefinition productDefinition, Double withdrawableBalance) {
+ this.account = account;
+ this.product = product;
+ this.productDefinition = productDefinition;
+ this.withdrawableBalance = withdrawableBalance;
+ }
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropAccountingService.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropAccountingService.java
new file mode 100644
index 0000000..9d98fbc
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropAccountingService.java
@@ -0,0 +1,120 @@
+/*
+ * 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.cn.interoperation.service.internal.service.helper;
+
+import com.google.common.collect.Lists;
+import org.apache.fineract.cn.accounting.api.v1.client.AccountNotFoundException;
+import org.apache.fineract.cn.accounting.api.v1.client.LedgerManager;
+import org.apache.fineract.cn.accounting.api.v1.client.LedgerNotFoundException;
+import org.apache.fineract.cn.accounting.api.v1.domain.Account;
+import org.apache.fineract.cn.accounting.api.v1.domain.AccountEntry;
+import org.apache.fineract.cn.accounting.api.v1.domain.AccountPage;
+import org.apache.fineract.cn.accounting.api.v1.domain.JournalEntry;
+import org.apache.fineract.cn.accounting.api.v1.domain.Ledger;
+import org.apache.fineract.cn.interoperation.service.ServiceConstants;
+import org.apache.fineract.cn.lang.ServiceException;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+public class InteropAccountingService {
+
+ private Logger logger;
+ private LedgerManager ledgerManager;
+
+ @Autowired
+ public InteropAccountingService(@Qualifier(ServiceConstants.LOGGER_NAME) Logger logger,
+ LedgerManager ledgerManager) {
+ super();
+ this.logger = logger;
+ this.ledgerManager = ledgerManager;
+ }
+
+ public void createAccount(String equityLedger,
+ String productName,
+ String customer,
+ String accountNumber,
+ String alternativeAccountNumber,
+ Double balance) {
+ try {
+ Ledger ledger = ledgerManager.findLedger(equityLedger);
+ Account account = new Account();
+ account.setIdentifier(accountNumber);
+ account.setType(ledger.getType());
+ account.setLedger(equityLedger);
+ account.setName(productName);
+ account.setHolders(new HashSet<>(Lists.newArrayList(customer)));
+ account.setBalance(balance != null ? balance : 0.00D);
+ account.setAlternativeAccountNumber(alternativeAccountNumber);
+
+ ledgerManager.createAccount(account);
+ } catch (LedgerNotFoundException lnfex) {
+ throw ServiceException.notFound("Ledger {0} not found.", equityLedger);
+ }
+ }
+
+ public List<Account> fetchAccounts(boolean includeClosed, String term, String type, boolean includeCustomerAccounts,
+ Integer pageIndex, Integer size, String sortColumn, String sortDirection) {
+ return ledgerManager.fetchAccounts(includeClosed, term, type, includeCustomerAccounts, pageIndex, size, sortColumn, sortDirection).getAccounts();
+ }
+
+ public Account findAccount(final String accountNumber) {
+ try {
+ return this.ledgerManager.findAccount(accountNumber);
+ } catch (final AccountNotFoundException anfex) {
+ final AccountPage accountPage = this.ledgerManager.fetchAccounts(true, accountNumber, null, true,
+ 0, 10, null, null);
+
+ return accountPage.getAccounts()
+ .stream()
+ .filter(account -> account.getAlternativeAccountNumber().equals(accountNumber))
+ .findFirst()
+ .orElseThrow(() -> ServiceException.notFound("Account {0} not found.", accountNumber));
+ }
+ }
+
+ public void modifyAccount(Account account) {
+ ledgerManager.modifyAccount(account.getIdentifier(), account);
+ }
+
+ public List<AccountEntry> fetchAccountEntries(String identifier, String dateRange, String direction) {
+ return ledgerManager
+ .fetchAccountEntries(identifier, dateRange, null, 0, 1, "transactionDate", direction)
+ .getAccountEntries();
+ }
+
+ public List<JournalEntry> fetchJournalEntries(final String dateRange, final String accountNumber, final BigDecimal amount) {
+ return ledgerManager.fetchJournalEntries(dateRange, accountNumber, amount);
+ }
+
+ public JournalEntry findJournalEntry(@NotNull String transactionIdentifier) {
+ return ledgerManager.findJournalEntry(transactionIdentifier);
+ }
+
+ public void createJournalEntry(@NotNull JournalEntry journalEntry) {
+ ledgerManager.createJournalEntry(journalEntry);
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropDepositService.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropDepositService.java
new file mode 100644
index 0000000..80bfd52
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/internal/service/helper/InteropDepositService.java
@@ -0,0 +1,128 @@
+/*
+ * 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.cn.interoperation.service.internal.service.helper;
+
+import org.apache.fineract.cn.deposit.api.v1.client.DepositAccountManager;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Action;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.Charge;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.DividendDistribution;
+import org.apache.fineract.cn.deposit.api.v1.definition.domain.ProductDefinition;
+import org.apache.fineract.cn.deposit.api.v1.instance.domain.AvailableTransactionType;
+import org.apache.fineract.cn.deposit.api.v1.instance.domain.ProductInstance;
+import org.apache.fineract.cn.interoperation.api.v1.domain.TransactionType;
+import org.apache.fineract.cn.interoperation.service.ServiceConstants;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class InteropDepositService {
+
+ private Logger logger;
+ private DepositAccountManager depositAccountManager;
+
+ @Autowired
+ public InteropDepositService(@Qualifier(ServiceConstants.LOGGER_NAME) Logger logger,
+ DepositAccountManager depositAccountManager) {
+ super();
+ this.logger = logger;
+ this.depositAccountManager = depositAccountManager;
+ }
+
+ public List<Charge> getWithdrawCharges(String accountIdentifier) {
+ return getCharges(accountIdentifier, TransactionType.CURRENCY_WITHDRAWAL);
+ }
+
+ public List<Charge> getDepositCharges(String accountIdentifier) {
+ return getCharges(accountIdentifier, TransactionType.CURRENCY_DEPOSIT);
+ }
+
+ public List<Charge> getCharges(String accountIdentifier, TransactionType transactionType) {
+ List<Action> actions = depositAccountManager.fetchActions();
+
+ List<String> actionIds = actions
+ .stream()
+ .filter(action -> action.getTransactionType().equals(transactionType.getCode()))
+ .map(Action::getIdentifier)
+ .collect(Collectors.toList());
+
+ ProductInstance productInstance = depositAccountManager.findProductInstance(accountIdentifier);
+ ProductDefinition productDefinition = depositAccountManager.findProductDefinition(productInstance.getProductIdentifier());
+
+ return productDefinition.getCharges()
+ .stream()
+ .filter(charge -> actionIds.contains(charge.getActionIdentifier()))
+ .collect(Collectors.toList());
+ }
+
+ public void createAction(@NotNull Action action) {
+ depositAccountManager.create(action);
+ }
+
+ public List<Action> fetchActions() {
+ return depositAccountManager.fetchActions();
+ }
+
+ public void createProductDefinition(@NotNull ProductDefinition productDefinition) {
+ depositAccountManager.create(productDefinition);
+ }
+
+ public List<ProductDefinition> fetchProductDefinitions() {
+ return depositAccountManager.fetchProductDefinitions();
+ }
+
+ public ProductDefinition findProductDefinition(@NotNull String identifier) {
+ return depositAccountManager.findProductDefinition(identifier);
+ }
+
+ public List<ProductInstance> findProductInstances(@NotNull String identifier) {
+ return depositAccountManager.findProductInstances(identifier);
+ }
+
+ public void createProductInstance(@NotNull ProductInstance productInstance) {
+ depositAccountManager.create(productInstance);
+ }
+
+ public List<ProductInstance> fetchProductInstances(@NotNull String customer) {
+ return depositAccountManager.fetchProductInstances(customer);
+ }
+
+ public Set<AvailableTransactionType> fetchPossibleTransactionTypes(String customer) {
+ return depositAccountManager.fetchPossibleTransactionTypes(customer);
+ }
+
+ public ProductInstance findProductInstance(@NotNull String accountIdentifier) {
+ return depositAccountManager.findProductInstance(accountIdentifier);
+ }
+
+ public void dividendDistribution(@NotNull String identifier, @NotNull DividendDistribution distribution) {
+ depositAccountManager.dividendDistribution(identifier, distribution);
+ }
+
+ public List<DividendDistribution> fetchDividendDistributions(@NotNull String identifier) {
+ return depositAccountManager.fetchDividendDistributions(identifier);
+ }
+}
+
diff --git a/service/src/main/java/org/apache/fineract/cn/interoperation/service/rest/InteropRestController.java b/service/src/main/java/org/apache/fineract/cn/interoperation/service/rest/InteropRestController.java
new file mode 100644
index 0000000..b00fbd5
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/interoperation/service/rest/InteropRestController.java
@@ -0,0 +1,277 @@
+/**
+ * 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.cn.interoperation.service.rest;
+
+import org.apache.fineract.cn.anubis.annotation.AcceptedTokenType;
+import org.apache.fineract.cn.anubis.annotation.Permittable;
+import org.apache.fineract.cn.command.domain.CommandCallback;
+import org.apache.fineract.cn.command.gateway.CommandGateway;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropIdentifierType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.InteropTransferActionType;
+import org.apache.fineract.cn.interoperation.api.v1.domain.data.*;
+import org.apache.fineract.cn.interoperation.service.internal.command.InitializeServiceCommand;
+import org.apache.fineract.cn.interoperation.service.internal.service.InteropService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.UriInfo;
+
+import static org.apache.fineract.cn.interoperation.api.v1.PermittableGroupIds.INTEROPERATION_SINGLE;
+
+@SuppressWarnings("unused")
+@RestController
+@RequestMapping("/") //interoperation/v1
+public class InteropRestController {
+
+ private CommandGateway commandGateway;
+
+ private InteropService interopService;
+
+ @Autowired
+ public InteropRestController(CommandGateway commandGateway,
+ InteropService interopService) {
+ this.commandGateway = commandGateway;
+ this.interopService = interopService;
+ }
+
+ @Permittable(value = AcceptedTokenType.SYSTEM)
+ @RequestMapping(
+ value = "/initialize",
+ method = RequestMethod.POST,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public
+ @ResponseBody
+ ResponseEntity<Void> initialize() {
+ this.commandGateway.process(new InitializeServiceCommand());
+ return ResponseEntity.accepted().build();
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/health",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public ResponseEntity<Void> health(@Context UriInfo uriInfo) {
+ return ResponseEntity.ok().build();
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public ResponseEntity<InteropIdentifierData> getAccountByIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue) {
+ InteropIdentifierData account = interopService.getAccountByIdentifier(idType, idValue, null);
+
+ return ResponseEntity.ok(account);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}/{subIdOrType}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public ResponseEntity<InteropIdentifierData> getAccountByIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue,
+ @PathVariable(value = "subIdOrType") String subIdOrType) {
+ InteropIdentifierData account = interopService.getAccountByIdentifier(idType, idValue, subIdOrType);
+
+ return ResponseEntity.ok(account);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropIdentifierData> registerAccountIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue,
+ @RequestBody @Valid InteropIdentifierData requestData)
+ throws Throwable {
+ CommandCallback<InteropIdentifierData> result = commandGateway.process(new InteropIdentifierCommand(requestData,
+ idType, idValue, null), InteropIdentifierData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}/{subIdOrType}",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropIdentifierData> registerAccountIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue,
+ @PathVariable(value = "subIdOrType", required = false) String subIdOrType,
+ @RequestBody @Valid InteropIdentifierData requestData)
+ throws Throwable {
+ CommandCallback<InteropIdentifierData> result = commandGateway.process(new InteropIdentifierCommand(requestData,
+ idType, idValue, subIdOrType), InteropIdentifierData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}",
+ method = RequestMethod.DELETE,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropIdentifierData> deleteAccountIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue)
+ throws Throwable {
+ CommandCallback<InteropIdentifierData> result = commandGateway.process(new InteropIdentifierDeleteCommand(idType, idValue, null), InteropIdentifierData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/parties/{idType}/{idValue}/{subIdOrType}",
+ method = RequestMethod.DELETE,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropIdentifierData> deleteAccountIdentifier(@PathVariable("idType") InteropIdentifierType idType,
+ @PathVariable("idValue") String idValue,
+ @PathVariable(value = "subIdOrType", required = false) String subIdOrType)
+ throws Throwable {
+ CommandCallback<InteropIdentifierData> result = commandGateway.process(new InteropIdentifierDeleteCommand(idType, idValue, subIdOrType), InteropIdentifierData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "transactions/{transactionCode}/requests/{requestCode}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropTransactionRequestResponseData> getTransactionRequest(@PathVariable("transactionCode") String transactionCode,
+ @PathVariable("requestCode") String requestCode) {
+ InteropTransactionRequestResponseData result = interopService.getTransactionRequest(transactionCode, requestCode);
+
+ return ResponseEntity.ok(result);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/requests",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropTransactionRequestResponseData> createTransactionRequest(@RequestBody @Valid InteropTransactionRequestData requestData)
+ throws Throwable {
+ CommandCallback<InteropTransactionRequestResponseData> result = commandGateway.process(requestData, InteropTransactionRequestResponseData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "transactions/{transactionCode}/quotes/{quoteCode}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropQuoteResponseData> getQuote(@PathVariable("transactionCode") String transactionCode,
+ @PathVariable("quoteCode") String quoteCode) {
+ InteropQuoteResponseData result = interopService.getQuote(transactionCode, quoteCode);
+
+ return ResponseEntity.ok(result);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/quotes",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropQuoteResponseData> createQuote(@RequestBody @Valid InteropQuoteRequestData requestData) throws Throwable {
+ CommandCallback<InteropQuoteResponseData> result = commandGateway.process(requestData, InteropQuoteResponseData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "transactions/{transactionCode}/transfers/{transferCode}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropTransferResponseData> getTransfer(@PathVariable("transactionCode") String transactionCode,
+ @PathVariable("transferCode") String transferCode) {
+ InteropTransferResponseData result = interopService.getTransfer(transactionCode, transferCode);
+
+ return ResponseEntity.ok(result);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = INTEROPERATION_SINGLE)
+ @RequestMapping(
+ value = "/transfers",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ResponseBody
+ public ResponseEntity<InteropTransferResponseData> performTransfer(@RequestParam("action") String action,
+ @RequestBody @Valid InteropTransferRequestData requestData)
+ throws Throwable {
+ CommandCallback<InteropTransferResponseData> result = commandGateway.process(new InteropTransferCommand(requestData, InteropTransferActionType.valueOf(action)),
+ InteropTransferResponseData.class);
+
+ return ResponseEntity.ok(result.get());
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
new file mode 100644
index 0000000..68e0269
--- /dev/null
+++ b/service/src/main/resources/application.yml
@@ -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.
+#
+
+server:
+ port: 2034
+ contextPath: /interoperation/v1/*
+
+async:
+ corePoolSize: 32
+ maxPoolSize: 16384
+ queueCapacity: 0
+ threadName: async-processor-
+
+flyway:
+ enabled: false
diff --git a/service/src/main/resources/bootstrap.yml b/service/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..ce260b0
--- /dev/null
+++ b/service/src/main/resources/bootstrap.yml
@@ -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.
+#
+
+spring:
+ application:
+ name: interoperation-v1
+ cloud:
+ config:
+ uri: http://localhost:8761/config
+ username: config
+ password: config1234
diff --git a/service/src/main/resources/db/migrations/mariadb/V1__interop_init.sql b/service/src/main/resources/db/migrations/mariadb/V1__interop_init.sql
new file mode 100644
index 0000000..6a942df
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V1__interop_init.sql
@@ -0,0 +1,91 @@
+--
+-- 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.
+--
+
+CREATE TABLE hathor_identifiers (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ customer_account_identifier VARCHAR(32) NOT NULL,
+ type VARCHAR(32) NOT NULL,
+ a_value VARCHAR(128) NOT NULL,
+ sub_value_or_type VARCHAR(128) NULL,
+ created_by VARCHAR(32) NOT NULL,
+ created_on TIMESTAMP(3) NOT NULL,
+ last_modified_by VARCHAR(32) NULL,
+ last_modified_on TIMESTAMP(3) NULL,
+ CONSTRAINT pk_hathor_identifiers PRIMARY KEY (id),
+ CONSTRAINT uk_hathor_identifiers_value UNIQUE (type, a_value, sub_value_or_type)
+);
+
+
+CREATE TABLE hathor_transactions (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ identifier VARCHAR(36) NOT NULL,
+ a_name VARCHAR(256) NULL,
+ description VARCHAR(1024) NULL,
+ transaction_type VARCHAR(32) NOT NULL,
+ amount NUMERIC(22, 4) NOT NULL,
+ state VARCHAR(32) NOT NULL,
+ customer_account_identifier VARCHAR(32) NOT NULL,
+ payable_account_identifier VARCHAR(32) NULL,
+ nostro_account_identifier VARCHAR(32) NOT NULL,
+ transaction_date TIMESTAMP(3) NULL,
+ expiration_date TIMESTAMP(3) NULL,
+ created_by VARCHAR(32) NOT NULL,
+ created_on TIMESTAMP(3) NOT NULL,
+ last_modified_by VARCHAR(32) NULL,
+ last_modified_on TIMESTAMP(3) NULL,
+ CONSTRAINT pk_hathor_transactions PRIMARY KEY (id),
+ CONSTRAINT uk_hathor_transactions_id UNIQUE (identifier)
+);
+
+
+CREATE TABLE hathor_actions (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ identifier VARCHAR(64) NOT NULL,
+ transaction_id BIGINT NOT NULL,
+ action_type VARCHAR(32) NOT NULL,
+ seq_no INT NOT NULL,
+ state VARCHAR(32) NOT NULL,
+ amount NUMERIC(22, 4) NOT NULL,
+ fee NUMERIC(22, 4) NULL,
+ commission NUMERIC(22, 4) NULL,
+ error_code CHAR(4) NULL,
+ error_msg VARCHAR(128) NULL,
+ expiration_date TIMESTAMP(3) NULL,
+ created_by VARCHAR(32) NOT NULL,
+ created_on TIMESTAMP(3) NOT NULL,
+ CONSTRAINT pk_hathor_identifiers PRIMARY KEY (id),
+ CONSTRAINT uk_hathor_actions_id UNIQUE (identifier),
+ CONSTRAINT uk_hathor_actions_type UNIQUE (transaction_id, action_type),
+ CONSTRAINT uk_hathor_actions_seq UNIQUE (transaction_id, seq_no),
+ CONSTRAINT fk_hathor_actions_trans FOREIGN KEY (transaction_id) REFERENCES hathor_transactions (id)
+);
+
+-- data initialization
+
+INSERT INTO thoth_tx_types (identifier, a_name, description)
+VALUES ('FCWD', 'Interoperation withdrawal', 'Demo FCWD Interoperation withdrawal');
+INSERT INTO thoth_tx_types (identifier, a_name, description)
+VALUES ('FCDP', 'Interoperation deposit', 'Demo FCDP Interoperation deposit');
+
+
+INSERT INTO shed_actions (identifier, a_name, description, transaction_type)
+VALUES ('InteropWithdraw', 'Interoperation withdrawal', 'Demo FCWD Interoperation withdrawal', 'FCWD');
+INSERT INTO shed_actions (identifier, a_name, description, transaction_type)
+VALUES ('InteropDeposit', 'Interoperation deposit', 'Demo FCDP Interoperation deposit', 'FCDP');
+
diff --git a/service/src/main/resources/db/migrations/sample_data/interop_sample_data.sql b/service/src/main/resources/db/migrations/sample_data/interop_sample_data.sql
new file mode 100644
index 0000000..503c179
--- /dev/null
+++ b/service/src/main/resources/db/migrations/sample_data/interop_sample_data.sql
@@ -0,0 +1,61 @@
+--
+-- 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.
+--
+
+-- sample data initialization
+
+INSERT INTO horus_offices (identifier, a_name, description, created_by, created_on)
+VALUES ('Headquarter', 'Headquarter', 'Demo Headquarter', 'operator', CURDATE());
+
+SET @office_id = -1;
+SELECT id INTO @office_id FROM horus_offices WHERE identifier = 'Headquarter';
+
+INSERT INTO horus_addresses (office_id, street, city, country_code, country)
+VALUES (@office_id, 'Headquarter street', 'Headquarter city', 'IC', 'Inclusia');
+
+INSERT INTO maat_addresses (street, city, country_code, country)
+VALUES ('Customer street', 'Customer city', 'IC', 'Inclusia');
+
+
+SET @ledger_1300_id = -1;
+SELECT id INTO @ledger_1300_id FROM thoth_ledgers WHERE identifier = '1300';
+SET @ledger_3700_id = -1;
+SELECT id INTO @ledger_3700_id FROM thoth_ledgers WHERE identifier = '3700';
+SET @ledger_7300_id = -1;
+SELECT id INTO @ledger_7300_id FROM thoth_ledgers WHERE identifier = '7300';
+SET @ledger_7900_id = -1;
+SELECT id INTO @ledger_7900_id FROM thoth_ledgers WHERE identifier = '7900';
+SET @ledger_8100_id = -1;
+SELECT id INTO @ledger_8100_id FROM thoth_ledgers WHERE identifier = '8100';
+SET @ledger_9100_id = -1;
+SELECT id INTO @ledger_9100_id FROM thoth_ledgers WHERE identifier = '9100';
+
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('97b1470ffd664cb799c3a9', 'Interoperation Payable Liability', 'LIABILITY', 1000000, @ledger_8100_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('353388f8c343445eac1bd6', 'Interoperation NOSTRO', 'ASSET', 1000000, @ledger_7900_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('0faaf81e05674f19859f18', 'Interoperation Product Cash', 'ASSET', 0, @ledger_7300_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('72df4f6613a911e9ab14d6', 'Interoperation Product Expenses', 'EXPENSE', 0, @ledger_3700_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('65f7de0e13a811e9ab14d6', 'Interoperation Product Accrue Liability', 'LIABILITY', 0, @ledger_8100_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('020323b613aa11e9ab14d6', 'Interoperation Product Equity', 'EQUITY', 0, @ledger_9100_id, 'OPEN', 'operator', CURDATE());
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, ledger_id, a_state, created_by, created_on)
+VALUES ('87d607ba13aa11e9ab14d6', 'Interoperation Product Fees Revenue', 'REVENUE', 1000000, @ledger_1300_id, 'OPEN', 'operator', CURDATE());
\ No newline at end of file
diff --git a/service/src/main/resources/db/migrations/sample_data/tn01_interop_sample_data.sql b/service/src/main/resources/db/migrations/sample_data/tn01_interop_sample_data.sql
new file mode 100644
index 0000000..aa277fb
--- /dev/null
+++ b/service/src/main/resources/db/migrations/sample_data/tn01_interop_sample_data.sql
@@ -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.
+--
+
+USE tn01;
+
+INSERT INTO maat_customers (identifier, a_type, given_name, surname, date_of_birth, is_member, current_state, address_id, created_by, created_on)
+ VALUES ('InteropCustomer', 'PERSON', 'Interoperation', 'Customer', '2000-01-01', 1, 'ACTIVE',
+ (SELECT id FROM maat_addresses WHERE street = 'Customer street'),
+ 'operator', CURDATE());
+
+
+SET @ledger_7900_id = -1;
+SELECT id INTO @ledger_7900_id FROM thoth_ledgers WHERE identifier = '7900';
+
+SET @account_payable_id = -1;
+SELECT id INTO @account_payable_id FROM thoth_accounts WHERE identifier = '97b1470ffd664cb799c3a9';
+
+SET @account_customer_id = '31d77b06141c11e9ab14d6';
+
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, reference_account_id, ledger_id, a_state, created_by, created_on)
+VALUES (@account_customer_id, 'Interoperation Customer Account', 'ASSET', 1000000, @account_payable_id, @ledger_7900_id, 'OPEN', 'operator', CURDATE());
+
+
+INSERT INTO hathor_identifiers (customer_account_identifier, type, a_value, sub_value_or_type, created_by, created_on)
+VALUES (@account_customer_id, 'IBAN', 'IC11in01tn0131d77b06141c11e9ab14d6', null, 'operator', CURDATE());
+INSERT INTO hathor_identifiers (customer_account_identifier, type, a_value, sub_value_or_type, created_by, created_on)
+VALUES (@account_customer_id, 'MSISDN', '27710101999', null, 'operator', CURDATE());
+
+
+SET @action_withdraw_id = -1;
+SELECT id INTO @action_withdraw_id FROM shed_actions WHERE identifier = 'InteropWithdraw';
+
+INSERT INTO shed_product_definitions (identifier, a_name, description, a_type, minimum_balance, equity_ledger_identifier,
+ cash_account_identifier, expense_account_identifier, accrue_account_identifier, is_flexible,
+ is_active, created_by, created_on)
+ VALUES ('InteropCustomerProduct', 'Interoperation Customer Product', 'Demo Interoperation Product', 'SAVINGS', 100, '9100', '0faaf81e05674f19859f18',
+ '72df4f6613a911e9ab14d6', '65f7de0e13a811e9ab14d6', 0, 1, 'operator', CURDATE());
+
+SET @prod_def_id = -1;
+SELECT id INTO @prod_def_id FROM shed_product_definitions WHERE identifier = 'InteropCustomerProduct';
+
+INSERT INTO shed_currencies (product_definition_id, a_code, a_name, sign, scale)
+ VALUES (@prod_def_id, 'TZS', 'Inclusia Rupee', 'TSh', 2);
+
+INSERT INTO shed_terms (product_definition_id, period, time_unit, interest_payable)
+ VALUES (@prod_def_id, 1, 'YEAR', 'ANNUALLY');
+
+INSERT INTO shed_charges (product_definition_id, action_id, a_name, description, income_account_identifier, proportional, amount)
+VALUES (@prod_def_id, @action_withdraw_id, 'Interoperation Withdraw Fee', 'Interoperation Revenue Fees', '87d607ba13aa11e9ab14d6', 1, 1.00);
+
+INSERT INTO shed_product_instances (product_definition_id, customer_identifier, account_identifier, a_state, opened_on, created_by, created_on)
+VALUES (@prod_def_id, 'InteropCustomer', @account_customer_id, 'ACTIVE', CURDATE(), 'operator', CURDATE());
\ No newline at end of file
diff --git a/service/src/main/resources/db/migrations/sample_data/tn02_interop_sample_data.sql b/service/src/main/resources/db/migrations/sample_data/tn02_interop_sample_data.sql
new file mode 100644
index 0000000..efa3669
--- /dev/null
+++ b/service/src/main/resources/db/migrations/sample_data/tn02_interop_sample_data.sql
@@ -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.
+--
+
+USE tn02;
+
+INSERT INTO maat_customers (identifier, a_type, given_name, surname, date_of_birth, is_member, current_state, address_id, created_by, created_on)
+ VALUES ('InteropMerchant', 'PERSON', 'Interoperation', 'Merchant', '2000-01-01', 1, 'ACTIVE',
+ (SELECT id FROM maat_addresses WHERE street = 'Customer street'),
+ 'operator', CURDATE());
+
+
+SET @ledger_7900_id = -1;
+SELECT id INTO @ledger_7900_id FROM thoth_ledgers WHERE identifier = '7900';
+
+SET @account_payable_id = -1;
+SELECT id INTO @account_payable_id FROM thoth_accounts WHERE identifier = '97b1470ffd664cb799c3a9';
+
+SET @account_customer_id = '83c4ed1c074b484e85cc79';
+
+INSERT INTO thoth_accounts (identifier, a_name, a_type, balance, reference_account_id, ledger_id, a_state, created_by, created_on)
+VALUES (@account_customer_id, 'Interoperation Merchant Account', 'ASSET', 1000000, @account_payable_id, @ledger_7900_id, 'OPEN', 'operator', CURDATE());
+
+
+INSERT INTO hathor_identifiers (customer_account_identifier, type, a_value, sub_value_or_type, created_by, created_on)
+VALUES (@account_customer_id, 'IBAN', 'IC11in01tn0283c4ed1c074b484e85cc79', null, 'operator', CURDATE());
+INSERT INTO hathor_identifiers (customer_account_identifier, type, a_value, sub_value_or_type, created_by, created_on)
+VALUES (@account_customer_id, 'MSISDN', '27710102999', null, 'operator', CURDATE());
+
+
+SET @action_withdraw_id = -1;
+SELECT id INTO @action_withdraw_id FROM shed_actions WHERE identifier = 'InteropWithdraw';
+
+INSERT INTO shed_product_definitions (identifier, a_name, description, a_type, minimum_balance, equity_ledger_identifier,
+ cash_account_identifier, expense_account_identifier, accrue_account_identifier, is_flexible,
+ is_active, created_by, created_on)
+ VALUES ('InteropMerchantProduct', 'Interoperation Merchant Product', 'Demo Interoperation Product', 'SAVINGS', 100, '9100', '0faaf81e05674f19859f18',
+ '72df4f6613a911e9ab14d6', '65f7de0e13a811e9ab14d6', 0, 1, 'operator', CURDATE());
+
+SET @prod_def_id = -1;
+SELECT id INTO @prod_def_id FROM shed_product_definitions WHERE identifier = 'InteropMerchantProduct';
+
+INSERT INTO shed_currencies (product_definition_id, a_code, a_name, sign, scale)
+ VALUES (@prod_def_id, 'TZS', 'Inclusia Rupee', 'TSh', 2);
+
+INSERT INTO shed_terms (product_definition_id, period, time_unit, interest_payable)
+ VALUES (@prod_def_id, 1, 'YEAR', 'ANNUALLY');
+
+INSERT INTO shed_charges (product_definition_id, action_id, a_name, description, income_account_identifier, proportional, amount)
+VALUES (@prod_def_id, @action_withdraw_id, 'Interoperation Withdraw Fee', 'Interoperation Revenue Fees', '87d607ba13aa11e9ab14d6', 1, 1.00);
+
+INSERT INTO shed_product_instances (product_definition_id, customer_identifier, account_identifier, a_state, opened_on, created_by, created_on)
+VALUES (@prod_def_id, 'InteropMerchant', @account_customer_id, 'ACTIVE', CURDATE(), 'operator', CURDATE());
\ No newline at end of file
diff --git a/service/src/main/resources/logback.xml b/service/src/main/resources/logback.xml
new file mode 100644
index 0000000..a5b8aa3
--- /dev/null
+++ b/service/src/main/resources/logback.xml
@@ -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.
+
+-->
+<configuration>
+ <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <file>logs/interoperation.log</file>
+ <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+ <fileNamePattern>logs/archive/interoperation.%d{yyyy-MM-dd}.log</fileNamePattern>
+ <maxHistory>7</maxHistory>
+ <totalSizeCap>2GB</totalSizeCap>
+ </rollingPolicy>
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="com" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="org" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="io" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="net" level="INFO">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <root level="DEBUG">
+ <appender-ref ref="FILE"/>
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..f36b432
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+rootProject.name = 'interoperation'
+
+includeBuild 'api'
+includeBuild 'service'
+
diff --git a/shared.gradle b/shared.gradle
new file mode 100644
index 0000000..9d10733
--- /dev/null
+++ b/shared.gradle
@@ -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.
+ */
+
+group 'org.apache.fineract.cn.interoperation'
+version '0.1.0-BUILD-SNAPSHOT'
+
+ext.versions = [
+ frameworkanubis : '0.1.0-BUILD-SNAPSHOT',
+ frameworkapi : '0.1.0-BUILD-SNAPSHOT',
+ frameworklang : '0.1.0-BUILD-SNAPSHOT',
+ frameworkmariadb : '0.1.0-BUILD-SNAPSHOT',
+ frameworkcassandra : '0.1.0-BUILD-SNAPSHOT',
+ frameworkcommand : '0.1.0-BUILD-SNAPSHOT',
+ frameworktest : '0.1.0-BUILD-SNAPSHOT',
+ frameworkasync : '0.1.0-BUILD-SNAPSHOT',
+ frameworkcustomer : '0.1.0-BUILD-SNAPSHOT',
+ frameworkcompany : '0.1.0-BUILD-SNAPSHOT',
+ frameworkledger : '0.1.0-BUILD-SNAPSHOT',
+ frameworkdeposit : '0.1.0-BUILD-SNAPSHOT',
+ frameworkteller : '0.1.0-BUILD-SNAPSHOT',
+ frameworkinter : '0.1.0-BUILD-SNAPSHOT',
+ apachecsvreader : '1.4',
+ validator : '5.3.0.Final'
+]
+
+apply plugin: 'java'
+apply plugin: 'idea'
+apply plugin: 'maven-publish'
+apply plugin: 'io.spring.dependency-management'
+
+tasks.withType(JavaCompile) {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+repositories {
+ jcenter()
+ mavenLocal()
+}
+
+dependencyManagement {
+ imports {
+ mavenBom 'io.spring.platform:platform-bom:Athens-RELEASE'
+ mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.SR1'
+ }
+}
+
+// override certain dependency provided by Spring platform using newer releases
+ext['cassandra.version'] = '3.6'
+ext['cassandra-driver.version'] = '3.1.2'
+ext['activemq.version'] = '5.13.2'
+ext['spring-data-releasetrain.version'] = 'Gosling-SR2A'
+
+dependencies {
+ compile(
+ [group: 'com.google.code.findbugs', name: 'jsr305']
+ )
+
+ testCompile(
+ [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+ )
+}
+
+jar {
+ from sourceSets.main.allSource
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+}
+
+license {
+ header rootProject.file('../HEADER')
+ strictCheck false
+ mapping {
+ java = 'SLASHSTAR_STYLE'
+ xml = 'XML_STYLE'
+ yml = 'SCRIPT_STYLE'
+ yaml = 'SCRIPT_STYLE'
+ }
+}
+
+rat {
+ // List of exclude directives, defaults to ['**/.gradle/**']
+ excludes = [
+ "**/.idea/**",
+ "**/.gradle/**",
+ "**/gradle/**",
+ "**/build/**",
+ "gradlew",
+ "gradlew.bat",
+ "README.md"
+ ]
+}