├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── industrieit
│ │ └── ledger
│ │ └── clientledger
│ │ └── core
│ │ └── memory
│ │ ├── Application.java
│ │ ├── config
│ │ └── LedgerConfig.java
│ │ ├── consumer
│ │ ├── Consumer.java
│ │ ├── Processor.java
│ │ ├── Producer.java
│ │ └── impl
│ │ │ ├── BackUpProcessor.java
│ │ │ ├── ConsumerImpl.java
│ │ │ ├── CreateAccountProcessor.java
│ │ │ ├── P2PProcessor.java
│ │ │ ├── ProducerImpl.java
│ │ │ └── TopUpProcessor.java
│ │ ├── controller
│ │ ├── AccountController.java
│ │ ├── JournalEntryController.java
│ │ └── TransactionController.java
│ │ ├── entity
│ │ ├── Account.java
│ │ ├── JournalEntry.java
│ │ ├── TransactionEvent.java
│ │ └── TransactionResult.java
│ │ ├── exception
│ │ └── InvalidBusinessRuleException.java
│ │ ├── ledger
│ │ ├── committer
│ │ │ ├── Committer.java
│ │ │ └── impl
│ │ │ │ └── BaseCommitter.java
│ │ └── validator
│ │ │ ├── Validator.java
│ │ │ └── impl
│ │ │ ├── P2PValidator.java
│ │ │ └── TopUpValidator.java
│ │ ├── model
│ │ ├── ledger
│ │ │ ├── Itemizable.java
│ │ │ ├── Type.java
│ │ │ └── impl
│ │ │ │ ├── P2PItemizable.java
│ │ │ │ └── TopUpItemizable.java
│ │ └── request
│ │ │ ├── EventRequest.java
│ │ │ └── impl
│ │ │ ├── CreateAccountRequest.java
│ │ │ ├── P2PRequest.java
│ │ │ ├── SnapshotRequest.java
│ │ │ └── TopUpRequest.java
│ │ ├── repository
│ │ ├── AccountRepository.java
│ │ ├── JournalEntryRepository.java
│ │ ├── TransactionEventRepository.java
│ │ ├── TransactionResultRepository.java
│ │ └── impl
│ │ │ ├── AccountRepositoryImpl.java
│ │ │ ├── JournalEntryRepositoryImpl.java
│ │ │ ├── TransactionEventRepositoryImpl.java
│ │ │ └── TransactionResultRepositoryImpl.java
│ │ └── service
│ │ ├── AccountService.java
│ │ ├── JournalService.java
│ │ ├── SnapshotService.java
│ │ ├── TransactionService.java
│ │ └── impl
│ │ ├── AccountServiceImpl.java
│ │ ├── P2PServiceImpl.java
│ │ ├── SnapshotServiceImpl.java
│ │ ├── TopUpServiceImpl.java
│ │ └── TransactionServiceImpl.java
└── resources
│ └── application.properties
└── test
└── java
└── com
└── industrieit
└── ledger
└── clientledger
└── core
└── memory
├── ApplicationTests.java
├── config
└── LedgerConfigTest.java
├── consumer
└── impl
│ ├── ConsumerImplTest.java
│ ├── CreateAccountProcessorTest.java
│ ├── P2PProcessorTest.java
│ ├── ProducerImplTest.java
│ └── TopUpProcessorTest.java
├── controller
├── AccountControllerTest.java
├── JournalEntryControllerTest.java
└── TransactionControllerTest.java
├── entity
├── AccountTest.java
├── JournalEntryTest.java
├── TransactionEventTest.java
└── TransactionResultTest.java
├── ledger
├── committer
│ └── impl
│ │ └── BaseCommitterTest.java
└── validator
│ └── impl
│ ├── P2PValidatorTest.java
│ └── TopUpValidatorTest.java
├── model
└── ledger
│ └── impl
│ ├── P2PItemizableTest.java
│ └── TopUpItemizableTest.java
└── service
└── impl
├── AccountServiceImplTest.java
├── P2PServiceImplTest.java
├── SnapshotServiceImplTest.java
└── TopUpServiceImplTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 | .sts4-cache
12 |
13 | ### IntelliJ IDEA ###
14 | .idea
15 | *.iws
16 | *.iml
17 | *.ipr
18 |
19 | ### NetBeans ###
20 | /nbproject/private/
21 | /nbbuild/
22 | /dist/
23 | /nbdist/
24 | /.nb-gradle/
25 | /build/
26 |
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | import java.io.File;
21 | import java.io.FileInputStream;
22 | import java.io.FileOutputStream;
23 | import java.io.IOException;
24 | import java.net.URL;
25 | import java.nio.channels.Channels;
26 | import java.nio.channels.ReadableByteChannel;
27 | import java.util.Properties;
28 |
29 | public class MavenWrapperDownloader {
30 |
31 | /**
32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
33 | */
34 | private static final String DEFAULT_DOWNLOAD_URL =
35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
36 |
37 | /**
38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
39 | * use instead of the default one.
40 | */
41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
42 | ".mvn/wrapper/maven-wrapper.properties";
43 |
44 | /**
45 | * Path where the maven-wrapper.jar will be saved to.
46 | */
47 | private static final String MAVEN_WRAPPER_JAR_PATH =
48 | ".mvn/wrapper/maven-wrapper.jar";
49 |
50 | /**
51 | * Name of the property which should be used to override the default download url for the wrapper.
52 | */
53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
54 |
55 | public static void main(String args[]) {
56 | System.out.println("- Downloader started");
57 | File baseDirectory = new File(args[0]);
58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
59 |
60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
61 | // wrapperUrl parameter.
62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
63 | String url = DEFAULT_DOWNLOAD_URL;
64 | if (mavenWrapperPropertyFile.exists()) {
65 | FileInputStream mavenWrapperPropertyFileInputStream = null;
66 | try {
67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
68 | Properties mavenWrapperProperties = new Properties();
69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
71 | } catch (IOException e) {
72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
73 | } finally {
74 | try {
75 | if (mavenWrapperPropertyFileInputStream != null) {
76 | mavenWrapperPropertyFileInputStream.close();
77 | }
78 | } catch (IOException e) {
79 | // Ignore ...
80 | }
81 | }
82 | }
83 | System.out.println("- Downloading from: : " + url);
84 |
85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
86 | if (!outputFile.getParentFile().exists()) {
87 | if (!outputFile.getParentFile().mkdirs()) {
88 | System.out.println(
89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
90 | }
91 | }
92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
93 | try {
94 | downloadFileFromURL(url, outputFile);
95 | System.out.println("Done");
96 | System.exit(0);
97 | } catch (Throwable e) {
98 | System.out.println("- Error downloading");
99 | e.printStackTrace();
100 | System.exit(1);
101 | }
102 | }
103 |
104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
105 | URL website = new URL(urlString);
106 | ReadableByteChannel rbc;
107 | rbc = Channels.newChannel(website.openStream());
108 | FileOutputStream fos = new FileOutputStream(destination);
109 | fos.getChannel().transferFrom(rbc, 0, long.MAX_VALUE);
110 | fos.close();
111 | rbc.close();
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewkkchan/client-ledger-core-memory/3a2199a46b958de6b6bc9a37879cf3f714a2d983/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # client-ledger-core-memory: In memory consumer to demonstrate competing consumer in event sourcing at fastest speed to handle burst
2 | ## What it does?
3 | This code base is the bonus part of the 3 Part demo of event sourcing. This code implements the consumption on in-process-memory instead of Redis and Database to illustrate competing consumers and eventual consistency can be pushed to an extreme.
4 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven2 Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | # TODO classpath?
118 | fi
119 |
120 | if [ -z "$JAVA_HOME" ]; then
121 | javaExecutable="`which javac`"
122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
123 | # readlink(1) is not available as standard on Solaris 10.
124 | readLink=`which readlink`
125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
126 | if $darwin ; then
127 | javaHome="`dirname \"$javaExecutable\"`"
128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
129 | else
130 | javaExecutable="`readlink -f \"$javaExecutable\"`"
131 | fi
132 | javaHome="`dirname \"$javaExecutable\"`"
133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
134 | JAVA_HOME="$javaHome"
135 | export JAVA_HOME
136 | fi
137 | fi
138 | fi
139 |
140 | if [ -z "$JAVACMD" ] ; then
141 | if [ -n "$JAVA_HOME" ] ; then
142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
143 | # IBM's JDK on AIX uses strange locations for the executables
144 | JAVACMD="$JAVA_HOME/jre/sh/java"
145 | else
146 | JAVACMD="$JAVA_HOME/bin/java"
147 | fi
148 | else
149 | JAVACMD="`which java`"
150 | fi
151 | fi
152 |
153 | if [ ! -x "$JAVACMD" ] ; then
154 | echo "Error: JAVA_HOME is not defined correctly." >&2
155 | echo " We cannot execute $JAVACMD" >&2
156 | exit 1
157 | fi
158 |
159 | if [ -z "$JAVA_HOME" ] ; then
160 | echo "Warning: JAVA_HOME environment variable is not set."
161 | fi
162 |
163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
164 |
165 | # traverses directory structure from process work directory to filesystem root
166 | # first directory with .mvn subdirectory is considered project base directory
167 | find_maven_basedir() {
168 |
169 | if [ -z "$1" ]
170 | then
171 | echo "Path not specified to find_maven_basedir"
172 | return 1
173 | fi
174 |
175 | basedir="$1"
176 | wdir="$1"
177 | while [ "$wdir" != '/' ] ; do
178 | if [ -d "$wdir"/.mvn ] ; then
179 | basedir=$wdir
180 | break
181 | fi
182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
183 | if [ -d "${wdir}" ]; then
184 | wdir=`cd "$wdir/.."; pwd`
185 | fi
186 | # end of workaround
187 | done
188 | echo "${basedir}"
189 | }
190 |
191 | # concatenates all lines of a file
192 | concat_lines() {
193 | if [ -f "$1" ]; then
194 | echo "$(tr -s '\n' ' ' < "$1")"
195 | fi
196 | }
197 |
198 | BASE_DIR=`find_maven_basedir "$(pwd)"`
199 | if [ -z "$BASE_DIR" ]; then
200 | exit 1;
201 | fi
202 |
203 | ##########################################################################################
204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
205 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
206 | ##########################################################################################
207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
208 | if [ "$MVNW_VERBOSE" = true ]; then
209 | echo "Found .mvn/wrapper/maven-wrapper.jar"
210 | fi
211 | else
212 | if [ "$MVNW_VERBOSE" = true ]; then
213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
214 | fi
215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
216 | while IFS="=" read key value; do
217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
218 | esac
219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
220 | if [ "$MVNW_VERBOSE" = true ]; then
221 | echo "Downloading from: $jarUrl"
222 | fi
223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
224 |
225 | if command -v wget > /dev/null; then
226 | if [ "$MVNW_VERBOSE" = true ]; then
227 | echo "Found wget ... using wget"
228 | fi
229 | wget "$jarUrl" -O "$wrapperJarPath"
230 | elif command -v curl > /dev/null; then
231 | if [ "$MVNW_VERBOSE" = true ]; then
232 | echo "Found curl ... using curl"
233 | fi
234 | curl -o "$wrapperJarPath" "$jarUrl"
235 | else
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Falling back to using Java to download"
238 | fi
239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
240 | if [ -e "$javaClass" ]; then
241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
242 | if [ "$MVNW_VERBOSE" = true ]; then
243 | echo " - Compiling MavenWrapperDownloader.java ..."
244 | fi
245 | # Compiling the Java class
246 | ("$JAVA_HOME/bin/javac" "$javaClass")
247 | fi
248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
249 | # Running the downloader
250 | if [ "$MVNW_VERBOSE" = true ]; then
251 | echo " - Running MavenWrapperDownloader.java ..."
252 | fi
253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
254 | fi
255 | fi
256 | fi
257 | fi
258 | ##########################################################################################
259 | # End of extension
260 | ##########################################################################################
261 |
262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
263 | if [ "$MVNW_VERBOSE" = true ]; then
264 | echo $MAVEN_PROJECTBASEDIR
265 | fi
266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
267 |
268 | # For Cygwin, switch paths to Windows format before running java
269 | if $cygwin; then
270 | [ -n "$M2_HOME" ] &&
271 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
272 | [ -n "$JAVA_HOME" ] &&
273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
274 | [ -n "$CLASSPATH" ] &&
275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
276 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
278 | fi
279 |
280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
281 |
282 | exec "$JAVACMD" \
283 | $MAVEN_OPTS \
284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
287 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven2 Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
126 | )
127 |
128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
130 | if exist %WRAPPER_JAR% (
131 | echo Found %WRAPPER_JAR%
132 | ) else (
133 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
134 | echo Downloading from: %DOWNLOAD_URL%
135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
136 | echo Finished downloading %WRAPPER_JAR%
137 | )
138 | @REM End of extension
139 |
140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
141 | if ERRORLEVEL 1 goto error
142 | goto end
143 |
144 | :error
145 | set ERROR_CODE=1
146 |
147 | :end
148 | @endlocal & set ERROR_CODE=%ERROR_CODE%
149 |
150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
154 | :skipRcPost
155 |
156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
158 |
159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
160 |
161 | exit /B %ERROR_CODE%
162 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.1.2.RELEASE
9 |
10 |
11 | com.industrie.ledger
12 | client-ledger-core-redis
13 | 1.0.0-SNAPSHOT
14 | client-ledger-core-redis
15 | Client Ledger Core by Redis
16 |
17 |
18 |
19 |
20 | org.springframework.boot
21 | spring-boot-maven-plugin
22 |
23 |
24 |
25 |
26 |
27 | 1.8
28 | ${java.version}
29 | ${java.version}
30 | 2.7.0
31 | 1.5.0
32 | 4.0.2
33 | 3.0.0
34 | 2.22.0
35 | 3.0.1
36 | 3.7.1
37 | 2.3
38 | 3.1.1
39 | 2.12.1
40 | 1.16
41 | 3.0.0
42 | 3.0.0
43 | 2.8.0
44 | 0.5.3
45 | 1.0.0
46 | 2.7.0
47 | 2.2.2.RELEASE
48 |
49 |
50 |
51 |
52 |
53 |
54 | com.google.guava
55 | guava
56 | 28.1-jre
57 |
58 |
59 | org.springframework.boot
60 | spring-boot-starter-aop
61 |
62 |
63 | org.springframework.boot
64 | spring-boot-starter-logging
65 |
66 |
67 | org.springframework.boot
68 | spring-boot-starter-web
69 |
70 |
71 | org.springframework.boot
72 | spring-boot-starter-test
73 | test
74 |
75 |
76 | org.springframework.kafka
77 | spring-kafka
78 |
79 |
80 | org.springframework.boot
81 | spring-boot-autoconfigure
82 |
83 |
84 |
85 |
86 |
87 | reports
88 |
89 |
90 |
91 | org.apache.maven.plugins
92 | maven-site-plugin
93 | ${maven-site-plugin.version}
94 |
95 |
96 |
97 |
98 |
99 |
100 | org.apache.maven.plugins
101 | maven-changelog-plugin
102 | ${maven-changelog-plugin.version}
103 |
104 |
105 | org.apache.maven.plugins
106 | maven-project-info-reports-plugin
107 | ${maven-project-info-reports-plugin.version}
108 |
109 |
110 |
111 | index
112 | ci-management
113 | dependencies
114 | dependency-convergence
115 | dependency-info
116 | dependency-management
117 | distribution-management
118 | issue-management
119 | licenses
120 | modules
121 | plugin-management
122 | plugins
123 | scm
124 | team
125 | summary
126 |
127 |
128 |
129 |
130 |
131 |
132 | org.apache.maven.plugins
133 | maven-surefire-report-plugin
134 | ${maven-surefire-report-plugin.version}
135 |
136 |
137 | org.apache.maven.plugins
138 | maven-checkstyle-plugin
139 | ${maven-checkstyle-plugin.version}
140 |
141 |
142 | org.apache.maven.plugins
143 | maven-jxr-plugin
144 | ${maven-jxr-plugin.version}
145 |
146 |
147 | org.apache.maven.plugins
148 | maven-dependency-plugin
149 | ${maven-dependency-plugin.version}
150 |
151 |
152 |
153 | analyze-report
154 |
155 |
156 |
157 |
158 |
159 | org.owasp
160 | dependency-check-maven
161 | ${dependency-check-maven.version}
162 |
163 |
164 |
165 | aggregate
166 |
167 |
168 |
169 |
170 | false
171 |
172 |
173 |
174 | org.codehaus.mojo
175 | license-maven-plugin
176 | ${license-maven-plugin.version}
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/Application.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.boot.CommandLineRunner;
7 | import org.springframework.boot.ExitCodeGenerator;
8 | import org.springframework.boot.SpringApplication;
9 | import org.springframework.boot.autoconfigure.SpringBootApplication;
10 |
11 | /**
12 | * Non-blocking, zero-locking client ledger which produces {@link Account}
13 | * and {@link JournalEntry} compliance with Accounting Standards, to be fed into
14 | * General Ledger for reporting purpose.
15 | * Run on Single Thread Processor behind a queued system. Support at least 100 high-level business transaction per second.
16 | * Support Atomic Business Transactions which can be itemized into any number of {@link JournalEntry},
17 | * but all the {@link JournalEntry} must sum up to zero.
18 | */
19 | @SpringBootApplication
20 | public class Application implements CommandLineRunner {
21 | @Value(value = "${auth.domain}")
22 | String authDomain;
23 |
24 | @Override
25 | public void run(String... arg0) {
26 | if (arg0.length > 0 && arg0[0].equals("exitcode")) {
27 | throw new ExitException();
28 | }
29 | }
30 |
31 | public static void main(String[] args) {
32 | new SpringApplication(Application.class).run(args);
33 | }
34 |
35 | /**
36 | * Exit Exception on command line args of exitcode
37 | */
38 | public static class ExitException extends RuntimeException implements ExitCodeGenerator {
39 | private static final long serialVersionUID = 1L;
40 |
41 | @Override
42 | public int getExitCode() {
43 | return 10;
44 | }
45 |
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/config/LedgerConfig.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.config;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
6 | import org.apache.kafka.clients.consumer.Consumer;
7 | import org.apache.kafka.clients.consumer.ConsumerConfig;
8 | import org.apache.kafka.clients.producer.ProducerConfig;
9 | import org.apache.kafka.common.serialization.StringDeserializer;
10 | import org.apache.kafka.common.serialization.StringSerializer;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.Configuration;
15 | import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
16 | import org.springframework.kafka.core.*;
17 | import org.springframework.kafka.support.serializer.JsonSerializer;
18 |
19 | import java.util.HashMap;
20 | import java.util.Map;
21 | import java.util.concurrent.ExecutorService;
22 | import java.util.concurrent.Executors;
23 |
24 | /**
25 | * Centralized place to inject beans
26 | */
27 | @Configuration
28 | public class LedgerConfig {
29 | /**
30 | * @return a single thread executor for {@link Processor} to run on
31 | */
32 | @Bean
33 | public ExecutorService executorService() {
34 | return Executors.newSingleThreadExecutor();
35 | }
36 |
37 | /**
38 | * @return {@link Logger} for standardized logging
39 | */
40 | @Bean
41 | public Logger logger() {
42 | return LoggerFactory.getLogger("com.industrieit.dragon.clientledger.core.memory");
43 | }
44 |
45 | /**
46 | * @return {@link ObjectMapper} for JSON serialization, as Ledger is JSON-based.
47 | */
48 | @Bean
49 | public ObjectMapper objectMapper() {
50 | return new ObjectMapper();
51 | }
52 |
53 |
54 | @Bean
55 | public ProducerFactory producerFactoryForResult() {
56 | Map config = new HashMap<>();
57 |
58 | config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
59 | config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
60 | config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
61 |
62 | return new DefaultKafkaProducerFactory<>(config);
63 | }
64 |
65 |
66 | @Bean
67 | public KafkaTemplate kafkaTemplateForResult() {
68 | return new KafkaTemplate<>(producerFactoryForResult());
69 | }
70 |
71 | @Bean
72 | public ConsumerFactory consumerFactory() {
73 | Map config = new HashMap<>();
74 | config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
75 | config.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer_core_memory");
76 | config.put(ConsumerConfig.GROUP_ID_CONFIG, "group_core_memory");
77 | config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
78 | config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
79 | return new DefaultKafkaConsumerFactory<>(config);
80 | }
81 |
82 | @Bean
83 | public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
84 | ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
85 | factory.setConcurrency(1);
86 | factory.setConsumerFactory(consumerFactory());
87 | return factory;
88 | }
89 |
90 | @Bean
91 | public Consumer consumer() {
92 | return consumerFactory().createConsumer();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/Consumer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 | import org.apache.kafka.clients.consumer.ConsumerRecord;
5 |
6 | import java.io.IOException;
7 |
8 | /**
9 | * Consumer which runs on single thread, and consume the request event strictly serially
10 | * No locking of database tables is therefore needed.
11 | */
12 | public interface Consumer {
13 |
14 | /**
15 | * consume exactly one {@link TransactionEvent}
16 | * protect for idempotency, so that {@link TransactionEvent} with same UUID represents same request, and will not be processed
17 | * @param consumerRecord which can be parsed into exactly one {@link TransactionEvent} with offset and partition meta data
18 | * @throws IOException on wrong parsing
19 | */
20 | void consume(ConsumerRecord consumerRecord) throws IOException;
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/Processor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
6 |
7 | /**
8 | * Processor to process {@link TransactionEvent} in a strict serial order
9 | * Can make use of {@link Producer} to produce result as a side effect of processing
10 | */
11 | public interface Processor {
12 | /**
13 | * Process one {@link TransactionEvent} in strictly serial order
14 | *
15 | * @param transactionEvent {@link TransactionEvent} to be processed
16 | */
17 | void process(TransactionEvent transactionEvent);
18 |
19 | /**
20 | * Provide {@link Type} for {@link Consumer} to rightly pick the responsible {@link Processor}
21 | *
22 | * @return {@link Type} to String
23 | */
24 | String getType();
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/Producer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
6 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
7 | import org.springframework.transaction.annotation.Isolation;
8 | import org.springframework.transaction.annotation.Transactional;
9 |
10 | /**
11 | * Producer to produce {@link TransactionResult} for each of the {@link TransactionEvent}
12 | */
13 | public interface Producer {
14 | /**
15 | * Produce {@link TransactionEvent} which records a processing error
16 | * @param requestId unique ID which matches to the {@link TransactionEvent}
17 | * @param e {@link InvalidBusinessRuleException} which represents a processing error usually on violation of business rules
18 | * @param kafkaPartition
19 | * @param kafkaOffset
20 | */
21 | void produceError(String requestId, InvalidBusinessRuleException e, Integer kafkaPartition, long kafkaOffset);
22 |
23 | /**
24 | * Produce {@link TransactionEvent} which records a processing success
25 | * @param type of the response, can be anything which best represents the processing success (e.g., journals committed, account created, snapshot timestamp)
26 | * @param requestId unique ID which matches to the {@link TransactionEvent}
27 | * @param response object which is serialized into JSON and written to the response field of {@link TransactionResult}
28 | * @param kafkaPartition
29 | * @param kafkaOffset
30 | */
31 | @Transactional(isolation = Isolation.SERIALIZABLE)
32 | void produceSuccess(String requestId, T response, Integer kafkaPartition, long kafkaOffset);
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/BackUpProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
6 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
7 | import org.springframework.stereotype.Component;
8 |
9 | @Component
10 | public class BackUpProcessor implements Processor {
11 | private final Producer producer;
12 |
13 |
14 | public BackUpProcessor(Producer producer) {
15 | this.producer = producer;
16 | }
17 |
18 | @Override
19 | public void process(TransactionEvent transactionEvent) {
20 | producer.produceSuccess(transactionEvent.getId(), null,
21 | transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
22 |
23 | }
24 |
25 | @Override
26 | public String getType() {
27 | return Type.BACK_UP.toString();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/ConsumerImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Consumer;
5 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionEventRepository;
9 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
10 | import com.industrieit.ledger.clientledger.core.memory.service.TransactionService;
11 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
12 | import org.apache.kafka.clients.consumer.ConsumerRecord;
13 | import org.apache.kafka.clients.consumer.ConsumerRecords;
14 | import org.apache.kafka.common.TopicPartition;
15 | import org.slf4j.Logger;
16 | import org.springframework.beans.factory.annotation.Autowired;
17 | import org.springframework.stereotype.Component;
18 |
19 | import javax.annotation.PostConstruct;
20 | import javax.annotation.PreDestroy;
21 | import java.io.IOException;
22 | import java.time.Duration;
23 | import java.util.Collection;
24 | import java.util.Collections;
25 | import java.util.List;
26 | import java.util.concurrent.ExecutorService;
27 |
28 | @Component
29 | public class ConsumerImpl implements Consumer, Runnable {
30 | private final List processors;
31 | private final ObjectMapper objectMapper;
32 | private final TransactionEventRepository transactionEventRepository;
33 | private final TransactionResultRepository transactionResultRepository;
34 | private final org.apache.kafka.clients.consumer.Consumer kafkaConsumer;
35 | private final ExecutorService executorService;
36 | private final Logger logger;
37 | private final TransactionService transactionService;
38 | public static final String TOPIC = "Transaction_Event";
39 | boolean running = true;
40 |
41 | @Autowired
42 | public ConsumerImpl(List processors, ObjectMapper objectMapper,
43 | TransactionEventRepository transactionEventRepository,
44 | TransactionResultRepository transactionResultRepository,
45 | org.apache.kafka.clients.consumer.Consumer kafkaConsumer,
46 | ExecutorService executorService, Logger logger,
47 | TransactionService transactionService) {
48 | this.processors = processors;
49 | this.objectMapper = objectMapper;
50 | this.transactionEventRepository = transactionEventRepository;
51 | this.transactionResultRepository = transactionResultRepository;
52 | this.kafkaConsumer = kafkaConsumer;
53 | this.executorService = executorService;
54 | this.logger = logger;
55 | this.transactionService = transactionService;
56 | }
57 |
58 | @PostConstruct
59 | public void init() {
60 | executorService.submit(this);
61 | }
62 |
63 | @PreDestroy
64 | public void destroy() {
65 | kafkaConsumer.unsubscribe();
66 | executorService.shutdown();
67 | }
68 |
69 | @Override
70 | public void run() {
71 | TransactionResult lastResult = transactionService.getLastResult();
72 | kafkaConsumer.subscribe(Collections.singleton(TOPIC), new ConsumerRebalanceListener() {
73 | @Override
74 | public void onPartitionsRevoked(Collection partitions) {
75 | }
76 |
77 | @Override
78 | public void onPartitionsAssigned(Collection partitions) {
79 | if (lastResult == null){
80 | kafkaConsumer.seekToBeginning(partitions);
81 | } else {
82 | for (TopicPartition topicPartition : partitions) {
83 | if (topicPartition.partition() == lastResult.getKafkaPartition()) {
84 | kafkaConsumer.seek(topicPartition, lastResult.getKafkaOffset());
85 | }
86 | }
87 | }
88 | }
89 | });
90 |
91 | while (running) {
92 | ConsumerRecords poll = kafkaConsumer.poll(Duration.ofMillis(100));
93 | for (ConsumerRecord consumerRecord : poll) {
94 | logger.info("start consuming: " + consumerRecord.value());
95 | consume(consumerRecord);
96 | logger.info("done consuming : " + consumerRecord.value());
97 | }
98 | kafkaConsumer.commitSync();
99 | }
100 |
101 | }
102 |
103 | @Override
104 | public void consume(ConsumerRecord consumerRecord) {
105 | TransactionEvent transactionEvent;
106 | try {
107 | transactionEvent = objectMapper.readValue(consumerRecord.value(), TransactionEvent.class);
108 | } catch (IOException e) {
109 | return;
110 | }
111 | if (!transactionEventRepository.existsById(transactionEvent.getId()) || !transactionResultRepository.findByRequestId(transactionEvent.getId()).isPresent()) {
112 | transactionEvent.setKafkaOffset(consumerRecord.offset());
113 | transactionEvent.setKafkaPartition(consumerRecord.partition());
114 | TransactionEvent save = transactionEventRepository.save(transactionEvent);
115 | processors
116 | .stream()
117 | .filter(processor ->
118 | processor.getType().equals(transactionEvent.getType()))
119 | .findFirst()
120 | .ifPresent(processor ->
121 | processor.process(save));
122 | }
123 |
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/CreateAccountProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
6 |
7 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
8 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
9 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
10 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
11 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.CreateAccountRequest;
12 | import com.industrieit.ledger.clientledger.core.memory.service.AccountService;
13 | import org.springframework.stereotype.Component;
14 | import org.springframework.transaction.annotation.Isolation;
15 | import org.springframework.transaction.annotation.Transactional;
16 |
17 | import java.io.IOException;
18 |
19 | @Component
20 | public class CreateAccountProcessor implements Processor {
21 | private final ObjectMapper objectMapper;
22 | private final AccountService accountService;
23 | private final Producer producer;
24 |
25 |
26 | public CreateAccountProcessor(ObjectMapper objectMapper, AccountService accountService, Producer producer) {
27 | this.objectMapper = objectMapper;
28 | this.accountService = accountService;
29 | this.producer = producer;
30 | }
31 |
32 | public void process(TransactionEvent transactionEvent) {
33 | String requestId = transactionEvent.getId();
34 |
35 | CreateAccountRequest createAccountRequest;
36 | try {
37 | createAccountRequest = objectMapper.readValue(transactionEvent.getRequest(), CreateAccountRequest.class);
38 | } catch (IOException e) {
39 | producer.produceError(requestId, new InvalidBusinessRuleException("Malformed request"),
40 | transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
41 | return;
42 | }
43 | try {
44 | createAccountAndProduce(requestId, createAccountRequest, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
45 | } catch (InvalidBusinessRuleException e) {
46 | producer.produceError(requestId, e, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
47 | }
48 | }
49 |
50 | @Transactional(isolation = Isolation.SERIALIZABLE)
51 | public void createAccountAndProduce(String requestId, CreateAccountRequest createAccountRequest, Integer kafkaPartition, long kafkaOffset) {
52 | Account account = this.accountService.createAccount(createAccountRequest);
53 | producer.produceSuccess(requestId, account, kafkaPartition, kafkaOffset);
54 | }
55 |
56 | @Override
57 | public String getType() {
58 | return Type.CREATE_ACCOUNT.toString();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/P2PProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
7 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
8 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
9 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
10 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
11 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
12 | import org.springframework.stereotype.Component;
13 | import org.springframework.transaction.annotation.Isolation;
14 | import org.springframework.transaction.annotation.Transactional;
15 |
16 | import java.io.IOException;
17 |
18 | @Component
19 | public class P2PProcessor implements Processor {
20 | private final ObjectMapper objectMapper;
21 | private final JournalService p2PService;
22 | private final Producer producer;
23 |
24 |
25 | public P2PProcessor(ObjectMapper objectMapper,
26 | JournalService p2PService, Producer producer) {
27 | this.objectMapper = objectMapper;
28 | this.p2PService = p2PService;
29 | this.producer = producer;
30 | }
31 |
32 | public void process(TransactionEvent transactionEvent) {
33 | String requestId = transactionEvent.getId();
34 |
35 | P2PRequest p2PRequest;
36 | try {
37 | p2PRequest = objectMapper.readValue(transactionEvent.getRequest(), P2PRequest.class);
38 | } catch (IOException e) {
39 | producer.produceError(requestId, new InvalidBusinessRuleException("Malformed request"),
40 | transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
41 | return;
42 | }
43 | try {
44 | journalAndProduce(requestId, p2PRequest, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
45 | } catch (InvalidBusinessRuleException e) {
46 | producer.produceError(requestId, e, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
47 | }
48 | }
49 |
50 | @Transactional(isolation = Isolation.SERIALIZABLE)
51 | public void journalAndProduce(String requestId, P2PRequest p2PRequest, Integer kafkaPartition, long kafkaOffset) {
52 | Iterable transactionLogs = p2PService.journal(requestId, p2PRequest, kafkaPartition, kafkaOffset);
53 | producer.produceSuccess(requestId, transactionLogs, kafkaPartition, kafkaOffset);
54 | }
55 |
56 | @Override
57 | public String getType() {
58 | return Type.P2P.toString();
59 | }
60 |
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/ProducerImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
7 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
9 | import org.springframework.kafka.core.KafkaTemplate;
10 | import org.springframework.stereotype.Component;
11 | import org.springframework.transaction.annotation.Isolation;
12 | import org.springframework.transaction.annotation.Transactional;
13 |
14 | import java.util.Date;
15 |
16 | @Component
17 | public class ProducerImpl implements Producer {
18 | private final TransactionResultRepository transactionResultRepository;
19 | private final ObjectMapper objectMapper;
20 | private final KafkaTemplate kafkaTemplate;
21 | private static final String TOPIC = "Transaction_Result_Memory";
22 |
23 |
24 |
25 | public ProducerImpl(TransactionResultRepository transactionResultRepository, ObjectMapper objectMapper,
26 | KafkaTemplate kafkaTemplate) {
27 | this.transactionResultRepository = transactionResultRepository;
28 | this.objectMapper = objectMapper;
29 | this.kafkaTemplate = kafkaTemplate;
30 | }
31 |
32 | public void produceError(String requestId, InvalidBusinessRuleException e, Integer kafkaPartition, long kafkaOffset) {
33 | TransactionResult transactionResult = new TransactionResult();
34 | transactionResult.setRequestId(requestId);
35 | if (e != null) {
36 | transactionResult.setResponse("{\"message\": \"" + e.getMessage() + "\"}");
37 | } else {
38 | transactionResult.setResponse("{\"message\": null}");
39 | }
40 | transactionResult.setSuccess(false);
41 | transactionResult.setKafkaPartition(kafkaPartition);
42 | transactionResult.setKafkaOffset(kafkaOffset);
43 | TransactionResult save = transactionResultRepository.save(transactionResult);
44 | kafkaTemplate.send(TOPIC, save);
45 |
46 | }
47 |
48 | @Transactional(isolation = Isolation.SERIALIZABLE)
49 | public void produceSuccess(String requestId, T response, Integer kafkaPartition, long kafkaOffset) {
50 | TransactionResult transactionResult = new TransactionResult();
51 | transactionResult.setRequestId(requestId);
52 | try {
53 | transactionResult.setResponse(objectMapper.writeValueAsString(response));
54 | } catch (JsonProcessingException e) {
55 | transactionResult.setResponse("{}");
56 | }
57 | transactionResult.setSuccess(true);
58 | transactionResult.setKafkaPartition(kafkaPartition);
59 | transactionResult.setKafkaOffset(kafkaOffset);
60 | TransactionResult save = transactionResultRepository.save(transactionResult);
61 | save.setCreateTime(new Date().getTime());
62 | kafkaTemplate.send(TOPIC, save);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/TopUpProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
5 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
7 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
8 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
9 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
10 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
11 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
12 | import org.springframework.stereotype.Component;
13 | import org.springframework.transaction.annotation.Isolation;
14 | import org.springframework.transaction.annotation.Transactional;
15 |
16 | import java.io.IOException;
17 |
18 | @Component
19 | public class TopUpProcessor implements Processor {
20 | private final ObjectMapper objectMapper;
21 | private final Producer producer;
22 | private final JournalService topUpService;
23 |
24 | public TopUpProcessor(ObjectMapper objectMapper,
25 | Producer producer, JournalService topUpService) {
26 | this.objectMapper = objectMapper;
27 | this.producer = producer;
28 | this.topUpService = topUpService;
29 | }
30 |
31 |
32 | @Override
33 | public void process(TransactionEvent transactionEvent) {
34 | String requestId = transactionEvent.getId();
35 |
36 | TopUpRequest topUpRequest;
37 | try {
38 | topUpRequest = objectMapper.readValue(transactionEvent.getRequest(), TopUpRequest.class);
39 | } catch (IOException e) {
40 | producer.produceError(requestId, new InvalidBusinessRuleException("Malformed request"),
41 | transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
42 | return;
43 | }
44 |
45 | try {
46 | journalAndProduce(requestId, topUpRequest, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
47 | } catch (InvalidBusinessRuleException e) {
48 | producer.produceError(requestId, e, transactionEvent.getKafkaPartition(), transactionEvent.getKafkaOffset());
49 | }
50 | }
51 |
52 | @Transactional(isolation = Isolation.SERIALIZABLE)
53 | public void journalAndProduce(String requestId, TopUpRequest topUpRequest, Integer kafkaPartition, long kafkaOffset) {
54 | Iterable transactionLogs = this.topUpService.journal(requestId, topUpRequest, kafkaPartition, kafkaOffset);
55 | producer.produceSuccess(requestId, transactionLogs, kafkaPartition, kafkaOffset);
56 | }
57 |
58 | @Override
59 | public String getType() {
60 | return Type.TOP_UP.toString();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/controller/AccountController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
5 | import org.springframework.web.bind.annotation.*;
6 |
7 | import java.util.Optional;
8 |
9 | /**
10 | * REST controller which provides only GET endpoints for {@link Account}.
11 | */
12 | @RestController
13 | @RequestMapping("/account")
14 | public class AccountController {
15 | private final AccountRepository accountRepository;
16 |
17 | public AccountController(AccountRepository accountRepository) {
18 | this.accountRepository = accountRepository;
19 | }
20 |
21 |
22 | /**
23 | * Get all {@link Account} from ledger.
24 | * @return all {@link Account}
25 | */
26 | @GetMapping(value = "/",
27 | produces = {"application/json"},
28 | consumes = {"application/json"})
29 | @ResponseBody
30 | public Iterable getAll() {
31 | return accountRepository.findAll();
32 | }
33 |
34 | /**
35 | * Get one {@link Account} which matches the ID
36 | * @param id account ID
37 | * @return {@link Account} matching the ID
38 | */
39 | @GetMapping(value = "/{id}",
40 | produces = {"application/json"},
41 | consumes = {"application/json"})
42 | @ResponseBody
43 | public Account get(@PathVariable String id) {
44 | Optional byId = accountRepository.findById(id);
45 | return byId.orElse(null);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/controller/JournalEntryController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
6 | import org.springframework.web.bind.annotation.*;
7 |
8 | /**
9 | * REST controller which provides only GET endpoints for {@link JournalEntry}.
10 | */
11 | @RestController
12 | @RequestMapping("/journal")
13 | public class JournalEntryController {
14 | private final JournalEntryRepository journalEntryRepository;
15 |
16 | public JournalEntryController(JournalEntryRepository journalEntryRepository) {
17 | this.journalEntryRepository = journalEntryRepository;
18 | }
19 |
20 | /**
21 | * GET all {@link JournalEntry} committed to the Ledger
22 | * @return all {@link JournalEntry}
23 | */
24 | @GetMapping(value = "/",
25 | produces = {"application/json"},
26 | consumes = {"application/json"})
27 | @ResponseBody
28 | public Iterable getAll() {
29 | return journalEntryRepository.findAll();
30 | }
31 |
32 | /**
33 | * GET all {@link JournalEntry} driven by one {@link TransactionEvent}
34 | * @param requestId request ID unique to {@link TransactionEvent}
35 | * @return a list of {@link JournalEntry} related
36 | */
37 | @GetMapping(value = "/request/{requestId}",
38 | produces = {"application/json"},
39 | consumes = {"application/json"})
40 | @ResponseBody
41 | public Iterable get(@PathVariable String requestId) {
42 | return journalEntryRepository.findAllByRequestId(requestId);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/controller/TransactionController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.google.common.collect.Lists;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
6 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionEventRepository;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.service.TransactionService;
9 | import org.springframework.web.bind.annotation.*;
10 |
11 | import java.util.Collections;
12 | import java.util.Optional;
13 |
14 | /**
15 | * REST Controller which is exclusively allowed to POST on the Ledger through creating and enqueuing {@link TransactionEvent}
16 | */
17 | @RestController
18 | @RequestMapping("/transaction")
19 | public class TransactionController {
20 | private final TransactionEventRepository transactionEventRepository;
21 | private final TransactionResultRepository transactionResultRepository;
22 | private final TransactionService transactionService;
23 |
24 | public TransactionController(TransactionEventRepository transactionEventRepository, TransactionResultRepository transactionResultRepository, TransactionService transactionService) {
25 | this.transactionEventRepository = transactionEventRepository;
26 | this.transactionResultRepository = transactionResultRepository;
27 | this.transactionService = transactionService;
28 | }
29 |
30 | /**
31 | * GET one {@link TransactionResult} based on request ID
32 | *
33 | * @param requestId ID which uniquely identifies the {@link TransactionResult}
34 | * @return {@link TransactionResult}
35 | */
36 | @GetMapping(value = "/result/event/{requestId}",
37 | produces = {"application/json"},
38 | consumes = {"application/json"})
39 | @ResponseBody
40 | public TransactionResult getResult(@PathVariable String requestId) {
41 | Optional byId = transactionResultRepository.findByRequestId(requestId);
42 | return byId.orElse(null);
43 | }
44 |
45 | @GetMapping(value = "/result/current",
46 | produces = {"application/json"},
47 | consumes = {"application/json"})
48 | @ResponseBody
49 | public TransactionResult getLastResult() {
50 | return transactionService.getLastResult();
51 | }
52 |
53 |
54 | /**
55 | * GET all {@link TransactionResult} produced by the Ledger
56 | *
57 | * @return all {@link TransactionResult}
58 | */
59 | @GetMapping(value = "/result",
60 | produces = {"application/json"},
61 | consumes = {"application/json"})
62 | @ResponseBody
63 | public Iterable getAllResult() {
64 | return transactionResultRepository.findAll();
65 | }
66 |
67 | /**
68 | * GET one {@link TransactionEvent} based on request ID
69 | *
70 | * @param id ID which uniquely identifies the {@link TransactionEvent}
71 | * @return {@link TransactionEvent}
72 | */
73 | @GetMapping(value = "/event/{id}",
74 | produces = {"application/json"},
75 | consumes = {"application/json"})
76 | @ResponseBody
77 | public TransactionEvent getEvent(@PathVariable String id) {
78 | Optional byId = transactionEventRepository.findById(id);
79 | return byId.orElse(null);
80 | }
81 |
82 | @GetMapping(value = "/event/current",
83 | produces = {"application/json"},
84 | consumes = {"application/json"})
85 | @ResponseBody
86 | public TransactionEvent getLastEvent() {
87 | Iterable all = transactionEventRepository.findAll();
88 | return Collections.max(Lists.newArrayList(all), (o1, o2) -> {
89 | long diff = o1.getKafkaOffset() - o2.getKafkaOffset();
90 | if (diff > 0) {
91 | return 1;
92 | }
93 | if (diff == 0) {
94 | return 0;
95 | }
96 | return -1;
97 | });
98 | }
99 |
100 |
101 | /**
102 | * GET all {@link TransactionEvent} enqueued for the Ledger
103 | *
104 | * @return all {@link TransactionEvent}
105 | */
106 | @GetMapping(value = "/event",
107 | produces = {"application/json"},
108 | consumes = {"application/json"})
109 | @ResponseBody
110 | public Iterable getAllEvent() {
111 | return transactionEventRepository.findAll();
112 | }
113 |
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/entity/Account.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import java.io.Serializable;
4 | import java.math.BigDecimal;
5 | import java.util.Date;
6 |
7 | /**
8 | * One of the only TWO building block for ledger, apart from {@link JournalEntry}
9 | * Represents the places where debit and credit goes
10 | * All functionality and transaction are provided by adding a new {@link Account}
11 | * {@link Account} can be grouped into assets, liabilities, profit and loss.
12 | * Equally, {@link Account} can represent settlement, customer, bank, internal, tax, and fee.
13 | */
14 |
15 | public class Account implements Serializable {
16 |
17 | private String id;
18 |
19 | private long createTime = new Date().getTime();
20 |
21 | private String currency;
22 |
23 | private String accountName;
24 |
25 | private String accountGroup;
26 |
27 | private BigDecimal balance;
28 |
29 | private Long kafkaOffset = 0L;
30 |
31 | private Integer kafkaPartition = 0;
32 |
33 |
34 | /**
35 | * @return Account Name which provides useful info for the user and accountant, e.g., "Andrew's customer account"
36 | */
37 | public String getAccountName() {
38 | return accountName;
39 | }
40 |
41 | public void setAccountName(String accountName) {
42 | this.accountName = accountName;
43 | }
44 |
45 | /**
46 | * @return Account Group which provides handy useful grouping for the account, e.g., "Settlement", "Customer", "Fee", Tax"
47 | * Mainly used for business rule validation
48 | * But actual grouping of the accounts under different financial statements are out of scope of any ledgers
49 | * And shall be done by the user application of the ledger (e.g., accounting software, banking module)
50 | */
51 | public String getAccountGroup() {
52 | return accountGroup;
53 | }
54 |
55 | public void setAccountGroup(String accountGroup) {
56 | this.accountGroup = accountGroup;
57 | }
58 |
59 | /**
60 | * @return unique identifier of the account
61 | */
62 | public String getId() {
63 | return id;
64 | }
65 |
66 | public void setId(String id) {
67 | this.id = id;
68 | }
69 |
70 | /**
71 | * @return creation time of the Account
72 | */
73 | public long getCreateTime() {
74 | return createTime;
75 | }
76 |
77 | public void setCreateTime(long createTime) {
78 | this.createTime = createTime;
79 | }
80 |
81 | /**
82 | * @return currency code, e.g., "USD", "JPY", "HKD"
83 | * allow for business rule validation
84 | * but actually not necessary if validation can be done out of ledger
85 | */
86 | public String getCurrency() {
87 | return currency;
88 | }
89 |
90 | public void setCurrency(String currency) {
91 | this.currency = currency;
92 | }
93 |
94 | public BigDecimal getBalance() {
95 | return balance;
96 | }
97 |
98 | public void setBalance(BigDecimal balance) {
99 | this.balance = balance;
100 | }
101 |
102 | public Long getKafkaOffset() {
103 | return kafkaOffset;
104 | }
105 |
106 | public void setKafkaOffset(Long kafkaOffset) {
107 | this.kafkaOffset = kafkaOffset;
108 | }
109 |
110 | public Integer getKafkaPartition() {
111 | return kafkaPartition;
112 | }
113 |
114 | public void setKafkaPartition(Integer kafkaPartition) {
115 | this.kafkaPartition = kafkaPartition;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/entity/JournalEntry.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import java.io.Serializable;
4 | import java.math.BigDecimal;
5 | import java.util.Date;
6 |
7 | /**
8 | * One of the only TWO building block for ledger, apart from {@link Account}
9 | * Represents the only allowable actions to be applied onto any {@link Account}
10 | * Summing up all {@link JournalEntry} for all {@link Account} in the ledger will always amount to ZERO.
11 | */
12 |
13 | public class JournalEntry implements Serializable {
14 | private String id;
15 | private String requestId;
16 |
17 | /**
18 | * @return request ID which drives the posting of this journal entry, used for debug and tracing
19 | */
20 | public String getRequestId() {
21 | return requestId;
22 | }
23 |
24 | public void setRequestId(String requestId) {
25 | this.requestId = requestId;
26 | }
27 |
28 | private String accountId;
29 |
30 | private String currency;
31 |
32 | /**
33 | * @return currency code which the journal entry is denominated in, not necessary but just handy info.
34 | * Because currency is determined by {@link Account}
35 | * Each {@link Account} shall have exactly one currency.
36 | * If a customer needs many currency, just create many {@link Account}
37 | */
38 | public String getCurrency() {
39 | return currency;
40 | }
41 |
42 | public void setCurrency(String currency) {
43 | this.currency = currency;
44 | }
45 |
46 | private long createTime = new Date().getTime();
47 |
48 | private BigDecimal amount;
49 |
50 | /**
51 | * @return id for database persisting, not particularly useful
52 | */
53 | public String getId() {
54 | return id;
55 | }
56 |
57 | public void setId(String id) {
58 | this.id = id;
59 | }
60 |
61 | /**
62 | * @return create Time
63 | */
64 | public long getCreateTime() {
65 | return createTime;
66 | }
67 |
68 | public void setCreateTime(long createTime) {
69 | this.createTime = createTime;
70 | }
71 |
72 |
73 | /**
74 | * @return Amount of the journal entry. Can be either positive and negative.
75 | * amount of one atomic block of journal entries shall also sum up to zero.
76 | */
77 | public BigDecimal getAmount() {
78 | return amount;
79 | }
80 |
81 | public void setAmount(BigDecimal amount) {
82 | this.amount = amount;
83 | }
84 |
85 | public JournalEntry(String accountId, String currency, BigDecimal amount, String requestId) {
86 | this.accountId = accountId;
87 | this.amount = amount;
88 | this.currency = currency;
89 | this.requestId = requestId;
90 | }
91 |
92 | public JournalEntry() {
93 | }
94 |
95 | public String getAccountId() {
96 | return accountId;
97 | }
98 |
99 | public void setAccountId(String accountId) {
100 | this.accountId = accountId;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/entity/TransactionEvent.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.consumer.Consumer;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
6 |
7 | import java.util.Date;
8 |
9 | /**
10 | * Entity which represents an accepted and enqueued high-level transaction, fully packed into a self-contained event
11 | * {@link TransactionEvent} can be consumed by {@link Consumer}
12 | * On consumption, exactly one {@link TransactionResult} will be produced and persisted.
13 | * The full enqueued list of {@link TransactionEvent}, in a strict serial order, will form the basis of Event Sourcing.
14 | * Event sourcing allows the full state of the ledger be replayed, on any platform and infrastructure, with any processors.
15 | * This allows in-memory processing and reliable recovery from crash.
16 | */
17 | public class TransactionEvent {
18 |
19 | private String id;
20 | private String type;
21 | private String request;
22 | private long createTime = new Date().getTime();
23 | private long kafkaOffset;
24 | private Integer kafkaPartition;
25 | /**
26 | * @return id which uniquely identify this transaction event.
27 | */
28 | public String getId() {
29 | return id;
30 | }
31 |
32 | public void setId(String id) {
33 | this.id = id;
34 | }
35 |
36 | /**
37 | * @return payload of the request, usually a JSON string
38 | */
39 | public String getRequest() {
40 | return request;
41 | }
42 |
43 | public void setRequest(String request) {
44 | this.request = request;
45 | }
46 |
47 | /**
48 | * @return create time of the event. Important for event sourcing, as the event must be strictly in serial order of this field.
49 | * Take as the actual creation of enqueuing, which forms the sequence of event sourcing.
50 | * Can be different from the sending time from the client.
51 | * Like an event sent at a later time point can happen to arrive at the queue earlier, due to multi-threading.
52 | * But sequence are in strict order after the point of queueing, forming the basis of event sourcing.
53 | */
54 | public long getCreateTime() {
55 | return createTime;
56 | }
57 |
58 | public void setCreateTime(long createTime) {
59 | this.createTime = createTime;
60 | }
61 |
62 | /**
63 | * @return type as defined in {@link Type} which calls for correct {@link Processor}
64 | */
65 | public String getType() {
66 | return type;
67 | }
68 |
69 | public void setType(String type) {
70 | this.type = type;
71 | }
72 |
73 | public long getKafkaOffset() {
74 | return kafkaOffset;
75 | }
76 |
77 | public void setKafkaOffset(long kafkaOffset) {
78 | this.kafkaOffset = kafkaOffset;
79 | }
80 |
81 | public Integer getKafkaPartition() {
82 | return kafkaPartition;
83 | }
84 |
85 | public void setKafkaPartition(Integer kafkaPartition) {
86 | this.kafkaPartition = kafkaPartition;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/entity/TransactionResult.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Producer;
5 |
6 | import java.util.Date;
7 |
8 | /**
9 | * Entity which is produced by {@link Producer} for exactly one {@link TransactionEvent} after consumption
10 | * Represents the result of processing of {@link TransactionEvent} by the {@link Processor}
11 | * Can be either success or failure.
12 | * In both cases, more info are available and packed into the Event.
13 | */
14 | public class TransactionResult {
15 |
16 | private String id;
17 | private String requestId;
18 | private String response;
19 | private long createTime = new Date().getTime();
20 | private boolean success;
21 | private long kafkaOffset;
22 | private Integer kafkaPartition;
23 |
24 | /**
25 | * @return id, for database key, not particularly useful
26 | */
27 | public String getId() {
28 | return id;
29 | }
30 |
31 | public void setId(String id) {
32 | this.id = id;
33 | }
34 |
35 | /**
36 | * @return response which is usually a well-formed JSON string representing the result of processing of {@link TransactionEvent}
37 | */
38 | public String getResponse() {
39 | return response;
40 | }
41 |
42 | public void setResponse(String response) {
43 | this.response = response;
44 | }
45 |
46 | /**
47 | * @return request event id which originates this result
48 | */
49 | public String getRequestId() {
50 | return requestId;
51 | }
52 |
53 | public void setRequestId(String requestId) {
54 | this.requestId = requestId;
55 | }
56 |
57 | /**
58 | * @return create time. Form the sequence of the result queue.
59 | * the order shall be 100% identical with the order of the {@link TransactionEvent} queue
60 | */
61 | public long getCreateTime() {
62 | return createTime;
63 | }
64 |
65 | public void setCreateTime(long createTime) {
66 | this.createTime = createTime;
67 | }
68 |
69 | /**
70 | * @return whether the processing of {@link TransactionEvent} is successful or not.
71 | */
72 | public boolean isSuccess() {
73 | return success;
74 | }
75 |
76 | public void setSuccess(boolean success) {
77 | this.success = success;
78 | }
79 |
80 | public long getKafkaOffset() {
81 | return kafkaOffset;
82 | }
83 |
84 | public void setKafkaOffset(long kafkaOffset) {
85 | this.kafkaOffset = kafkaOffset;
86 | }
87 |
88 | public Integer getKafkaPartition() {
89 | return kafkaPartition;
90 | }
91 |
92 | public void setKafkaPartition(Integer kafkaPartition) {
93 | this.kafkaPartition = kafkaPartition;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/exception/InvalidBusinessRuleException.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.exception;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 |
5 | /**
6 | * Run Time Exception thrown during processing of one {@link TransactionEvent}
7 | */
8 | public class InvalidBusinessRuleException extends RuntimeException {
9 | public InvalidBusinessRuleException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/ledger/committer/Committer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.committer;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 |
7 | /**
8 | * Committer to commit atomically a group of {@link JournalEntry} which represents one {@link TransactionEvent}
9 | */
10 | public interface Committer {
11 | /**
12 | * Commit atomically a group of {@link JournalEntry} which represents
13 | * one {@link TransactionEvent}.
14 | * Rollback all {@link JournalEntry} if any one is not successfully committed.
15 | * Throw run time error {@link InvalidBusinessRuleException} if
16 | * sum of amounts of all {@link JournalEntry} to commit does not equal to ZERO.
17 | * @param logsToCommit the atomic group of {@link JournalEntry}
18 | * @param kafkaPartition
19 | * @param kafkaOffset
20 | * @return successfully committed group of {@link JournalEntry}
21 | */
22 | Iterable commit(Iterable logsToCommit, Integer kafkaPartition, Long kafkaOffset);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/ledger/committer/impl/BaseCommitter.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.committer.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.memory.ledger.committer.Committer;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
9 | import org.springframework.stereotype.Component;
10 | import org.springframework.transaction.annotation.Isolation;
11 | import org.springframework.transaction.annotation.Transactional;
12 |
13 | import java.math.BigDecimal;
14 | import java.util.ArrayList;
15 |
16 | @Component
17 | public class BaseCommitter implements Committer {
18 | private final JournalEntryRepository journalEntryRepository;
19 | private final AccountRepository accountRepository;
20 |
21 | public BaseCommitter(JournalEntryRepository journalEntryRepository, AccountRepository accountRepository) {
22 | this.journalEntryRepository = journalEntryRepository;
23 | this.accountRepository = accountRepository;
24 | }
25 |
26 | @Transactional(isolation = Isolation.SERIALIZABLE)
27 | public Iterable commit(Iterable logsToCommit, Integer kafkaPartition, Long kafkaOffset) {
28 | if (logsToCommit == null) {
29 | return new ArrayList<>();
30 | }
31 | BigDecimal cumulativeAmount = BigDecimal.ZERO;
32 | for (JournalEntry journalEntry : logsToCommit) {
33 | cumulativeAmount = cumulativeAmount.add(journalEntry.getAmount());
34 | }
35 | if (cumulativeAmount.compareTo(BigDecimal.ZERO) != 0) {
36 | throw new InvalidBusinessRuleException("Unbalanced journal entries");
37 | }
38 | for (JournalEntry journalEntry : logsToCommit) {
39 | Account account = accountRepository.findById(journalEntry.getAccountId()).get();
40 | account.setBalance(account.getBalance().add(journalEntry.getAmount()));
41 | account.setKafkaOffset(kafkaOffset);
42 | account.setKafkaPartition(kafkaPartition);
43 | accountRepository.save(account);
44 | }
45 | return journalEntryRepository.saveAll(logsToCommit);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/ledger/validator/Validator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.validator;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
7 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
8 |
9 | /**
10 | * Validator which validates {@link EventRequest} against all relevant business rules
11 | * @param Type of the high level transaction event
12 | */
13 | public interface Validator {
14 | /**
15 | * Validates {@link EventRequest} against all relevant business rules
16 | * throw runtime error {@link InvalidBusinessRuleException} if any business rule is violated.
17 | * @param requestId request ID which uniquely identifies one {@link TransactionEvent} which the {@link EventRequest} is based on.
18 | * @param request {@link EventRequest} to be validated
19 | * @return {@link Itemizable} if no business rules are violated, and ready for committing as journal entries of the ledger.
20 | */
21 | Itemizable validate(String requestId, T request);
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/ledger/validator/impl/P2PValidator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
6 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
7 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.impl.P2PItemizable;
8 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
9 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
10 | import org.springframework.stereotype.Component;
11 | import org.springframework.transaction.annotation.Isolation;
12 | import org.springframework.transaction.annotation.Transactional;
13 |
14 | import java.math.BigDecimal;
15 | import java.util.Optional;
16 |
17 | @Component
18 | public class P2PValidator implements Validator {
19 | private final AccountRepository accountRepository;
20 |
21 | public P2PValidator(AccountRepository accountRepository) {
22 | this.accountRepository = accountRepository;
23 | }
24 |
25 | @Transactional(isolation = Isolation.SERIALIZABLE)
26 | public Itemizable validate(String requestId, P2PRequest request) {
27 |
28 | if (request.getAmount().compareTo(BigDecimal.ZERO) < 0) {
29 | throw new InvalidBusinessRuleException("Negative amount not supported");
30 | }
31 | if (request.getFee().compareTo(BigDecimal.ZERO) < 0) {
32 | throw new InvalidBusinessRuleException("Negative fee not supported");
33 | }
34 | if (request.getTax().compareTo(BigDecimal.ZERO) < 0) {
35 | throw new InvalidBusinessRuleException("Negative tax not supported");
36 | }
37 |
38 | Optional sourceAccountOptional = accountRepository.findById(request.getFromCustomerAccount());
39 | if (!sourceAccountOptional.isPresent()) {
40 | throw new InvalidBusinessRuleException("Source account not found");
41 | } else {
42 | if (!sourceAccountOptional.get().getCurrency().equals(request.getCurrency())) {
43 | throw new InvalidBusinessRuleException("Currency exchange not supported for source account");
44 | }
45 | }
46 |
47 | Optional destinationAccountOptional = accountRepository.findById(request.getToCustomerAccount());
48 | if (!destinationAccountOptional.isPresent()) {
49 | throw new InvalidBusinessRuleException("Destination account not found");
50 | } else {
51 | if (!destinationAccountOptional.get().getCurrency().equals(request.getCurrency())) {
52 | throw new InvalidBusinessRuleException("Currency exchange not supported for destination account");
53 | }
54 | }
55 | Optional feeAccountOptional = accountRepository.findById(request.getFeeAccount());
56 | if (!feeAccountOptional.isPresent()) {
57 | throw new InvalidBusinessRuleException("Fee account not found");
58 | } else {
59 | if (!feeAccountOptional.get().getCurrency().equals(request.getCurrency())) {
60 | throw new InvalidBusinessRuleException("Currency exchange not supported for fee account");
61 | }
62 | }
63 | Optional taxAccountOptional = accountRepository.findById(request.getTaxAccount());
64 | if (!taxAccountOptional.isPresent()) {
65 | throw new InvalidBusinessRuleException("Tax account not found");
66 | } else {
67 | if (!taxAccountOptional.get().getCurrency().equals(request.getCurrency())) {
68 | throw new InvalidBusinessRuleException("Currency exchange not supported for tax account");
69 | }
70 | }
71 | BigDecimal sourceFinalBalance = sourceAccountOptional.get().getBalance();
72 | BigDecimal totalFundNeeded = request.getAmount().add(request.getFee()).add(request.getTax());
73 | if (sourceFinalBalance.compareTo(totalFundNeeded) < 0) {
74 | throw new InvalidBusinessRuleException("Not enough fund to P2P");
75 | }
76 | return new P2PItemizable(sourceAccountOptional.get(), destinationAccountOptional.get(), feeAccountOptional.get(),
77 | taxAccountOptional.get(), request.getAmount(), request.getFee(), request.getTax(), request.getCurrency(), requestId);
78 | }
79 |
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/ledger/validator/impl/TopUpValidator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
4 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.impl.TopUpItemizable;
5 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
7 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
9 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
10 | import org.springframework.stereotype.Component;
11 |
12 | import java.math.BigDecimal;
13 | import java.util.Optional;
14 |
15 | @Component
16 | public class TopUpValidator implements Validator {
17 | private final AccountRepository accountRepository;
18 |
19 | public TopUpValidator(AccountRepository accountRepository) {
20 | this.accountRepository = accountRepository;
21 | }
22 |
23 | @Override
24 | public Itemizable validate(String requestId, TopUpRequest request) {
25 | if (request.getAmount().compareTo(BigDecimal.ZERO) < 0) {
26 | throw new InvalidBusinessRuleException("Negative amount not supported");
27 | }
28 | Optional topUpAccountOptional = accountRepository.findById(request.getTopUpAccount());
29 | if (!topUpAccountOptional.isPresent()) {
30 | throw new InvalidBusinessRuleException("Top-up account not found");
31 | } else {
32 | if (!topUpAccountOptional.get().getCurrency().equals(request.getCurrency())) {
33 | throw new InvalidBusinessRuleException("Currency exchange not supported for top-up account");
34 | }
35 | }
36 | Optional settlementAccountOptional = accountRepository.findById(request.getSettlementAccount());
37 | if (!settlementAccountOptional.isPresent()) {
38 | throw new InvalidBusinessRuleException("Settlement account not found");
39 | } else {
40 | if (!settlementAccountOptional.get().getCurrency().equals(request.getCurrency())) {
41 | throw new InvalidBusinessRuleException("Currency exchange not supported for settlement account");
42 | }
43 | }
44 | return new TopUpItemizable(topUpAccountOptional.get(), settlementAccountOptional.get(), request.getAmount(), request.getCurrency(), requestId);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/Itemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * Itemizable are successful result of business rule validation, which can be itemized into a list of {@link JournalEntry}
10 | */
11 | public interface Itemizable {
12 | /**
13 | * itemized into a list of {@link JournalEntry}
14 | * @return a list of {@link JournalEntry}
15 | */
16 | List itemize();
17 |
18 | /**
19 | * allow tracing of originating request event ID
20 | * @return the ID which identify the request event
21 | */
22 | String getRequestId();
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/Type.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 |
5 | /**
6 | * Type of {@link TransactionEvent} allowed for the Ledger to process
7 | */
8 | public enum Type {
9 | P2P("p2p"),
10 | CREATE_ACCOUNT("create-account"),
11 | TOP_UP("top-up"),
12 | BACK_UP("back-up");
13 |
14 | private final String text;
15 |
16 | Type(final String text) {
17 | this.text = text;
18 | }
19 |
20 | @Override
21 | public String toString() {
22 | return text;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/impl/P2PItemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger.impl;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
7 |
8 | import java.math.BigDecimal;
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | public class P2PItemizable implements Itemizable {
13 | private final Account source;
14 | private final Account destination;
15 | private final Account feeAccount;
16 | private final Account taxAccount;
17 | private final BigDecimal amount;
18 | private final BigDecimal fee;
19 | private final BigDecimal tax;
20 | private final String currency;
21 | private final String requestId;
22 |
23 | public P2PItemizable(Account source, Account destination, Account feeAccount, Account taxAccount,
24 | BigDecimal amount, BigDecimal fee, BigDecimal tax, String currency, String requestId) {
25 | this.source = source;
26 | this.destination = destination;
27 | this.feeAccount = feeAccount;
28 | this.taxAccount = taxAccount;
29 | this.amount = amount;
30 | this.fee = fee;
31 | this.tax = tax;
32 | this.currency = currency;
33 | this.requestId = requestId;
34 | }
35 |
36 | @Override
37 | public List itemize() {
38 | List journalEntries = new ArrayList<>();
39 | journalEntries.add(new JournalEntry(source.getId(), currency, amount.negate(), getRequestId()));
40 | journalEntries.add(new JournalEntry(destination.getId(), currency, amount,getRequestId()));
41 | journalEntries.add(new JournalEntry(source.getId(), currency, fee.negate(), getRequestId()));
42 | journalEntries.add(new JournalEntry(feeAccount.getId(), currency, fee, getRequestId()));
43 | journalEntries.add(new JournalEntry(source.getId(), currency, tax.negate(), getRequestId()));
44 | journalEntries.add(new JournalEntry(taxAccount.getId(), currency, tax, getRequestId()));
45 | return journalEntries;
46 | }
47 |
48 | @Override
49 | public String getRequestId() {
50 | return requestId;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/impl/TopUpItemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
6 |
7 | import java.math.BigDecimal;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class TopUpItemizable implements Itemizable {
12 | private final Account topUp;
13 | private final Account settlement;
14 | private final BigDecimal amount;
15 | private final String currency;
16 | private final String requestId;
17 |
18 |
19 | public TopUpItemizable(Account topUp, Account settlement, BigDecimal amount, String currency, String requestId) {
20 | this.topUp = topUp;
21 | this.settlement = settlement;
22 | this.amount = amount;
23 | this.currency = currency;
24 | this.requestId = requestId;
25 | }
26 |
27 | @Override
28 | public List itemize() {
29 | List journalEntries = new ArrayList<>();
30 | journalEntries.add(new JournalEntry(settlement.getId(), currency, amount.negate(), getRequestId()));
31 | journalEntries.add(new JournalEntry(topUp.getId(), currency, amount, getRequestId()));
32 | return journalEntries;
33 | }
34 |
35 | @Override
36 | public String getRequestId() {
37 | return requestId;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/request/EventRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.request;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 |
5 | /**
6 | * Interface for the request payload to be parsed for creating {@link TransactionEvent}
7 | * Currently an empty interface, but can support common methods for monitoring
8 | */
9 | public interface EventRequest {
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/request/impl/CreateAccountRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
4 |
5 | public class CreateAccountRequest implements EventRequest {
6 | private String id;
7 | private String currency;
8 | private String accountName;
9 | private String accountGroup;
10 |
11 |
12 | public String getId() {
13 | return id;
14 | }
15 |
16 | public String getCurrency() {
17 | return currency;
18 | }
19 |
20 | public String getAccountName() {
21 | return accountName;
22 | }
23 |
24 | public String getAccountGroup() {
25 | return accountGroup;
26 | }
27 |
28 | public CreateAccountRequest(String id, String currency, String accountName, String accountGroup) {
29 | this.id = id;
30 | this.currency = currency;
31 | this.accountName = accountName;
32 | this.accountGroup = accountGroup;
33 | }
34 |
35 | public CreateAccountRequest() {
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/request/impl/P2PRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
4 |
5 | import java.math.BigDecimal;
6 |
7 | public class P2PRequest implements EventRequest {
8 |
9 | private String currency;
10 | private String fromCustomerAccount;
11 | private String toCustomerAccount;
12 | private String feeAccount;
13 | private String taxAccount;
14 | private BigDecimal amount;
15 | private BigDecimal fee;
16 | private BigDecimal tax;
17 |
18 |
19 | public BigDecimal getAmount() {
20 | return amount;
21 | }
22 |
23 | public String getFromCustomerAccount() {
24 | return fromCustomerAccount;
25 | }
26 |
27 | public String getToCustomerAccount() {
28 | return toCustomerAccount;
29 | }
30 |
31 | public BigDecimal getFee() {
32 | return fee;
33 | }
34 |
35 | public BigDecimal getTax() {
36 | return tax;
37 | }
38 |
39 | public String getCurrency() {
40 | return currency;
41 | }
42 |
43 | public String getFeeAccount() {
44 | return feeAccount;
45 | }
46 |
47 | public String getTaxAccount() {
48 | return taxAccount;
49 | }
50 |
51 |
52 | public P2PRequest() {
53 | }
54 |
55 | public P2PRequest(String currency, String fromCustomerAccount, String toCustomerAccount, String feeAccount, String taxAccount, BigDecimal amount, BigDecimal fee, BigDecimal tax) {
56 | this.currency = currency;
57 | this.fromCustomerAccount = fromCustomerAccount;
58 | this.toCustomerAccount = toCustomerAccount;
59 | this.feeAccount = feeAccount;
60 | this.taxAccount = taxAccount;
61 | this.amount = amount;
62 | this.fee = fee;
63 | this.tax = tax;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/request/impl/SnapshotRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
4 |
5 | public class SnapshotRequest implements EventRequest {
6 | private String accountId;
7 |
8 | public String getAccountId() {
9 | return accountId;
10 | }
11 |
12 | public SnapshotRequest(String accountId) {
13 | this.accountId = accountId;
14 | }
15 |
16 | public SnapshotRequest() {
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/model/request/impl/TopUpRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
4 |
5 | import java.math.BigDecimal;
6 |
7 | public class TopUpRequest implements EventRequest {
8 | private String currency;
9 | private String topUpAccount;
10 | private String settlementAccount;
11 | private BigDecimal amount;
12 |
13 | public String getCurrency() {
14 | return currency;
15 | }
16 |
17 | public String getTopUpAccount() {
18 | return topUpAccount;
19 | }
20 |
21 | public String getSettlementAccount() {
22 | return settlementAccount;
23 | }
24 |
25 | public BigDecimal getAmount() {
26 | return amount;
27 | }
28 |
29 | public TopUpRequest() {
30 | }
31 |
32 | public TopUpRequest(String currency, String topUpAccount, String settlementAccount, BigDecimal amount) {
33 | this.currency = currency;
34 | this.topUpAccount = topUpAccount;
35 | this.settlementAccount = settlementAccount;
36 | this.amount = amount;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/AccountRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 |
5 | import java.util.Optional;
6 |
7 | public interface AccountRepository {
8 | Optional findById(String s);
9 |
10 | Account save(Account account);
11 |
12 | boolean existsById(String id);
13 |
14 | Iterable findAll();
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/JournalEntryRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 |
6 |
7 | public interface JournalEntryRepository {
8 |
9 | Iterable saveAll(Iterable iterable);
10 |
11 | Iterable findAllByRequestId(String requestId);
12 |
13 | Iterable findAll();
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/TransactionEventRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 |
5 | import java.util.Optional;
6 |
7 | public interface TransactionEventRepository {
8 | Iterable findAll();
9 |
10 | Optional findById(String id);
11 |
12 | boolean existsById(String id);
13 |
14 | TransactionEvent save(TransactionEvent transactionEvent);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/TransactionResultRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
4 |
5 | import java.util.Optional;
6 |
7 | public interface TransactionResultRepository {
8 | Optional findByRequestId(String requestId);
9 |
10 | Iterable findAll();
11 |
12 | TransactionResult save(TransactionResult transactionResult);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/impl/AccountRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.Optional;
10 |
11 | @Repository
12 | public class AccountRepositoryImpl implements AccountRepository {
13 | private final Map accounts = new HashMap<>();
14 | @Override
15 | public Optional findById(String s) {
16 | return Optional.ofNullable(accounts.get(s));
17 | }
18 |
19 | @Override
20 | public Account save(Account account) {
21 | accounts.put(account.getId(), account);
22 | return account;
23 | }
24 |
25 | @Override
26 | public boolean existsById(String id) {
27 | return accounts.containsKey(id);
28 | }
29 |
30 | @Override
31 | public Iterable findAll() {
32 | return accounts.values();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/impl/JournalEntryRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
4 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | @Repository
11 | public class JournalEntryRepositoryImpl implements JournalEntryRepository {
12 | private final List journalEntries = new ArrayList<>();
13 |
14 |
15 | @Override
16 | public Iterable saveAll(Iterable iterable) {
17 | for (JournalEntry journalEntry : iterable) {
18 | journalEntries.add(journalEntry);
19 | }
20 | return iterable;
21 | }
22 |
23 | @Override
24 | public Iterable findAllByRequestId(String requestId) {
25 | List result = new ArrayList<>();
26 | for (JournalEntry journalEntry : journalEntries) {
27 | if (journalEntry.getRequestId().equals(requestId)) {
28 | result.add(journalEntry);
29 | }
30 | }
31 | return result;
32 | }
33 |
34 | @Override
35 | public Iterable findAll() {
36 | return journalEntries;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/impl/TransactionEventRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionEventRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.Optional;
10 |
11 | @Repository
12 | public class TransactionEventRepositoryImpl implements TransactionEventRepository {
13 | private final Map transactionEvents = new HashMap<>();
14 | @Override
15 | public Iterable findAll() {
16 | return transactionEvents.values();
17 | }
18 |
19 | @Override
20 | public Optional findById(String id) {
21 | return Optional.ofNullable(transactionEvents.get(id));
22 | }
23 |
24 | @Override
25 | public boolean existsById(String id) {
26 | return transactionEvents.containsKey(id);
27 | }
28 |
29 | @Override
30 | public TransactionEvent save(TransactionEvent transactionEvent) {
31 | transactionEvents.put(transactionEvent.getId(), transactionEvent);
32 | return transactionEvent;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/repository/impl/TransactionResultRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.repository.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
4 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.Optional;
10 |
11 | @Repository
12 | public class TransactionResultRepositoryImpl implements TransactionResultRepository {
13 | private final Map transactionResults = new HashMap<>();
14 | @Override
15 | public Optional findByRequestId(String requestId) {
16 | return Optional.ofNullable(transactionResults.get(requestId));
17 | }
18 |
19 | @Override
20 | public Iterable findAll() {
21 | return transactionResults.values();
22 | }
23 |
24 | @Override
25 | public TransactionResult save(TransactionResult transactionResult) {
26 | transactionResults.put(transactionResult.getRequestId(), transactionResult);
27 | return transactionResult;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.CreateAccountRequest;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
5 |
6 | /**
7 | * Serializable isolated transactional service to mutate {@link Account}
8 | */
9 | public interface AccountService {
10 | /**
11 | * Create an {@link Account} in serialized isolated transaction
12 | * @param createAccountRequest all the info needed for creating {@link Account}
13 | * @return {@link Account} successfully created
14 | */
15 | Account createAccount(CreateAccountRequest createAccountRequest);
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/JournalService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.memory.model.request.EventRequest;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 |
7 | /**
8 | * Serializable isolated transactional service to mutate {@link JournalEntry}
9 | * @param the type of {@link EventRequest} seeking to mutate {@link JournalEntry}
10 | */
11 | public interface JournalService {
12 | /**
13 | * Journal {@link EventRequest} as a group of atomic {@link JournalEntry}
14 | * @param requestId ID which uniquely identifies the originating {@link TransactionEvent}
15 | * @param request {@link EventRequest} seeking to mutate {@link JournalEntry}
16 | * @param kafkaPartition
17 | * @param kafkaOffset
18 | * @return the atomic group of {@link JournalEntry} committed to the ledger
19 | */
20 | Iterable journal(String requestId, T request, Integer kafkaPartition, Long kafkaOffset);
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/SnapshotService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.SnapshotRequest;
6 |
7 | /**
8 | * Serializable isolated transactional service to mutate {@link Account}
9 | */
10 | public interface SnapshotService {
11 | /**
12 | * snapshot by capturing {@link JournalEntry} for required {@link Account}
13 | * and calculated {@link Account} exactly and precisely up to the snapshot epoch
14 | * @param snapshotRequest requires which {@link Account} to snapshot, null means all accounts are required
15 | * @return timestamp of the snapshot epoch which the snapshot are accurate as at.
16 | */
17 | long snapshot(SnapshotRequest snapshotRequest);
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/TransactionService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
4 |
5 | public interface TransactionService {
6 | TransactionResult getLastResult();
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/impl/AccountServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.CreateAccountRequest;
6 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
7 | import com.industrieit.ledger.clientledger.core.memory.service.AccountService;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.transaction.annotation.Isolation;
10 | import org.springframework.transaction.annotation.Transactional;
11 |
12 | import java.math.BigDecimal;
13 |
14 | @Service
15 | public class AccountServiceImpl implements AccountService {
16 | private final AccountRepository accountRepository;
17 |
18 | public AccountServiceImpl(AccountRepository accountRepository) {
19 | this.accountRepository = accountRepository;
20 | }
21 |
22 | @Transactional(isolation = Isolation.SERIALIZABLE)
23 | @Override
24 | public Account createAccount(CreateAccountRequest createAccountRequest) {
25 | if (accountRepository.existsById(createAccountRequest.getId())) {
26 | throw new InvalidBusinessRuleException("Account already existed");
27 | }
28 | Account account = new Account();
29 | account.setId(createAccountRequest.getId());
30 | account.setCurrency(createAccountRequest.getCurrency());
31 | account.setAccountGroup(createAccountRequest.getAccountGroup());
32 | account.setAccountName(createAccountRequest.getAccountName());
33 | account.setBalance(BigDecimal.ZERO);
34 | Account save = accountRepository.save(account);
35 |
36 | return save;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/impl/P2PServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.ledger.committer.Committer;
4 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
5 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
6 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
7 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
8 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
9 | import org.springframework.stereotype.Service;
10 | import org.springframework.transaction.annotation.Isolation;
11 | import org.springframework.transaction.annotation.Transactional;
12 |
13 | @Service
14 | public class P2PServiceImpl implements JournalService {
15 | private final Validator validator;
16 | private final Committer committer;
17 |
18 | public P2PServiceImpl(Validator validator, Committer committer) {
19 | this.validator = validator;
20 | this.committer = committer;
21 | }
22 |
23 | @Transactional(isolation = Isolation.SERIALIZABLE)
24 | @Override
25 | public Iterable journal(String requestId, P2PRequest request, Integer kafkaPartition, Long kafkaOffset) {
26 | Itemizable itemizable = validator.validate(requestId, request);
27 | return committer.commit(itemizable.itemize(), kafkaPartition, kafkaOffset);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/impl/SnapshotServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.SnapshotRequest;
4 | import com.industrieit.ledger.clientledger.core.memory.service.SnapshotService;
5 | import org.springframework.stereotype.Service;
6 | import org.springframework.transaction.annotation.Isolation;
7 | import org.springframework.transaction.annotation.Transactional;
8 |
9 | import java.util.Date;
10 |
11 | @Service
12 | public class SnapshotServiceImpl implements SnapshotService {
13 |
14 | public SnapshotServiceImpl() {
15 |
16 | }
17 |
18 | @Transactional(isolation = Isolation.SERIALIZABLE)
19 | @Override
20 | public long snapshot(SnapshotRequest snapshotRequest) {
21 | return new Date().getTime();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/impl/TopUpServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.ledger.committer.Committer;
4 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
5 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Itemizable;
6 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
7 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
8 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
9 | import org.springframework.stereotype.Service;
10 | import org.springframework.transaction.annotation.Isolation;
11 | import org.springframework.transaction.annotation.Transactional;
12 |
13 | @Service
14 | public class TopUpServiceImpl implements JournalService {
15 | private final Validator validator;
16 |
17 | private final Committer committer;
18 |
19 | public TopUpServiceImpl(Validator validator, Committer committer) {
20 | this.validator = validator;
21 | this.committer = committer;
22 | }
23 |
24 | @Transactional(isolation = Isolation.SERIALIZABLE)
25 | @Override
26 | public Iterable journal(String requestId, TopUpRequest request, Integer kafkaPartition, Long kafkaOffset) {
27 | Itemizable itemizable = validator.validate(requestId, request);
28 | return committer.commit(itemizable.itemize(), kafkaPartition, kafkaOffset);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/memory/service/impl/TransactionServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.google.common.collect.Lists;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
6 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
7 | import com.industrieit.ledger.clientledger.core.memory.service.TransactionService;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.util.Collections;
11 |
12 | @Service
13 | public class TransactionServiceImpl implements TransactionService {
14 | private final TransactionResultRepository transactionResultRepository;
15 |
16 | public TransactionServiceImpl(TransactionResultRepository transactionResultRepository) {
17 | this.transactionResultRepository = transactionResultRepository;
18 | }
19 |
20 | public TransactionResult getLastResult(){
21 | Iterable all = transactionResultRepository.findAll();
22 | if (all == null || Iterables.isEmpty(all)){
23 | return null;
24 | }
25 | return Collections.max(Lists.newArrayList(all), (o1, o2) -> {
26 | long diff = o1.getKafkaOffset() - o2.getKafkaOffset();
27 | if (diff > 0) {
28 | return 1;
29 | }
30 | if (diff == 0) {
31 | return 0;
32 | }
33 | return -1;
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=${CLIENT_LEDGER_CORE_REDIS_PORT}
2 | auth.domain=${AUTH_DOMAIN}
3 | # actuator
4 | management.endpoint.health.show-details=always
5 | management.endpoints.web.exposure.include=/health
6 |
7 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/ApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | public class ApplicationTests {
7 |
8 |
9 | @Test
10 | public void testRun() {
11 | Application clientLedgerServiceApplication = new Application();
12 | clientLedgerServiceApplication.run();
13 | }
14 |
15 | @Test
16 | public void testRunWithParam() {
17 | try {
18 | Application clientLedgerServiceApplication = new Application();
19 | clientLedgerServiceApplication.run("exitcode");
20 | } catch (Application.ExitException e) {
21 | Assert.assertEquals(10, e.getExitCode());
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/config/LedgerConfigTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.config;
2 |
3 |
4 | import org.junit.Assert;
5 | import org.junit.Test;
6 |
7 | public class LedgerConfigTest {
8 | @Test
9 | public void test(){
10 | LedgerConfig ledgerConfig = new LedgerConfig();
11 | Assert.assertNotNull(ledgerConfig.executorService());
12 | Assert.assertNotNull(ledgerConfig.logger());
13 | Assert.assertNotNull(ledgerConfig.objectMapper());
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/ConsumerImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.memory.consumer.Processor;
5 |
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionEventRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
9 | import com.industrieit.ledger.clientledger.core.memory.service.TransactionService;
10 | import org.apache.kafka.clients.consumer.ConsumerRecord;
11 | import org.apache.kafka.clients.consumer.ConsumerRecords;
12 | import org.apache.kafka.common.TopicPartition;
13 | import org.junit.Before;
14 | import org.junit.Test;
15 | import org.mockito.ArgumentMatchers;
16 | import org.mockito.Mock;
17 | import org.mockito.Mockito;
18 | import org.mockito.MockitoAnnotations;
19 | import org.slf4j.Logger;
20 |
21 | import java.io.IOException;
22 | import java.time.Duration;
23 | import java.util.Collections;
24 | import java.util.concurrent.ExecutorService;
25 |
26 | import static org.mockito.ArgumentMatchers.nullable;
27 |
28 | public class ConsumerImplTest {
29 | @Mock
30 | private Processor processor;
31 | @Mock
32 | private TransactionEventRepository transactionEventRepository;
33 | @Mock
34 | private TransactionResultRepository transactionResultRepository;
35 | @Mock
36 | private ExecutorService executorService;
37 | @Mock
38 | private ObjectMapper objectMapper;
39 | @Mock
40 | private Logger logger;
41 | @Mock
42 | private org.apache.kafka.clients.consumer.Consumer kafkaConsumer;
43 | @Mock
44 | private TransactionService transactionService;
45 |
46 | private ConsumerImpl consumer;
47 |
48 | @Before
49 | public void before() {
50 | MockitoAnnotations.initMocks(this);
51 | consumer = new ConsumerImpl(Collections.singletonList(processor), objectMapper, transactionEventRepository, transactionResultRepository, kafkaConsumer, executorService, logger, transactionService);
52 | Mockito.when(processor.getType()).thenReturn("P2P");
53 | }
54 |
55 | @Test
56 | public void testConsume_match() throws IOException {
57 | TransactionEvent transactionEvent = new TransactionEvent();
58 | transactionEvent.setId("abcd1234");
59 | transactionEvent.setRequest("{}");
60 | transactionEvent.setType("P2P");
61 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
62 | ConsumerRecord consumerRecord =
63 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
64 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
65 | consumer.consume(consumerRecord);
66 | Mockito.verify(processor).process(nullable(TransactionEvent.class));
67 | }
68 |
69 | @Test
70 | public void testConsume_noMatch() throws IOException {
71 | TransactionEvent transactionEvent = new TransactionEvent();
72 | transactionEvent.setId("abcd1234");
73 | transactionEvent.setRequest("{}");
74 | transactionEvent.setType("create-account");
75 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
76 | ConsumerRecord consumerRecord =
77 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
78 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
79 | consumer.consume(consumerRecord);
80 | Mockito.verify(processor, Mockito.never()).process(nullable(TransactionEvent.class));
81 | }
82 |
83 | @Test
84 | public void testConsume_InvalidBusinessRuleException() throws IOException {
85 | TransactionEvent transactionEvent = new TransactionEvent();
86 | transactionEvent.setId("abcd1234");
87 | transactionEvent.setRequest("{}");
88 | transactionEvent.setType("P2P");
89 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
90 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
91 | ConsumerRecord consumerRecord =
92 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
93 | consumer.consume(consumerRecord);
94 | }
95 |
96 | @Test
97 | public void testInit() {
98 | Mockito.when(executorService.submit(nullable(Runnable.class))).thenReturn(null);
99 | consumer.init();
100 | }
101 |
102 | @Test
103 | public void testDestory() {
104 | Mockito.when(executorService.shutdownNow()).thenReturn(null);
105 | consumer.destroy();
106 | }
107 |
108 | @Test
109 | public void testRun_notRunning() {
110 | consumer.running = false;
111 | consumer.run();
112 | }
113 |
114 | @Test
115 | public void testRun_running() throws IOException {
116 | consumer.running = true;
117 | TransactionEvent transactionEvent = new TransactionEvent();
118 | transactionEvent.setId("abcd1234");
119 | transactionEvent.setRequest("{}");
120 | transactionEvent.setType("P2P");
121 | ConsumerRecord consumerRecord =
122 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
123 | Mockito.when(kafkaConsumer.poll(nullable(Duration.class)))
124 | .thenReturn(new ConsumerRecords<>(Collections.singletonMap(new TopicPartition("hello", 0), Collections.singletonList(consumerRecord))));
125 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
126 | Thread t1 = new Thread(() -> {
127 | consumer.run();
128 | });
129 | t1.start();
130 | t1.interrupt();
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/CreateAccountProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
9 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.CreateAccountRequest;
10 | import com.industrieit.ledger.clientledger.core.memory.service.AccountService;
11 | import org.junit.Assert;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.*;
15 |
16 | import java.io.IOException;
17 |
18 | import static org.mockito.ArgumentMatchers.nullable;
19 |
20 | public class CreateAccountProcessorTest {
21 | @Mock
22 | private ObjectMapper objectMapper;
23 | @Mock
24 | private AccountService accountService;
25 | @Mock
26 | private ProducerImpl resultProcessor;
27 | @InjectMocks
28 | private CreateAccountProcessor createAccountProcessor;
29 |
30 | @Before
31 | public void before() {
32 | MockitoAnnotations.initMocks(this);
33 | }
34 |
35 | @Test
36 | public void testProcess() throws IOException {
37 | CreateAccountRequest createAccountRequest = new CreateAccountRequest("112233","JPY","test", "test-group");
38 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenReturn(createAccountRequest);
39 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenReturn(new Account());
40 | TransactionEvent transactionEvent = new TransactionEvent();
41 | transactionEvent.setId("abcd1234");
42 | transactionEvent.setRequest("{}");
43 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
44 | createAccountProcessor.process(transactionEvent);
45 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
46 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
47 | }
48 |
49 | @Test
50 | public void testProcess_cannotRead() throws IOException {
51 | CreateAccountRequest createAccountRequest = new CreateAccountRequest();
52 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenThrow(new IOException());
53 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenReturn(new Account());
54 | TransactionEvent transactionEvent = new TransactionEvent();
55 | transactionEvent.setId("abcd1234");
56 | transactionEvent.setRequest("{}");
57 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
58 | createAccountProcessor.process(transactionEvent);
59 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class),nullable(Integer.class), nullable(long.class));
60 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class),nullable(Integer.class), nullable(long.class));
61 | }
62 |
63 | @Test
64 | public void testProcess_serviceFail() throws IOException {
65 | CreateAccountRequest createAccountRequest = new CreateAccountRequest("112233","JPY","test", "test-group");
66 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenReturn(createAccountRequest);
67 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenThrow(new InvalidBusinessRuleException("service fails"));
68 | TransactionEvent transactionEvent = new TransactionEvent();
69 | transactionEvent.setId("abcd1234");
70 | transactionEvent.setRequest("{}");
71 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
72 | createAccountProcessor.process(transactionEvent);
73 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
74 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
75 | }
76 |
77 | @Test
78 | public void testGetType(){
79 | Assert.assertEquals(Type.CREATE_ACCOUNT.toString(), createAccountProcessor.getType());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/P2PProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
9 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
10 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
11 | import org.junit.Assert;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.*;
15 |
16 | import java.io.IOException;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | import static org.mockito.ArgumentMatchers.nullable;
21 |
22 | public class P2PProcessorTest {
23 | @Mock
24 | private ObjectMapper objectMapper;
25 | @Mock
26 | private JournalService p2PService;
27 | @Mock
28 | private ProducerImpl resultProcessor;
29 | @InjectMocks
30 | private P2PProcessor p2PProcessor;
31 |
32 | @Before
33 | public void before() {
34 | MockitoAnnotations.initMocks(this);
35 | }
36 |
37 | @Test
38 | public void testProcess() throws IOException {
39 | P2PRequest p2PRequest = new P2PRequest();
40 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(P2PRequest.class))).thenReturn(p2PRequest);
41 | List journalEntries = new ArrayList<>();
42 | Mockito.when(p2PService.journal(nullable(String.class), nullable(P2PRequest.class), nullable(Integer.class), nullable(long.class)))
43 | .thenReturn(journalEntries);
44 | TransactionEvent transactionEvent = new TransactionEvent();
45 | transactionEvent.setId("abcd1234");
46 | transactionEvent.setRequest("{}");
47 | transactionEvent.setType(Type.P2P.toString());
48 | p2PProcessor.process(transactionEvent);
49 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
50 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class),nullable(Integer.class), nullable(long.class));
51 | }
52 |
53 | @Test
54 | public void testProcess_cannotRead() throws IOException {
55 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(P2PRequest.class))).thenThrow(new IOException());
56 | List journalEntries = new ArrayList<>();
57 | Mockito.when(p2PService.journal(nullable(String.class), nullable(P2PRequest.class), nullable(Integer.class), nullable(long.class)))
58 | .thenReturn(journalEntries);
59 | TransactionEvent transactionEvent = new TransactionEvent();
60 | transactionEvent.setId("abcd1234");
61 | transactionEvent.setRequest("{}");
62 | transactionEvent.setType(Type.P2P.toString());
63 | p2PProcessor.process(transactionEvent);
64 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
65 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
66 | }
67 |
68 | @Test
69 | public void testProcess_serviceFail() throws IOException {
70 | P2PRequest p2PRequest = new P2PRequest();
71 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(P2PRequest.class))).thenReturn(p2PRequest);
72 | Mockito.when(p2PService.journal(nullable(String.class), nullable(P2PRequest.class), nullable(Integer.class), nullable(long.class)))
73 | .thenThrow(new InvalidBusinessRuleException("service fails"));
74 | TransactionEvent transactionEvent = new TransactionEvent();
75 | transactionEvent.setId("abcd1234");
76 | transactionEvent.setRequest("{}");
77 | transactionEvent.setType(Type.P2P.toString());
78 | p2PProcessor.process(transactionEvent);
79 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
80 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
81 | }
82 |
83 | @Test
84 | public void testGetType() {
85 | Assert.assertEquals(Type.P2P.toString(), p2PProcessor.getType());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/ProducerImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.core.io.JsonEOFException;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 |
7 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
8 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
9 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.InjectMocks;
13 | import org.mockito.Mock;
14 | import org.mockito.Mockito;
15 | import org.mockito.MockitoAnnotations;
16 | import org.springframework.kafka.core.KafkaTemplate;
17 |
18 | import static org.mockito.ArgumentMatchers.nullable;
19 |
20 | public class ProducerImplTest {
21 | @Mock
22 | private TransactionResultRepository transactionResultRepository;
23 | @Mock
24 | private ObjectMapper objectMapper;
25 | @Mock
26 | private KafkaTemplate kafkaTemplate;
27 |
28 | @InjectMocks
29 | ProducerImpl transactionResultProducer;
30 |
31 | @Before
32 | public void before() {
33 | MockitoAnnotations.initMocks(this);
34 | Mockito.when(kafkaTemplate.send(nullable(String.class), nullable(TransactionResult.class))).thenReturn(null);
35 | }
36 |
37 | @Test
38 | public void testProcessSuccessfulTransaction() throws JsonProcessingException {
39 | Mockito.when(transactionResultRepository.save(nullable(TransactionResult.class))).thenReturn(new TransactionResult());
40 | Mockito.when(objectMapper.writeValueAsString(nullable(Object.class))).thenReturn("{}");
41 | transactionResultProducer.produceSuccess(null, null, 0, 0L);
42 | Mockito.verify(transactionResultRepository).save(nullable(TransactionResult.class));
43 | Mockito.verify(kafkaTemplate).send(nullable(String.class), nullable(TransactionResult.class));
44 | }
45 |
46 | @Test
47 | public void testProcessSuccessfulTransaction_cannotRead() throws JsonProcessingException {
48 | Mockito.when(transactionResultRepository.save(nullable(TransactionResult.class))).thenReturn(new TransactionResult());
49 | Mockito.when(objectMapper.writeValueAsString(nullable(Object.class))).thenThrow(new JsonEOFException(null, null, null));
50 | transactionResultProducer.produceSuccess(null, null, 0 , 0L);
51 | Mockito.verify(transactionResultRepository).save(nullable(TransactionResult.class));
52 | Mockito.verify(kafkaTemplate).send(nullable(String.class), nullable(TransactionResult.class));
53 | }
54 |
55 | @Test
56 | public void testProduceErrorResult() {
57 | Mockito.when(transactionResultRepository.save(nullable(TransactionResult.class))).thenReturn(new TransactionResult());
58 | transactionResultProducer.produceError(null, new InvalidBusinessRuleException("test"), 0, 0L);
59 | Mockito.verify(transactionResultRepository).save(nullable(TransactionResult.class));
60 | Mockito.verify(kafkaTemplate).send(nullable(String.class), nullable(TransactionResult.class));
61 | }
62 |
63 | @Test
64 | public void testProduceErrorResult_null() {
65 | Mockito.when(transactionResultRepository.save(nullable(TransactionResult.class))).thenReturn(new TransactionResult());
66 | transactionResultProducer.produceError(null, null, 0, 0L);
67 | Mockito.verify(transactionResultRepository).save(nullable(TransactionResult.class));
68 | Mockito.verify(kafkaTemplate).send(nullable(String.class), nullable(TransactionResult.class));
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/consumer/impl/TopUpProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.Type;
9 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
10 | import com.industrieit.ledger.clientledger.core.memory.service.JournalService;
11 | import org.junit.Assert;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.*;
15 |
16 | import java.io.IOException;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | import static org.mockito.ArgumentMatchers.nullable;
21 |
22 | public class TopUpProcessorTest {
23 | @Mock
24 | private ObjectMapper objectMapper;
25 | @Mock
26 | private ProducerImpl resultProcessor;
27 | @Mock
28 | private JournalService topUpService;
29 | @InjectMocks
30 | private TopUpProcessor topUpProcessor;
31 |
32 | @Before
33 | public void before() {
34 | MockitoAnnotations.initMocks(this);
35 | }
36 |
37 | @Test
38 | public void testProcess() throws IOException {
39 | TopUpRequest topUpRequest = new TopUpRequest();
40 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TopUpRequest.class))).thenReturn(topUpRequest);
41 | List journalEntries = new ArrayList<>();
42 | Mockito.when(topUpService.journal(nullable(String.class), nullable(TopUpRequest.class), nullable(Integer.class), nullable(long.class)))
43 | .thenReturn(journalEntries);
44 | TransactionEvent transactionEvent = new TransactionEvent();
45 | transactionEvent.setId("abcd1234");
46 | transactionEvent.setRequest("{}");
47 | transactionEvent.setType(Type.TOP_UP.toString());
48 | topUpProcessor.process(transactionEvent);
49 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
50 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
51 | }
52 |
53 | @Test
54 | public void testProcess_cannotRead() throws IOException {
55 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TopUpRequest.class))).thenThrow(new IOException());
56 | List journalEntries = new ArrayList<>();
57 | Mockito.when(topUpService.journal(nullable(String.class), nullable(TopUpRequest.class), nullable(Integer.class), nullable(long.class)))
58 | .thenReturn(journalEntries);
59 | TransactionEvent transactionEvent = new TransactionEvent();
60 | transactionEvent.setId("abcd1234");
61 | transactionEvent.setRequest("{}");
62 | transactionEvent.setType(Type.TOP_UP.toString());
63 | topUpProcessor.process(transactionEvent);
64 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
65 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
66 | }
67 |
68 | @Test
69 | public void testProcess_serviceFail() throws IOException {
70 | TopUpRequest topUpRequest = new TopUpRequest();
71 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TopUpRequest.class))).thenReturn(topUpRequest);
72 | List journalEntries = new ArrayList<>();
73 | Mockito.when(topUpService.journal(nullable(String.class), nullable(TopUpRequest.class), nullable(Integer.class), nullable(long.class)))
74 | .thenThrow(new InvalidBusinessRuleException("service fails"));
75 | TransactionEvent transactionEvent = new TransactionEvent();
76 | transactionEvent.setId("abcd1234");
77 | transactionEvent.setRequest("{}");
78 | transactionEvent.setType(Type.TOP_UP.toString());
79 | topUpProcessor.process(transactionEvent);
80 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(Integer.class), nullable(long.class));
81 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(Integer.class), nullable(long.class));
82 | }
83 |
84 | @Test
85 | public void testGetType(){
86 | Assert.assertEquals(Type.TOP_UP.toString(), topUpProcessor.getType());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/controller/AccountControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
6 | import org.junit.Assert;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.mockito.InjectMocks;
10 | import org.mockito.Mock;
11 | import org.mockito.Mockito;
12 | import org.mockito.MockitoAnnotations;
13 |
14 | import java.util.ArrayList;
15 | import java.util.Optional;
16 |
17 | import static org.mockito.ArgumentMatchers.nullable;
18 |
19 | public class AccountControllerTest {
20 | @Mock
21 | private AccountRepository accountRepository;
22 | @InjectMocks
23 | private AccountController accountController;
24 |
25 | @Before
26 | public void before() {
27 | MockitoAnnotations.initMocks(this);
28 | }
29 |
30 | @Test
31 | public void testGetAll(){
32 | Mockito.when(accountRepository.findAll()).thenReturn(new ArrayList<>());
33 | Iterable all = accountController.getAll();
34 | Assert.assertNotNull(all);
35 | Assert.assertEquals(0, Iterables.size(all));
36 | }
37 |
38 | @Test
39 | public void testGet(){
40 | Mockito.when(accountRepository.findById(nullable(String.class))).thenReturn(Optional.of(new Account()));
41 | Account account = accountController.get("12345");
42 | Assert.assertNotNull(account);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/controller/JournalEntryControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
6 | import org.junit.Assert;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.mockito.InjectMocks;
10 | import org.mockito.Mock;
11 | import org.mockito.Mockito;
12 | import org.mockito.MockitoAnnotations;
13 |
14 | import java.util.ArrayList;
15 |
16 | import static org.mockito.ArgumentMatchers.nullable;
17 |
18 | public class JournalEntryControllerTest {
19 | @Mock
20 | private JournalEntryRepository journalEntryRepository;
21 |
22 | @InjectMocks
23 | private JournalEntryController journalEntryController;
24 |
25 | @Before
26 | public void before() {
27 | MockitoAnnotations.initMocks(this);
28 | }
29 |
30 | @Test
31 | public void testGetAll(){
32 | Mockito.when(journalEntryRepository.findAll()).thenReturn(new ArrayList<>());
33 | Iterable all = journalEntryController.getAll();
34 | Assert.assertNotNull(all);
35 | Assert.assertEquals(0, Iterables.size(all));
36 | }
37 |
38 | @Test
39 | public void testGet(){
40 | Mockito.when(journalEntryRepository.findAllByRequestId(nullable(String.class)))
41 | .thenReturn(new ArrayList<>());
42 | Iterable journalEntries = journalEntryController.get("12345");
43 | Assert.assertNotNull(journalEntries);
44 | Assert.assertEquals(0, Iterables.size(journalEntries));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/controller/TransactionControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.controller;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.google.common.collect.Iterables;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionEvent;
6 | import com.industrieit.ledger.clientledger.core.memory.entity.TransactionResult;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionEventRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.TransactionResultRepository;
9 | import org.junit.Assert;
10 | import org.junit.Before;
11 | import org.junit.Rule;
12 | import org.junit.Test;
13 | import org.junit.rules.ExpectedException;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.Mockito;
17 | import org.mockito.MockitoAnnotations;
18 | import org.springframework.kafka.core.KafkaTemplate;
19 |
20 | import java.util.ArrayList;
21 | import java.util.Optional;
22 |
23 | import static org.mockito.ArgumentMatchers.nullable;
24 |
25 | public class TransactionControllerTest {
26 | @Rule
27 | public ExpectedException thrown = ExpectedException.none();
28 | @Mock
29 | private TransactionEventRepository transactionEventRepository;
30 | @Mock
31 | private TransactionResultRepository transactionResultRepository;
32 | @Mock
33 | private ObjectMapper objectMapper;
34 | @Mock
35 | private KafkaTemplate kafkaTemplate;
36 | @InjectMocks
37 | private TransactionController transactionController;
38 |
39 | @Before
40 | public void before() {
41 | MockitoAnnotations.initMocks(this);
42 | Mockito.when(kafkaTemplate.send(nullable(String.class), nullable(TransactionEvent.class))).thenReturn(null);
43 | }
44 |
45 | @Test
46 | public void testGetResultById() {
47 | Mockito.when(transactionResultRepository.findByRequestId(nullable(String.class)))
48 | .thenReturn(Optional.of(new TransactionResult()));
49 | TransactionResult result = transactionController.getResult("1234");
50 | Assert.assertNotNull(result);
51 | }
52 |
53 | @Test
54 | public void testGetAllResult() {
55 | Mockito.when(transactionResultRepository.findAll())
56 | .thenReturn(new ArrayList<>());
57 | Iterable result = transactionController.getAllResult();
58 | Assert.assertNotNull(result);
59 | Assert.assertEquals(0, Iterables.size(result));
60 | }
61 |
62 | @Test
63 | public void testGetAllEvent() {
64 | Mockito.when(transactionEventRepository.findAll()).thenReturn(new ArrayList<>());
65 | Iterable event = transactionController.getAllEvent();
66 | Assert.assertNotNull(event);
67 | Assert.assertEquals(0, Iterables.size(event));
68 | }
69 |
70 | @Test
71 | public void testGetEvent() {
72 | TransactionEvent transactionEvent = new TransactionEvent();
73 | Mockito.when(transactionEventRepository.findById(nullable(String.class)))
74 | .thenReturn(Optional.of(transactionEvent));
75 | TransactionEvent event = transactionController.getEvent("12345");
76 | Assert.assertNotNull(event);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/entity/AccountTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | public class AccountTest {
7 | @Test
8 | public void testGetSet(){
9 | Account account = new Account();
10 | account.setCurrency("USD");
11 | account.setAccountName("Hello");
12 | account.setAccountGroup("Settlement");
13 | account.setId("1234");
14 | account.setCreateTime(10000L);
15 | Assert.assertEquals("USD", account.getCurrency());
16 | Assert.assertEquals("Hello", account.getAccountName());
17 | Assert.assertEquals("Settlement", account.getAccountGroup());
18 | Assert.assertEquals("1234", account.getId());
19 | Assert.assertEquals(10000L, account.getCreateTime());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/entity/JournalEntryTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.math.BigDecimal;
7 |
8 | public class JournalEntryTest {
9 | @Test
10 | public void testGetSet(){
11 | JournalEntry journalEntry = new JournalEntry();
12 | journalEntry.setAmount(BigDecimal.TEN);
13 | journalEntry.setAccountId("1234");
14 | journalEntry.setCreateTime(10000L);
15 | journalEntry.setCurrency("USD");
16 | journalEntry.setId("12345");
17 | journalEntry.setRequestId("1234567");
18 | Assert.assertEquals(BigDecimal.TEN, journalEntry.getAmount());
19 | Assert.assertEquals("1234", journalEntry.getAccountId());
20 | Assert.assertEquals(10000L, journalEntry.getCreateTime());
21 | Assert.assertEquals("USD", journalEntry.getCurrency());
22 | Assert.assertEquals("12345", journalEntry.getId());
23 | Assert.assertEquals("1234567", journalEntry.getRequestId());
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/entity/TransactionEventTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | public class TransactionEventTest {
7 | @Test
8 | public void testGetSet() {
9 | TransactionEvent transactionEvent = new TransactionEvent();
10 | transactionEvent.setType("P2P");
11 | transactionEvent.setId("12345");
12 | transactionEvent.setRequest("{}");
13 | transactionEvent.setCreateTime(10000L);
14 | Assert.assertEquals("P2P", transactionEvent.getType());
15 | Assert.assertEquals("12345", transactionEvent.getId());
16 | Assert.assertEquals("{}", transactionEvent.getRequest());
17 | Assert.assertEquals(10000, transactionEvent.getCreateTime());
18 |
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/entity/TransactionResultTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | public class TransactionResultTest {
7 | @Test
8 | public void testGetSet(){
9 | TransactionResult transactionResult = new TransactionResult();
10 | transactionResult.setSuccess(true);
11 | transactionResult.setResponse("{}");
12 | transactionResult.setId("12345");
13 | transactionResult.setRequestId("444");
14 | transactionResult.setCreateTime(1999L);
15 | Assert.assertEquals("{}", transactionResult.getResponse());
16 | Assert.assertEquals("12345", transactionResult.getId());
17 | Assert.assertEquals("444", transactionResult.getRequestId());
18 | Assert.assertEquals(1999L, transactionResult.getCreateTime());
19 | Assert.assertTrue(transactionResult.isSuccess());
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/ledger/committer/impl/BaseCommitterTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.committer.impl;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
9 | import org.junit.Assert;
10 | import org.junit.Before;
11 | import org.junit.Rule;
12 | import org.junit.Test;
13 | import org.junit.rules.ExpectedException;
14 | import org.mockito.*;
15 |
16 | import java.math.BigDecimal;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 | import java.util.Optional;
20 |
21 | import static org.mockito.ArgumentMatchers.anyIterable;
22 | import static org.mockito.ArgumentMatchers.nullable;
23 |
24 | public class BaseCommitterTest {
25 | @Rule
26 | public ExpectedException expectedException = ExpectedException.none();
27 | @Mock
28 | private JournalEntryRepository journalEntryRepository;
29 | @Mock
30 | private AccountRepository accountRepository;
31 | @InjectMocks
32 | private BaseCommitter baseCommitter;
33 |
34 | @Before
35 | public void before() {
36 | MockitoAnnotations.initMocks(this);
37 | }
38 |
39 | @Test
40 | public void testCommit_unBalanced() {
41 | expectedException.expect(InvalidBusinessRuleException.class);
42 | expectedException.expectMessage("Unbalanced journal entries");
43 | List journalEntries = new ArrayList<>();
44 | JournalEntry journalEntry = new JournalEntry();
45 | journalEntry.setAmount(BigDecimal.valueOf(10));
46 | journalEntries.add(journalEntry);
47 | journalEntries.add(journalEntry);
48 | Mockito.when(journalEntryRepository.saveAll(anyIterable())).thenReturn(journalEntries);
49 | Iterable commit = baseCommitter.commit(journalEntries, 0 , 0L);
50 | }
51 |
52 | @Test
53 | public void testCommit_balanced() {
54 | Account account = new Account();
55 | account.setBalance(BigDecimal.ZERO);
56 | account.setId("12345");
57 | List journalEntries = new ArrayList<>();
58 | JournalEntry journalEntry = new JournalEntry();
59 | journalEntry.setAmount(BigDecimal.valueOf(10));
60 | journalEntry.setAccountId("12345");
61 | journalEntries.add(journalEntry);
62 | JournalEntry balancingJournalEntry = new JournalEntry();
63 | balancingJournalEntry.setAmount(BigDecimal.valueOf(-10));
64 | balancingJournalEntry.setAccountId("23456");
65 | journalEntries.add(balancingJournalEntry);
66 | Mockito.when(journalEntryRepository.saveAll(anyIterable())).thenReturn(journalEntries);
67 | Mockito.when(accountRepository.findById(nullable(String.class))).thenReturn(Optional.of(account));
68 | Mockito.when(accountRepository.save(nullable(Account.class))).thenReturn(account);
69 | Iterable commit = baseCommitter.commit(journalEntries, 0, 0L);
70 | Assert.assertNotNull(commit);
71 | Assert.assertEquals(2, Iterables.size(commit));
72 | }
73 |
74 | @Test
75 | public void testCommit_null() {
76 | Iterable commit = baseCommitter.commit(null, 0, 0L);
77 | Assert.assertNotNull(commit);
78 | Assert.assertEquals(0, Iterables.size(commit));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/ledger/validator/impl/P2PValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
7 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.memory.repository.JournalEntryRepository;
9 | import org.junit.Before;
10 | import org.junit.Rule;
11 | import org.junit.Test;
12 | import org.junit.rules.ExpectedException;
13 | import org.mockito.InjectMocks;
14 | import org.mockito.Mock;
15 | import org.mockito.Mockito;
16 | import org.mockito.MockitoAnnotations;
17 |
18 | import java.math.BigDecimal;
19 | import java.util.ArrayList;
20 | import java.util.List;
21 | import java.util.Optional;
22 |
23 | import static org.mockito.ArgumentMatchers.nullable;
24 |
25 | public class P2PValidatorTest {
26 |
27 | @Rule
28 | public ExpectedException thrown = ExpectedException.none();
29 | @Mock
30 | private AccountRepository accountRepository;
31 | @Mock
32 | private JournalEntryRepository journalEntryRepository;
33 | @InjectMocks
34 | private P2PValidator p2PValidator;
35 |
36 | @Before
37 | public void before() {
38 | MockitoAnnotations.initMocks(this);
39 | }
40 |
41 | @Test
42 | public void testValidate() {
43 | Account fromAccount = new Account();
44 | fromAccount.setCurrency("USD");
45 | fromAccount.setBalance(BigDecimal.valueOf(1000));
46 | Account toAccount = new Account();
47 | toAccount.setCurrency("USD");
48 | Account feeAccount = new Account();
49 | feeAccount.setCurrency("USD");
50 | Account taxAccount = new Account();
51 | taxAccount.setCurrency("USD");
52 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
53 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
54 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
55 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
56 | List journalEntries = new ArrayList<>();
57 | JournalEntry journalEntry = new JournalEntry();
58 | journalEntry.setAmount(BigDecimal.valueOf(-3));
59 | journalEntries.add(journalEntry);
60 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
61 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
62 | p2PValidator.validate("1234567", p2PTransaction);
63 | }
64 |
65 | @Test
66 | public void testValidate_notEnoughFund() {
67 | thrown.expect(InvalidBusinessRuleException.class);
68 | thrown.expectMessage("Not enough fund to P2P");
69 | Account fromAccount = new Account();
70 | fromAccount.setCurrency("USD");
71 | fromAccount.setBalance(BigDecimal.ZERO);
72 | Account toAccount = new Account();
73 | toAccount.setCurrency("USD");
74 | Account feeAccount = new Account();
75 | feeAccount.setCurrency("USD");
76 | Account taxAccount = new Account();
77 | taxAccount.setCurrency("USD");
78 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
79 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
80 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
81 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
82 | List journalEntries = new ArrayList<>();
83 | JournalEntry journalEntry = new JournalEntry();
84 | journalEntry.setAmount(BigDecimal.valueOf(-7));
85 | journalEntries.add(journalEntry);
86 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
87 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
88 | p2PValidator.validate("1234567", p2PTransaction);
89 | }
90 |
91 | @Test
92 | public void testValidate_notSupportCurrencyExchangeSource() {
93 | thrown.expect(InvalidBusinessRuleException.class);
94 | thrown.expectMessage("Currency exchange not supported for source account");
95 | Account fromAccount = new Account();
96 | fromAccount.setCurrency("JPY");
97 | Account toAccount = new Account();
98 | toAccount.setCurrency("USD");
99 | Account feeAccount = new Account();
100 | feeAccount.setCurrency("USD");
101 | Account taxAccount = new Account();
102 | taxAccount.setCurrency("USD");
103 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
104 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
105 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
106 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
107 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
108 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
109 | p2PValidator.validate("1234567", p2PTransaction);
110 | }
111 |
112 | @Test
113 | public void testValidate_notSupportCurrencyExchangeDestination() {
114 | thrown.expect(InvalidBusinessRuleException.class);
115 | thrown.expectMessage("Currency exchange not supported for destination account");
116 | Account fromAccount = new Account();
117 | fromAccount.setCurrency("USD");
118 | Account toAccount = new Account();
119 | toAccount.setCurrency("JPY");
120 | Account feeAccount = new Account();
121 | feeAccount.setCurrency("USD");
122 | Account taxAccount = new Account();
123 | taxAccount.setCurrency("USD");
124 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
125 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
126 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
127 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
128 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
129 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
130 | p2PValidator.validate("1234567", p2PTransaction);
131 | }
132 |
133 | @Test
134 | public void testValidate_notSupportCurrencyExchangeFee() {
135 | thrown.expect(InvalidBusinessRuleException.class);
136 | thrown.expectMessage("Currency exchange not supported for fee account");
137 | Account fromAccount = new Account();
138 | fromAccount.setCurrency("USD");
139 | Account toAccount = new Account();
140 | toAccount.setCurrency("USD");
141 | Account feeAccount = new Account();
142 | feeAccount.setCurrency("JPY");
143 | Account taxAccount = new Account();
144 | taxAccount.setCurrency("USD");
145 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
146 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
147 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
148 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
149 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
150 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
151 | p2PValidator.validate("1234567", p2PTransaction);
152 | }
153 |
154 | @Test
155 | public void testValidate_notSupportCurrencyExchangeTax() {
156 | thrown.expect(InvalidBusinessRuleException.class);
157 | thrown.expectMessage("Currency exchange not supported for tax account");
158 | Account fromAccount = new Account();
159 | fromAccount.setCurrency("USD");
160 | Account toAccount = new Account();
161 | toAccount.setCurrency("USD");
162 | Account feeAccount = new Account();
163 | feeAccount.setCurrency("USD");
164 | Account taxAccount = new Account();
165 | taxAccount.setCurrency("JPY");
166 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
167 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
168 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
169 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
170 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
171 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
172 | p2PValidator.validate("1234567", p2PTransaction);
173 | }
174 |
175 | @Test
176 | public void testValidate_negativeAmount() {
177 | thrown.expect(InvalidBusinessRuleException.class);
178 | thrown.expectMessage("Negative amount not supported");
179 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
180 | "23456", "34567", "45678", BigDecimal.valueOf(-100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
181 | p2PValidator.validate("1234567", p2PTransaction);
182 | }
183 |
184 | @Test
185 | public void testValidate_negativeFee() {
186 | thrown.expect(InvalidBusinessRuleException.class);
187 | thrown.expectMessage("Negative fee not supported");
188 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
189 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(-10), BigDecimal.valueOf(5));
190 | p2PValidator.validate("1234567", p2PTransaction);
191 | }
192 |
193 | @Test
194 | public void testValidate_negativeTax() {
195 | thrown.expect(InvalidBusinessRuleException.class);
196 | thrown.expectMessage("Negative tax not supported");
197 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
198 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(-5));
199 | p2PValidator.validate("1234567", p2PTransaction);
200 | }
201 |
202 | @Test
203 | public void testValidate_noSourceAccount() {
204 | thrown.expect(InvalidBusinessRuleException.class);
205 | thrown.expectMessage("Source account not found");
206 | Account toAccount = new Account();
207 | toAccount.setCurrency("USD");
208 | Account feeAccount = new Account();
209 | feeAccount.setCurrency("USD");
210 | Account taxAccount = new Account();
211 | taxAccount.setCurrency("USD");
212 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.empty());
213 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
214 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
215 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
216 | List journalEntries = new ArrayList<>();
217 | JournalEntry journalEntry = new JournalEntry();
218 | journalEntry.setAmount(BigDecimal.valueOf(-7));
219 | journalEntries.add(journalEntry);
220 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
221 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
222 | p2PValidator.validate("1234567", p2PTransaction);
223 | }
224 |
225 | @Test
226 | public void testValidate_noDestinationAccount() {
227 | thrown.expect(InvalidBusinessRuleException.class);
228 | thrown.expectMessage("Destination account not found");
229 | Account fromAccount = new Account();
230 | fromAccount.setCurrency("USD");
231 | Account feeAccount = new Account();
232 | feeAccount.setCurrency("USD");
233 | Account taxAccount = new Account();
234 | taxAccount.setCurrency("USD");
235 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
236 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.empty());
237 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
238 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
239 | List journalEntries = new ArrayList<>();
240 | JournalEntry journalEntry = new JournalEntry();
241 | journalEntry.setAmount(BigDecimal.valueOf(-7));
242 | journalEntries.add(journalEntry);
243 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
244 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
245 | p2PValidator.validate("1234567", p2PTransaction);
246 | }
247 |
248 | @Test
249 | public void testValidate_noFeeAccount() {
250 | thrown.expect(InvalidBusinessRuleException.class);
251 | thrown.expectMessage("Fee account not found");
252 | Account fromAccount = new Account();
253 | fromAccount.setCurrency("USD");
254 | Account toAccount = new Account();
255 | toAccount.setCurrency("USD");
256 | Account taxAccount = new Account();
257 | taxAccount.setCurrency("USD");
258 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
259 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
260 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.empty());
261 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
262 | List journalEntries = new ArrayList<>();
263 | JournalEntry journalEntry = new JournalEntry();
264 | journalEntry.setAmount(BigDecimal.valueOf(-7));
265 | journalEntries.add(journalEntry);
266 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
267 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
268 | p2PValidator.validate("1234567", p2PTransaction);
269 | }
270 |
271 | @Test
272 | public void testValidate_noTaxAccount() {
273 | thrown.expect(InvalidBusinessRuleException.class);
274 | thrown.expectMessage("Tax account not found");
275 | Account fromAccount = new Account();
276 | fromAccount.setCurrency("USD");
277 | Account toAccount = new Account();
278 | toAccount.setCurrency("USD");
279 | Account feeAccount = new Account();
280 | feeAccount.setCurrency("USD");
281 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
282 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
283 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
284 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.empty());
285 | List journalEntries = new ArrayList<>();
286 | JournalEntry journalEntry = new JournalEntry();
287 | journalEntry.setAmount(BigDecimal.valueOf(-7));
288 | journalEntries.add(journalEntry);
289 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
290 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
291 | p2PValidator.validate("1234567", p2PTransaction);
292 | }
293 |
294 | @Test
295 | public void testValidate_noAccountBalance() {
296 | thrown.expect(InvalidBusinessRuleException.class);
297 | thrown.expectMessage("Not enough fund to P2P");
298 | Account fromAccount = new Account();
299 | fromAccount.setCurrency("USD");
300 | fromAccount.setBalance(BigDecimal.ZERO);
301 | Account toAccount = new Account();
302 | toAccount.setCurrency("USD");
303 | Account feeAccount = new Account();
304 | feeAccount.setCurrency("USD");
305 | Account taxAccount = new Account();
306 | taxAccount.setCurrency("USD");
307 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
308 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
309 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
310 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
311 | List journalEntries = new ArrayList<>();
312 | JournalEntry journalEntry = new JournalEntry();
313 | journalEntry.setAmount(BigDecimal.valueOf(150));
314 | journalEntries.add(journalEntry);
315 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
316 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
317 | p2PValidator.validate("1234567", p2PTransaction);
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/ledger/validator/impl/TopUpValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
6 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
7 | import org.junit.Before;
8 | import org.junit.Rule;
9 | import org.junit.Test;
10 | import org.junit.rules.ExpectedException;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.Mockito;
14 | import org.mockito.MockitoAnnotations;
15 |
16 | import java.math.BigDecimal;
17 | import java.util.Optional;
18 |
19 | public class TopUpValidatorTest {
20 | @Rule
21 | public ExpectedException thrown = ExpectedException.none();
22 | @Mock
23 | private AccountRepository accountRepository;
24 | @InjectMocks
25 | private TopUpValidator topUpValidator;
26 |
27 | @Before
28 | public void before() {
29 | MockitoAnnotations.initMocks(this);
30 | }
31 |
32 | @Test
33 | public void testValidate(){
34 | Account topUpAccount = new Account();
35 | topUpAccount.setCurrency("USD");
36 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(topUpAccount));
37 | Account settlementAccount = new Account();
38 | settlementAccount.setCurrency("USD");
39 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.of(settlementAccount));
40 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(9999));
41 | topUpValidator.validate("1234567", topUpRequest);
42 | }
43 |
44 | @Test
45 | public void testValidate_noTopUpAccount(){
46 | thrown.expect(InvalidBusinessRuleException.class);
47 | thrown.expectMessage("Top-up account not found");
48 | Account topUpAccount = new Account();
49 | topUpAccount.setCurrency("USD");
50 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.empty());
51 | Account settlementAccount = new Account();
52 | settlementAccount.setCurrency("USD");
53 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.of(settlementAccount));
54 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(9999));
55 | topUpValidator.validate("1234567", topUpRequest);
56 | }
57 |
58 | @Test
59 | public void testValidate_noSettlementAccount(){
60 | thrown.expect(InvalidBusinessRuleException.class);
61 | thrown.expectMessage("Settlement account not found");
62 | Account topUpAccount = new Account();
63 | topUpAccount.setCurrency("USD");
64 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(topUpAccount));
65 | Account settlementAccount = new Account();
66 | settlementAccount.setCurrency("USD");
67 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.empty());
68 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(9999));
69 | topUpValidator.validate("1234567", topUpRequest);
70 | }
71 |
72 | @Test
73 | public void testValidate_negativeAmount(){
74 | thrown.expect(InvalidBusinessRuleException.class);
75 | thrown.expectMessage("Negative amount not supported");
76 | Account topUpAccount = new Account();
77 | topUpAccount.setCurrency("USD");
78 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(topUpAccount));
79 | Account settlementAccount = new Account();
80 | settlementAccount.setCurrency("USD");
81 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.of(settlementAccount));
82 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(-9999));
83 | topUpValidator.validate("1234567", topUpRequest);
84 | }
85 |
86 | @Test
87 | public void testValidate_exchangeTopUp(){
88 | thrown.expect(InvalidBusinessRuleException.class);
89 | thrown.expectMessage("Currency exchange not supported for top-up account");
90 | Account topUpAccount = new Account();
91 | topUpAccount.setCurrency("JPY");
92 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(topUpAccount));
93 | Account settlementAccount = new Account();
94 | settlementAccount.setCurrency("USD");
95 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.of(settlementAccount));
96 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(9999));
97 | topUpValidator.validate("1234567", topUpRequest);
98 | }
99 |
100 | @Test
101 | public void testValidate_exchangeSettlement(){
102 | thrown.expect(InvalidBusinessRuleException.class);
103 | thrown.expectMessage("Currency exchange not supported for settlement account");
104 | Account topUpAccount = new Account();
105 | topUpAccount.setCurrency("USD");
106 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(topUpAccount));
107 | Account settlementAccount = new Account();
108 | settlementAccount.setCurrency("JPY");
109 | Mockito.when(accountRepository.findById("67890")).thenReturn(Optional.of(settlementAccount));
110 | TopUpRequest topUpRequest = new TopUpRequest("USD", "12345", "67890", BigDecimal.valueOf(9999));
111 | topUpValidator.validate("1234567", topUpRequest);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/impl/P2PItemizableTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import org.junit.Assert;
6 | import org.junit.Test;
7 |
8 | import java.math.BigDecimal;
9 | import java.util.List;
10 |
11 | public class P2PItemizableTest {
12 | @Test
13 | public void testItemize() {
14 | Account fromAccount = new Account();
15 | Account toAccount = new Account();
16 | Account feeAccount = new Account();
17 | Account taxAccount = new Account();
18 | P2PItemizable p2PItemizable = new P2PItemizable(fromAccount, toAccount, feeAccount, taxAccount,
19 | BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5), "USD", "1234567");
20 | List itemize = p2PItemizable.itemize();
21 | Assert.assertNotNull(itemize);
22 | Assert.assertEquals(6, itemize.size());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/model/ledger/impl/TopUpItemizableTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import org.junit.Assert;
6 | import org.junit.Test;
7 |
8 | import java.math.BigDecimal;
9 | import java.util.List;
10 |
11 | public class TopUpItemizableTest {
12 | @Test
13 | public void testItemize(){
14 | Account topUpAccount = new Account();
15 | Account settlementAccount = new Account();
16 | TopUpItemizable topUpItemizable = new TopUpItemizable(topUpAccount, settlementAccount, BigDecimal.valueOf(100), "USD", "1234567");
17 | List itemize = topUpItemizable.itemize();
18 | Assert.assertNotNull(itemize);
19 | Assert.assertEquals(2, itemize.size());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/service/impl/AccountServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.CreateAccountRequest;
6 | import com.industrieit.ledger.clientledger.core.memory.repository.AccountRepository;
7 | import org.junit.Before;
8 | import org.junit.Rule;
9 | import org.junit.Test;
10 | import org.junit.rules.ExpectedException;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.Mockito;
14 | import org.mockito.MockitoAnnotations;
15 |
16 | import static org.mockito.ArgumentMatchers.nullable;
17 | import static org.mockito.Mockito.never;
18 |
19 | public class AccountServiceImplTest {
20 | @Rule
21 | public ExpectedException thrown = ExpectedException.none();
22 | @Mock
23 | private AccountRepository accountRepository;
24 |
25 | @InjectMocks
26 | private AccountServiceImpl accountService;
27 |
28 | @Before
29 | public void before() {
30 | MockitoAnnotations.initMocks(this);
31 | }
32 |
33 | @Test
34 | public void testCreateAccount() {
35 | Mockito.when(accountRepository.existsById(nullable(String.class))).thenReturn(false);
36 | accountService.createAccount(new CreateAccountRequest("1234567", "USD", "Andrew", "Customer"));
37 | Mockito.verify(accountRepository).save(nullable(Account.class));
38 | }
39 |
40 | @Test
41 | public void testCreateAccount_accountExist() {
42 | thrown.expect(InvalidBusinessRuleException.class);
43 | thrown.expectMessage("Account already existed");
44 | Mockito.when(accountRepository.existsById(nullable(String.class))).thenReturn(true);
45 | accountService.createAccount(new CreateAccountRequest("1234567", "USD", "Andrew", "Customer"));
46 | Mockito.verify(accountRepository, never()).save(nullable(Account.class));
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/service/impl/P2PServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.ledger.committer.Committer;
6 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
7 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.impl.P2PItemizable;
8 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.P2PRequest;
9 | import org.junit.Assert;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.InjectMocks;
13 | import org.mockito.Mock;
14 | import org.mockito.Mockito;
15 | import org.mockito.MockitoAnnotations;
16 |
17 | import java.math.BigDecimal;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | import static org.mockito.ArgumentMatchers.anyList;
22 | import static org.mockito.ArgumentMatchers.nullable;
23 |
24 | public class P2PServiceImplTest {
25 | @Mock
26 | private Validator p2PTransactionValidator;
27 | @Mock
28 | private Committer committer;
29 | @InjectMocks
30 | private P2PServiceImpl p2PService;
31 |
32 | @Before
33 | public void before() {
34 | MockitoAnnotations.initMocks(this);
35 | }
36 |
37 | @Test
38 | public void testJournal() {
39 | Account account = new Account();
40 | account.setId("1234567");
41 | Mockito.when(p2PTransactionValidator.validate(nullable(String.class), nullable(P2PRequest.class)))
42 | .thenReturn(new P2PItemizable(account, account, account, account, BigDecimal.TEN, BigDecimal.TEN, BigDecimal.TEN, "USD", "1234567"));
43 | List journalEntries = new ArrayList<>();
44 | JournalEntry journalEntry = new JournalEntry();
45 | journalEntry.setAmount(BigDecimal.valueOf(10));
46 | journalEntries.add(journalEntry);
47 | journalEntries.add(journalEntry);
48 | Mockito.when(committer.commit(anyList(), nullable(Integer.class), nullable(long.class))).thenReturn(journalEntries);
49 | Iterable journal = p2PService.journal("123", new P2PRequest("USD", "123456789", "34556903003",
50 | "67890", "89777",
51 | BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5)), 0, 0L);
52 | Assert.assertNotNull(journal);
53 | for (JournalEntry journalEntry1 : journal) {
54 | Assert.assertEquals(BigDecimal.valueOf(10), journalEntry1.getAmount());
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/service/impl/SnapshotServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.SnapshotRequest;
4 | import org.junit.Assert;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.mockito.InjectMocks;
8 | import org.mockito.MockitoAnnotations;
9 |
10 | public class SnapshotServiceImplTest {
11 |
12 | @InjectMocks
13 | private SnapshotServiceImpl snapshotService;
14 |
15 | @Before
16 | public void before() {
17 | MockitoAnnotations.initMocks(this);
18 | }
19 |
20 | @Test
21 | public void testSnapshot() {
22 | long snapshot = snapshotService.snapshot(new SnapshotRequest());
23 | Assert.assertNotEquals(0L, snapshot);
24 |
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/memory/service/impl/TopUpServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.memory.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.memory.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.memory.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.memory.ledger.committer.Committer;
6 | import com.industrieit.ledger.clientledger.core.memory.ledger.validator.Validator;
7 | import com.industrieit.ledger.clientledger.core.memory.model.ledger.impl.TopUpItemizable;
8 | import com.industrieit.ledger.clientledger.core.memory.model.request.impl.TopUpRequest;
9 | import org.junit.Assert;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.InjectMocks;
13 | import org.mockito.Mock;
14 | import org.mockito.Mockito;
15 | import org.mockito.MockitoAnnotations;
16 |
17 | import java.math.BigDecimal;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | import static org.mockito.ArgumentMatchers.anyList;
22 | import static org.mockito.ArgumentMatchers.nullable;
23 |
24 | public class TopUpServiceImplTest {
25 | @Mock
26 | private Validator validator;
27 | @Mock
28 | private Committer committer;
29 | @InjectMocks
30 | private TopUpServiceImpl topUpService;
31 |
32 | @Before
33 | public void before() {
34 | MockitoAnnotations.initMocks(this);
35 | }
36 |
37 | @Test
38 | public void testJournal(){
39 | List journalEntries = new ArrayList<>();
40 | JournalEntry journalEntry = new JournalEntry();
41 | journalEntry.setAmount(BigDecimal.valueOf(10));
42 | journalEntries.add(journalEntry);
43 | journalEntries.add(journalEntry);
44 | Account account = new Account();
45 | account.setId("1234567");
46 | Mockito.when(validator.validate(nullable(String.class), nullable(TopUpRequest.class)))
47 | .thenReturn(new TopUpItemizable(account, account, BigDecimal.TEN, null, "1234567"));
48 | Mockito.when(committer.commit(anyList(), nullable(Integer.class), nullable(long.class))).thenReturn(journalEntries);
49 | TopUpRequest topUpRequest = new TopUpRequest();
50 | Iterable journal = topUpService.journal("123", topUpRequest, 0, 0L);
51 | Assert.assertNotNull(journal);
52 | for (JournalEntry journalEntry1 : journal) {
53 | Assert.assertEquals(BigDecimal.valueOf(10), journalEntry1.getAmount());
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------