├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── Dockerfile
├── README.md
├── database
├── schema_and_data.sql
└── show_result.sql
├── docker-compose.yml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── industrieit
│ │ └── ledger
│ │ └── clientledger
│ │ └── core
│ │ └── db
│ │ ├── 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
│ │ └── service
│ │ ├── AccountService.java
│ │ ├── JournalService.java
│ │ └── impl
│ │ ├── AccountServiceImpl.java
│ │ ├── P2PServiceImpl.java
│ │ └── TopUpServiceImpl.java
└── resources
│ ├── application-docker.properties
│ └── application.properties
└── test
└── java
└── com
└── industrieit
└── ledger
└── clientledger
└── core
└── db
├── 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
└── 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-db/482e33ed3873361526d18102e51f1e73875b8440/.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 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:11
2 | RUN apt-get update && apt-get install -y maven
3 | COPY . /project
4 | RUN cd /project && mvn package
5 | EXPOSE 8080
6 | ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=docker", "/project/target/client-ledger-core-db-1.0.0-SNAPSHOT.jar"]
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # client-ledger-core-db: MariaDB-based consumer to demonstrate competing consumer in event sourcing
2 | ## What does it do?
3 | client-ledger-core-db is Part 2 of 3 Part demo of event sourcing over common web/mobile architecture with commoditized software. This code base relies on MariaDB, which must be installed before running this demo.
4 |
5 | ## Quick Start
6 | ### Step 1
7 | Log in MariaDB to show the database command line tool.
8 | ```
9 | mysql -u username -p password
10 | ```
11 |
12 | ### Step 2
13 | Go to the database script folder.
14 | ```
15 | cd database
16 | ls
17 | ```
18 | Now you can see the two scripts:
19 | * schema_and_data.sql: This is for setting up the tables and sourcing empty states.
20 | * show_result.sql: This is for showing the state during event sourcing, and printing out the log as well.
21 |
22 | ### Step 3
23 | In the database command line tool, input
24 | ```
25 | source {Path}/schema_and_data.sql
26 | ```
27 | Then you can start the spring boot app.
28 |
29 | ### Step 4
30 | During event sourcing, in the database command line tool, input repeatedly
31 | ```
32 | source {Path}/show_result.sql
33 | ```
34 | to show the result of states up to the high water mark.
35 |
36 |
--------------------------------------------------------------------------------
/database/schema_and_data.sql:
--------------------------------------------------------------------------------
1 | -- SQL Version: MariaDB 10.3
2 |
3 | -- Setup default DB user and privileges
4 | GRANT ALL ON ledger.* TO 'sample_user'@'localhost' IDENTIFIED BY '19283746';
5 |
6 | SET FOREIGN_KEY_CHECKS = 0;
7 | SET GLOBAL FOREIGN_KEY_CHECKS = 0;
8 |
9 | -- Create ledger schema and all tables
10 | DROP SCHEMA IF EXISTS ledger;
11 | CREATE SCHEMA IF NOT EXISTS ledger DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
12 |
13 | USE ledger;
14 |
15 | CREATE TABLE IF NOT EXISTS ledger.`account`
16 | (
17 | `id` VARCHAR(36) NOT NULL,
18 | `account_name` VARCHAR(360) NOT NULL,
19 | `account_group` VARCHAR(360) NOT NULL,
20 | `createTime` TIMESTAMP(3) NOT NULL,
21 | `currency` VARCHAR(10) NOT NULL,
22 | `balance` DECIMAL(20, 5) NOT NULL,
23 | `kafkaOffset` BIGINT NOT NULL,
24 | `kafkaPartition` INTEGER NOT NULL,
25 | PRIMARY KEY (`id`)
26 | )
27 | ENGINE = InnoDB;
28 |
29 |
30 | CREATE TABLE IF NOT EXISTS ledger.journal_entry
31 | (
32 | `id` VARCHAR(36) NOT NULL,
33 | `requestId` VARCHAR(36) NOT NULL,
34 | `accountId` VARCHAR(36) NOT NULL,
35 | `amount` DECIMAL(20, 5) NOT NULL,
36 | `currency` VARCHAR(10) NOT NULL,
37 | `createTime` TIMESTAMP(3) NOT NULL,
38 | PRIMARY KEY (`id`),
39 | CONSTRAINT `fk_journal_entry_log_account`
40 | FOREIGN KEY (`accountId`)
41 | REFERENCES ledger.`account` (`id`)
42 | ON DELETE NO ACTION
43 | ON UPDATE NO ACTION
44 | )
45 | ENGINE = InnoDB;
46 |
47 |
48 | CREATE TABLE IF NOT EXISTS ledger.`transaction_event`
49 | (
50 | `id` VARCHAR(36) NOT NULL,
51 | `type` VARCHAR(36) NOT NULL,
52 | `request` VARCHAR(3600) NOT NULL,
53 | `createTime` TIMESTAMP(3) NOT NULL,
54 | `kafkaOffset` BIGINT NOT NULL,
55 | `kafkaPartition` INTEGER NOT NULL,
56 |
57 | PRIMARY KEY (`id`)
58 | )
59 | ENGINE = InnoDB;
60 |
61 | CREATE TABLE IF NOT EXISTS ledger.`transaction_result`
62 | (
63 | `id` VARCHAR(36) NOT NULL,
64 | `requestId` VARCHAR(36) NOT NULL UNIQUE,
65 | `response` VARCHAR(3600) NOT NULL,
66 | `createTime` TIMESTAMP(3) NOT NULL,
67 | `success` BOOLEAN NOT NULL,
68 | `kafkaOffset` BIGINT NOT NULL,
69 | `kafkaPartition` INTEGER NOT NULL,
70 |
71 | PRIMARY KEY (`id`)
72 | )
73 | ENGINE = InnoDB;
74 |
75 | -- Create index to support faster read
76 | ALTER TABLE `journal_entry`
77 | ADD INDEX `index_journal_entry_on_requestId` (`requestId`);
78 | ALTER TABLE `journal_entry`
79 | ADD INDEX `index_journal_entry_on_accountId` (`accountId`);
80 | ALTER TABLE `journal_entry`
81 | ADD INDEX `index_journal_entry_on_createTime` (`createTime`);
82 | ALTER TABLE `transaction_result`
83 | ADD INDEX `index_transaction_result_on_requestId` (`requestId`);
84 |
85 |
86 |
--------------------------------------------------------------------------------
/database/show_result.sql:
--------------------------------------------------------------------------------
1 | -- show result of the ledger transaction processing
2 | use ledger;
3 | show tables;
4 |
5 | -- show mapping of each of the request event to its respective result, calculating the time elapsed in ms, and success/fail
6 | select result.requestId,
7 | event.createTime as eventTime,
8 | event.kafkaOffset as kafkaOffset,
9 | event.kafkaPartition as kafkaPartition,
10 | result.createTime as resultTime,
11 | format((result.createTime - event.createTime) * 1000, '#,###') as msElapsed,
12 | result.success,
13 | event.type
14 | from transaction_result result
15 | join transaction_event event on result.requestId like event.id
16 | order by result.createTime;
17 |
18 | -- show final account balances after snapshot
19 | select *
20 | from account;
21 |
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "2.1"
2 |
3 | networks:
4 | test:
5 | driver: bridge
6 |
7 | volumes:
8 | db-data:
9 |
10 | services:
11 | app:
12 | build:
13 | context: .
14 | dockerfile: Dockerfile
15 | ports:
16 | - "3001:3001"
17 | depends_on:
18 | db:
19 | condition: service_healthy
20 | kafka:
21 | condition: service_started
22 | networks:
23 | - test
24 |
25 | db:
26 | image: postgres:11
27 | ports:
28 | - "5432:5432"
29 | volumes:
30 | - db-data:/var/lib/postgresql/data
31 | environment:
32 | - POSTGRES_PASSWORD=pitest
33 | - POSTGRES_DB=ledger
34 | command: ["postgres", "-c", "shared_buffers=512MB", "-c", "max_connections=200"]
35 | healthcheck:
36 | test: ["CMD-SHELL", "pg_isready -U postgres"]
37 | interval: 10s
38 | timeout: 5s
39 | retries: 5
40 | networks:
41 | - test
42 |
43 | zookeeper:
44 | image: wurstmeister/zookeeper
45 | networks:
46 | - test
47 |
48 | kafka:
49 | image: wurstmeister/kafka
50 | hostname: kafka
51 | ports:
52 | - "9092:9092"
53 | environment:
54 | KAFKA_ADVERTISED_HOST_NAME: kafka
55 | ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
56 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
57 | depends_on:
58 | - zookeeper
59 | networks:
60 | - test
61 |
62 |
--------------------------------------------------------------------------------
/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-db
13 | 1.0.0-SNAPSHOT
14 | client-ledger-core-db
15 | Client Ledger Core by DB
16 |
17 |
18 |
19 |
20 | org.springframework.boot
21 | spring-boot-maven-plugin
22 |
23 |
24 |
25 | repackage
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 11
35 | ${java.version}
36 | ${java.version}
37 | 2.7.0
38 | 1.5.0
39 | 4.0.2
40 | 3.0.0
41 | 2.22.0
42 | 3.0.1
43 | 3.7.1
44 | 2.3
45 | 3.1.1
46 | 2.12.1
47 | 1.16
48 | 3.0.0
49 | 3.0.0
50 | 2.8.0
51 | 0.5.3
52 | 1.0.0
53 | 2.7.0
54 | 2.2.2.RELEASE
55 |
56 |
57 |
58 |
59 |
60 |
61 | com.google.guava
62 | guava
63 | 28.1-jre
64 |
65 |
66 | org.springframework.boot
67 | spring-boot-starter-aop
68 |
69 |
70 | org.springframework.boot
71 | spring-boot-starter-logging
72 |
73 |
74 | org.springframework.boot
75 | spring-boot-starter-web
76 |
77 |
78 | org.springframework.boot
79 | spring-boot-starter-data-jpa
80 |
81 |
82 | org.apache.tomcat
83 | tomcat-jdbc
84 |
85 |
86 |
87 |
88 | org.postgresql
89 | postgresql
90 |
91 |
92 | org.springframework.boot
93 | spring-boot-starter-test
94 | test
95 |
96 |
97 | org.springframework.kafka
98 | spring-kafka
99 |
100 |
101 | org.springframework.boot
102 | spring-boot-autoconfigure
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/Application.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import org.springframework.boot.CommandLineRunner;
6 | import org.springframework.boot.ExitCodeGenerator;
7 | import org.springframework.boot.SpringApplication;
8 | import org.springframework.boot.autoconfigure.SpringBootApplication;
9 |
10 | /**
11 | * Non-blocking, zero-locking client ledger which produces {@link Account}
12 | * and {@link JournalEntry} compliance with Accounting Standards, to be fed into
13 | * General Ledger for reporting purpose.
14 | * Run on Single Thread Processor behind a queued system. Support at least 100 high-level business transaction per second.
15 | * Support Atomic Business Transactions which can be itemized into any number of {@link JournalEntry},
16 | * but all the {@link JournalEntry} must sum up to zero.
17 | */
18 | @SpringBootApplication
19 | public class Application implements CommandLineRunner {
20 |
21 | @Override
22 | public void run(String... arg0) {
23 | if (arg0.length > 0 && arg0[0].equals("exitcode")) {
24 | throw new ExitException();
25 | }
26 | }
27 |
28 | public static void main(String[] args) {
29 | new SpringApplication(Application.class).run(args);
30 | }
31 |
32 | /**
33 | * Exit Exception on command line args of exitcode
34 | */
35 | public static class ExitException extends RuntimeException implements ExitCodeGenerator {
36 | private static final long serialVersionUID = 1L;
37 |
38 | @Override
39 | public int getExitCode() {
40 | return 10;
41 | }
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/config/LedgerConfig.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.config;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.db.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.db");
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, "kafka: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, "kafka:9092");
75 | config.put(ConsumerConfig.CLIENT_ID_CONFIG, "client_core_maria");
76 | config.put(ConsumerConfig.GROUP_ID_CONFIG, "group_core_maria");
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 |
83 | @Bean
84 | public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
85 | ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
86 | factory.setConcurrency(1);
87 | factory.setConsumerFactory(consumerFactory());
88 | return factory;
89 | }
90 |
91 | @Bean
92 | public Consumer consumer(){
93 | return consumerFactory().createConsumer();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/Consumer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/consumer/Processor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.db.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/db/consumer/Producer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionResult;
6 | import com.industrieit.ledger.clientledger.core.db.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 kafkaOffset
19 | * @param kafkaPartition
20 | */
21 | void produceError(String requestId, InvalidBusinessRuleException e, long kafkaOffset, Integer kafkaPartition);
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 kafkaOffset
29 | * @param kafkaPartition
30 | */
31 | @Transactional(isolation = Isolation.SERIALIZABLE)
32 | void produceSuccess(String requestId, T response, long kafkaOffset, Integer kafkaPartition);
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/BackUpProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
5 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
6 | import com.industrieit.ledger.clientledger.core.db.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 | public BackUpProcessor(Producer producer) {
14 | this.producer = producer;
15 | }
16 |
17 |
18 | @Override
19 | public void process(TransactionEvent transactionEvent) {
20 | producer.produceSuccess(transactionEvent.getId(), null,
21 | transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
22 | }
23 |
24 | @Override
25 | public String getType() {
26 | return Type.BACK_UP.toString();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/ConsumerImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Consumer;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
7 |
8 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionEventRepository;
9 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionResultRepository;
10 | import org.apache.kafka.clients.consumer.ConsumerRecord;
11 | import org.apache.kafka.clients.consumer.ConsumerRecords;
12 | import org.slf4j.Logger;
13 | import org.springframework.beans.factory.annotation.Autowired;
14 | import org.springframework.stereotype.Component;
15 |
16 | import javax.annotation.PostConstruct;
17 | import javax.annotation.PreDestroy;
18 | import java.io.IOException;
19 | import java.time.Duration;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import java.util.concurrent.ExecutorService;
23 |
24 | @Component
25 | public class ConsumerImpl implements Consumer, Runnable {
26 | private final List processors;
27 | private final ObjectMapper objectMapper;
28 | private final TransactionEventRepository transactionEventRepository;
29 | private final TransactionResultRepository transactionResultRepository;
30 | private final org.apache.kafka.clients.consumer.Consumer kafkaConsumer;
31 | private final ExecutorService executorService;
32 | private final Logger logger;
33 | public static final String TOPIC = "Transaction_Event";
34 | boolean running = true;
35 |
36 | @Autowired
37 | public ConsumerImpl(List processors, ObjectMapper objectMapper,
38 | TransactionEventRepository transactionEventRepository,
39 | TransactionResultRepository transactionResultRepository,
40 | org.apache.kafka.clients.consumer.Consumer kafkaConsumer,
41 | ExecutorService executorService, Logger logger) {
42 | this.processors = processors;
43 | this.objectMapper = objectMapper;
44 | this.transactionEventRepository = transactionEventRepository;
45 | this.transactionResultRepository = transactionResultRepository;
46 | this.kafkaConsumer = kafkaConsumer;
47 | this.executorService = executorService;
48 | this.logger = logger;
49 | }
50 |
51 | @PostConstruct
52 | public void init() {
53 | executorService.submit(this);
54 | }
55 |
56 | @PreDestroy
57 | public void destroy() {
58 | kafkaConsumer.unsubscribe();
59 | executorService.shutdown();
60 | }
61 |
62 | @Override
63 | public void run() {
64 | kafkaConsumer.subscribe(Collections.singleton(TOPIC));
65 | while (running) {
66 | ConsumerRecords poll = kafkaConsumer.poll(Duration.ofDays(1000));
67 | for (ConsumerRecord consumerRecord : poll) {
68 | consume(consumerRecord);
69 | logger.info(consumerRecord.value());
70 | }
71 | kafkaConsumer.commitSync();
72 | }
73 | }
74 |
75 | @Override
76 | public void consume(ConsumerRecord consumerRecord) {
77 | TransactionEvent transactionEvent;
78 | try {
79 | transactionEvent = objectMapper.readValue(consumerRecord.value(), TransactionEvent.class);
80 | } catch (IOException e) {
81 | return;
82 | }
83 | if (!transactionEventRepository.existsById(transactionEvent.getId()) || !transactionResultRepository.existsByRequestId(transactionEvent.getId()) ) {
84 | transactionEvent.setKafkaOffset(consumerRecord.offset());
85 | transactionEvent.setKafkaPartition(consumerRecord.partition());
86 | TransactionEvent save = transactionEventRepository.save(transactionEvent);
87 | processors
88 | .stream()
89 | .filter(processor ->
90 | processor.getType().equals(transactionEvent.getType()))
91 | .findFirst()
92 | .ifPresent(processor ->
93 | processor.process(save));
94 | }
95 |
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/CreateAccountProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
6 |
7 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
8 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
9 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
10 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
11 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.CreateAccountRequest;
12 | import com.industrieit.ledger.clientledger.core.db.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.getKafkaOffset(), transactionEvent.getKafkaPartition());
41 | return;
42 | }
43 | try {
44 | createAccountAndProduce(requestId, createAccountRequest, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
45 | } catch (InvalidBusinessRuleException e) {
46 | producer.produceError(requestId, e, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
47 | }
48 | }
49 |
50 | @Transactional(isolation = Isolation.SERIALIZABLE)
51 | public void createAccountAndProduce(String requestId, CreateAccountRequest createAccountRequest, long kafkaOffset, Integer kafkaPartition) {
52 | Account account = this.accountService.createAccount(createAccountRequest);
53 | producer.produceSuccess(requestId, account, kafkaOffset, kafkaPartition);
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/db/consumer/impl/P2PProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
6 |
7 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
8 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
9 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
10 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
11 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
12 | import com.industrieit.ledger.clientledger.core.db.service.JournalService;
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 P2PProcessor implements Processor {
21 | private final ObjectMapper objectMapper;
22 | private final JournalService p2PService;
23 | private final Producer producer;
24 |
25 |
26 | public P2PProcessor(ObjectMapper objectMapper,
27 | JournalService p2PService, Producer producer) {
28 | this.objectMapper = objectMapper;
29 | this.p2PService = p2PService;
30 | this.producer = producer;
31 | }
32 |
33 | public void process(TransactionEvent transactionEvent) {
34 | String requestId = transactionEvent.getId();
35 |
36 | P2PRequest p2PRequest;
37 | try {
38 | p2PRequest = objectMapper.readValue(transactionEvent.getRequest(), P2PRequest.class);
39 | } catch (IOException e) {
40 | producer.produceError(requestId, new InvalidBusinessRuleException("Malformed request"),
41 | transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
42 | return;
43 | }
44 | try {
45 | journalAndProduce(requestId, p2PRequest, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
46 | } catch (InvalidBusinessRuleException e) {
47 | producer.produceError(requestId, e, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
48 | }
49 | }
50 |
51 | @Transactional(isolation = Isolation.SERIALIZABLE)
52 | public void journalAndProduce(String requestId, P2PRequest p2PRequest, long kafkaOffset, Integer kafkaPartition) {
53 | Iterable transactionLogs = this.p2PService.journal(requestId, p2PRequest, kafkaOffset, kafkaPartition);
54 | producer.produceSuccess(requestId, transactionLogs, kafkaOffset, kafkaPartition);
55 | }
56 |
57 | @Override
58 | public String getType() {
59 | return Type.P2P.toString();
60 | }
61 |
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/ProducerImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionResult;
7 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.db.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.sql.Timestamp;
15 | import java.util.Date;
16 |
17 | @Component
18 | public class ProducerImpl implements Producer {
19 | private final TransactionResultRepository transactionResultRepository;
20 | private final ObjectMapper objectMapper;
21 | private final KafkaTemplate kafkaTemplate;
22 | private static final String TOPIC = "Transaction_Result_Maria";
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, long kafkaOffset, Integer kafkaPartition) {
33 | TransactionResult transactionResult = prepareResult(requestId, kafkaOffset, kafkaPartition);
34 | if (e != null) {
35 | transactionResult.setResponse("{\"message\": \"" + e.getMessage() + "\"}");
36 | } else {
37 | transactionResult.setResponse("{\"message\": null}");
38 | }
39 | transactionResult.setSuccess(false);
40 | TransactionResult save = transactionResultRepository.save(transactionResult);
41 | kafkaTemplate.send(TOPIC, save);
42 |
43 | }
44 | private TransactionResult prepareResult(String requestId, long kafkaOffset, Integer kafkaPartition) {
45 | TransactionResult transactionResult = new TransactionResult();
46 | transactionResult.setRequestId(requestId);
47 | transactionResult.setKafkaOffset(kafkaOffset);
48 | transactionResult.setKafkaPartition(kafkaPartition);
49 | transactionResult.setCreateTime(new Timestamp(new Date().getTime()));
50 | return transactionResult;
51 | }
52 |
53 | @Transactional(isolation = Isolation.SERIALIZABLE)
54 | public void produceSuccess(String requestId, T response, long kafkaOffset, Integer kafkaPartition) {
55 | TransactionResult transactionResult = prepareResult(requestId, kafkaOffset, kafkaPartition);
56 | try {
57 | transactionResult.setResponse(objectMapper.writeValueAsString(response));
58 | } catch (JsonProcessingException e) {
59 | transactionResult.setResponse("{}");
60 | }
61 | transactionResult.setSuccess(true);
62 | TransactionResult save = transactionResultRepository.save(transactionResult);
63 | kafkaTemplate.send(TOPIC, save);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/TopUpProcessor.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
6 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
7 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
8 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
9 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
10 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
11 | import com.industrieit.ledger.clientledger.core.db.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"), transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
41 | return;
42 | }
43 |
44 | try {
45 | journalAndProduce(requestId, topUpRequest, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
46 | } catch (InvalidBusinessRuleException e) {
47 | producer.produceError(requestId, e, transactionEvent.getKafkaOffset(), transactionEvent.getKafkaPartition());
48 | }
49 | }
50 |
51 | @Transactional(isolation = Isolation.SERIALIZABLE)
52 | public void journalAndProduce(String requestId, TopUpRequest topUpRequest, long kafkaOffset, Integer kafkaPartition) {
53 | Iterable transactionLogs = this.topUpService.journal(requestId, topUpRequest, kafkaOffset, kafkaPartition);
54 | producer.produceSuccess(requestId, transactionLogs, kafkaOffset, kafkaPartition);
55 | }
56 |
57 | @Override
58 | public String getType() {
59 | return Type.TOP_UP.toString();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/controller/AccountController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.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/db/controller/JournalEntryController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.db.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/db/controller/TransactionController.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionResult;
5 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionEventRepository;
6 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionResultRepository;
7 | import org.springframework.web.bind.annotation.*;
8 |
9 | import java.util.Optional;
10 |
11 | /**
12 | * REST Controller which is exclusively allowed to POST on the Ledger through creating and enqueuing {@link TransactionEvent}
13 | */
14 | @RestController
15 | @RequestMapping("/transaction")
16 | public class TransactionController {
17 | private final TransactionEventRepository transactionEventRepository;
18 | private final TransactionResultRepository transactionResultRepository;
19 |
20 | public TransactionController(TransactionEventRepository transactionEventRepository, TransactionResultRepository transactionResultRepository) {
21 | this.transactionEventRepository = transactionEventRepository;
22 | this.transactionResultRepository = transactionResultRepository;
23 | }
24 |
25 | /**
26 | * GET one {@link TransactionResult} based on request ID
27 | *
28 | * @param requestId ID which uniquely identifies the {@link TransactionResult}
29 | * @return {@link TransactionResult}
30 | */
31 | @GetMapping(value = "/result/event/{requestId}",
32 | produces = {"application/json"},
33 | consumes = {"application/json"})
34 | @ResponseBody
35 | public TransactionResult getResult(@PathVariable String requestId) {
36 | Optional byId = transactionResultRepository.findByRequestId(requestId);
37 | return byId.orElse(null);
38 | }
39 |
40 | @GetMapping(value = "/result/current",
41 | produces = {"application/json"},
42 | consumes = {"application/json"})
43 | @ResponseBody
44 | public TransactionResult getLastResult() {
45 | Optional byId = transactionResultRepository.findTop1ByOrderByKafkaOffsetDesc();
46 | return byId.orElse(null);
47 | }
48 |
49 | /**
50 | * GET all {@link TransactionResult} produced by the Ledger
51 | *
52 | * @return all {@link TransactionResult}
53 | */
54 | @GetMapping(value = "/result",
55 | produces = {"application/json"},
56 | consumes = {"application/json"})
57 | @ResponseBody
58 | public Iterable getAllResult() {
59 | return transactionResultRepository.findAll();
60 | }
61 |
62 | /**
63 | * GET one {@link TransactionEvent} based on request ID
64 | *
65 | * @param id ID which uniquely identifies the {@link TransactionEvent}
66 | * @return {@link TransactionEvent}
67 | */
68 | @GetMapping(value = "/event/{id}",
69 | produces = {"application/json"},
70 | consumes = {"application/json"})
71 | @ResponseBody
72 | public TransactionEvent getEvent(@PathVariable String id) {
73 | Optional byId = transactionEventRepository.findById(id);
74 | return byId.orElse(null);
75 | }
76 |
77 | @GetMapping(value = "/event/current",
78 | produces = {"application/json"},
79 | consumes = {"application/json"})
80 | @ResponseBody
81 | public TransactionEvent getLastEvent() {
82 | Optional byId = transactionEventRepository.findTop1ByOrderByKafkaOffsetDesc();
83 | return byId.orElse(null);
84 | }
85 |
86 | /**
87 | * GET all {@link TransactionEvent} enqueued for the Ledger
88 | *
89 | * @return all {@link TransactionEvent}
90 | */
91 | @GetMapping(value = "/event",
92 | produces = {"application/json"},
93 | consumes = {"application/json"})
94 | @ResponseBody
95 | public Iterable getAllEvent() {
96 | return transactionEventRepository.findAll();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/entity/Account.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import org.hibernate.annotations.CreationTimestamp;
5 |
6 | import javax.persistence.*;
7 | import java.math.BigDecimal;
8 | import java.sql.Timestamp;
9 |
10 | /**
11 | * One of the only TWO building block for ledger, apart from {@link JournalEntry}
12 | * Represents the places where debit and credit goes
13 | * All functionality and transaction are provided by adding a new {@link Account}
14 | * {@link Account} can be grouped into assets, liabilities, profit and loss.
15 | * Equally, {@link Account} can represent settlement, customer, bank, internal, tax, and fee.
16 | */
17 | @Entity
18 | @Table(name = "account")
19 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
20 | public class Account {
21 | @Id
22 | @Column(name = "id")
23 | private String id;
24 |
25 | @CreationTimestamp
26 | private Timestamp createTime;
27 |
28 | private String currency;
29 |
30 | @Column(name = "account_name")
31 | private String accountName;
32 |
33 | @Column(name = "account_group")
34 | private String accountGroup;
35 |
36 | private BigDecimal balance;
37 |
38 | private Long kafkaOffset = 0L;
39 | private Integer kafkaPartition = 0;
40 |
41 |
42 | /**
43 | * @return Account Name which provides useful info for the user and accountant, e.g., "Andrew's customer account"
44 | */
45 | public String getAccountName() {
46 | return accountName;
47 | }
48 |
49 | public void setAccountName(String accountName) {
50 | this.accountName = accountName;
51 | }
52 |
53 | /**
54 | * @return Account Group which provides handy useful grouping for the account, e.g., "Settlement", "Customer", "Fee", Tax"
55 | * Mainly used for business rule validation
56 | * But actual grouping of the accounts under different financial statements are out of scope of any ledgers
57 | * And shall be done by the user application of the ledger (e.g., accounting software, banking module)
58 | */
59 | public String getAccountGroup() {
60 | return accountGroup;
61 | }
62 |
63 | public void setAccountGroup(String accountGroup) {
64 | this.accountGroup = accountGroup;
65 | }
66 |
67 | /**
68 | * @return unique identifier of the account
69 | */
70 | public String getId() {
71 | return id;
72 | }
73 |
74 | public void setId(String id) {
75 | this.id = id;
76 | }
77 |
78 | /**
79 | * @return creation time of the Account
80 | */
81 | public Timestamp getCreateTime() {
82 | return createTime;
83 | }
84 |
85 | public void setCreateTime(Timestamp createTime) {
86 | this.createTime = createTime;
87 | }
88 |
89 | /**
90 | * @return currency code, e.g., "USD", "JPY", "HKD"
91 | * allow for business rule validation
92 | * but actually not necessary if validation can be done out of ledger
93 | */
94 | public String getCurrency() {
95 | return currency;
96 | }
97 |
98 | public void setCurrency(String currency) {
99 | this.currency = currency;
100 | }
101 |
102 | public BigDecimal getBalance() {
103 | return balance;
104 | }
105 |
106 | public void setBalance(BigDecimal balance) {
107 | this.balance = balance;
108 | }
109 |
110 | public Long getKafkaOffset() {
111 | return kafkaOffset;
112 | }
113 |
114 | public void setKafkaOffset(Long kafkaOffset) {
115 | this.kafkaOffset = kafkaOffset;
116 | }
117 |
118 | public Integer getKafkaPartition() {
119 | return kafkaPartition;
120 | }
121 |
122 | public void setKafkaPartition(Integer kafkaPartition) {
123 | this.kafkaPartition = kafkaPartition;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/entity/JournalEntry.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import org.hibernate.annotations.CreationTimestamp;
5 | import org.hibernate.annotations.GenericGenerator;
6 |
7 | import javax.persistence.*;
8 | import java.math.BigDecimal;
9 | import java.sql.Timestamp;
10 |
11 | /**
12 | * One of the only TWO building block for ledger, apart from {@link Account}
13 | * Represents the only allowable actions to be applied onto any {@link Account}
14 | * Summing up all {@link JournalEntry} for all {@link Account} in the ledger will always amount to ZERO.
15 | */
16 | @Entity
17 | @Table(name = "journal_entry")
18 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
19 | public class JournalEntry {
20 | @Id
21 | @Column(name = "id")
22 | @GeneratedValue(generator = "uuid2")
23 | @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
24 | private String id;
25 |
26 | private String requestId;
27 |
28 | /**
29 | * @return request ID which drives the posting of this journal entry, used for debug and tracing
30 | */
31 | public String getRequestId() {
32 | return requestId;
33 | }
34 |
35 | public void setRequestId(String requestId) {
36 | this.requestId = requestId;
37 | }
38 |
39 |
40 | @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
41 | @JoinColumn(name = "accountId")
42 | private Account account;
43 |
44 | private String currency;
45 |
46 | /**
47 | * @return currency code which the journal entry is denominated in, not necessary but just handy info.
48 | * Because currency is determined by {@link Account}
49 | * Each {@link Account} shall have exactly one currency.
50 | * If a customer needs many currency, just create many {@link Account}
51 | */
52 | public String getCurrency() {
53 | return currency;
54 | }
55 |
56 | public void setCurrency(String currency) {
57 | this.currency = currency;
58 | }
59 |
60 | @CreationTimestamp
61 | private Timestamp createTime;
62 |
63 | private BigDecimal amount;
64 |
65 | /**
66 | * @return id for database persisting, not particularly useful
67 | */
68 | public String getId() {
69 | return id;
70 | }
71 |
72 | public void setId(String id) {
73 | this.id = id;
74 | }
75 |
76 | /**
77 | * @return {@link Account} which this journal entry is posted to
78 | */
79 | public Account getAccount() {
80 | return account;
81 | }
82 |
83 | public void setAccount(Account account) {
84 | this.account = account;
85 | }
86 |
87 | /**
88 | * @return create Time
89 | */
90 | public Timestamp getCreateTime() {
91 | return createTime;
92 | }
93 |
94 | public void setCreateTime(Timestamp createTime) {
95 | this.createTime = createTime;
96 | }
97 |
98 |
99 | /**
100 | * @return Amount of the journal entry. Can be either positive and negative.
101 | * amount of one atomic block of journal entries shall also sum up to zero.
102 | */
103 | public BigDecimal getAmount() {
104 | return amount;
105 | }
106 |
107 | public void setAmount(BigDecimal amount) {
108 | this.amount = amount;
109 | }
110 |
111 | public JournalEntry(Account account, String currency, BigDecimal amount, String requestId) {
112 | this.account = account;
113 | this.amount = amount;
114 | this.currency = currency;
115 | this.requestId = requestId;
116 | }
117 |
118 | public JournalEntry() {
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/entity/TransactionEvent.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Consumer;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
6 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
7 | import org.hibernate.annotations.CreationTimestamp;
8 |
9 | import javax.persistence.Column;
10 | import javax.persistence.Entity;
11 | import javax.persistence.Id;
12 | import javax.persistence.Table;
13 | import java.sql.Timestamp;
14 |
15 | /**
16 | * Entity which represents an accepted and enqueued high-level transaction, fully packed into a self-contained event
17 | * {@link TransactionEvent} can be consumed by {@link Consumer}
18 | * On consumption, exactly one {@link TransactionResult} will be produced and persisted.
19 | * The full enqueued list of {@link TransactionEvent}, in a strict serial order, will form the basis of Event Sourcing.
20 | * Event sourcing allows the full state of the ledger be replayed, on any platform and infrastructure, with any processors.
21 | * This allows in-memory processing and reliable recovery from crash.
22 | */
23 | @Entity
24 | @Table(name = "transaction_event")
25 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
26 | public class TransactionEvent {
27 | @Id
28 | @Column(name = "id")
29 | private String id;
30 | private String type;
31 | private String request;
32 | @CreationTimestamp
33 | private Timestamp createTime;
34 | private Long kafkaOffset = 0L;
35 | private Integer kafkaPartition = 0;
36 | /**
37 | * @return id which uniquely identify this transaction event.
38 | */
39 | public String getId() {
40 | return id;
41 | }
42 |
43 | public void setId(String id) {
44 | this.id = id;
45 | }
46 |
47 | /**
48 | * @return payload of the request, usually a JSON string
49 | */
50 | public String getRequest() {
51 | return request;
52 | }
53 |
54 | public void setRequest(String request) {
55 | this.request = request;
56 | }
57 |
58 | /**
59 | * @return create time of the event. Important for event sourcing, as the event must be strictly in serial order of this field.
60 | * Take as the actual creation of enqueuing, which forms the sequence of event sourcing.
61 | * Can be different from the sending time from the client.
62 | * Like an event sent at a later time point can happen to arrive at the queue earlier, due to multi-threading.
63 | * But sequence are in strict order after the point of queueing, forming the basis of event sourcing.
64 | */
65 | public Timestamp getCreateTime() {
66 | return createTime;
67 | }
68 |
69 | public void setCreateTime(Timestamp createTime) {
70 | this.createTime = createTime;
71 | }
72 |
73 | /**
74 | * @return type as defined in {@link Type} which calls for correct {@link Processor}
75 | */
76 | public String getType() {
77 | return type;
78 | }
79 |
80 | public void setType(String type) {
81 | this.type = type;
82 | }
83 |
84 | public Long getKafkaOffset() {
85 | return kafkaOffset;
86 | }
87 |
88 | public void setKafkaOffset(Long kafkaOffset) {
89 | this.kafkaOffset = kafkaOffset;
90 | }
91 |
92 | public Integer getKafkaPartition() {
93 | return kafkaPartition;
94 | }
95 |
96 | public void setKafkaPartition(Integer kafkaPartition) {
97 | this.kafkaPartition = kafkaPartition;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/entity/TransactionResult.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
5 | import com.industrieit.ledger.clientledger.core.db.consumer.Producer;
6 | import org.hibernate.annotations.CreationTimestamp;
7 | import org.hibernate.annotations.GenericGenerator;
8 |
9 | import javax.persistence.*;
10 | import java.sql.Timestamp;
11 |
12 | /**
13 | * Entity which is produced by {@link Producer} for exactly one {@link TransactionEvent} after consumption
14 | * Represents the result of processing of {@link TransactionEvent} by the {@link Processor}
15 | * Can be either success or failure.
16 | * In both cases, more info are available and packed into the Event.
17 | */
18 | @Entity
19 | @Table(name="transaction_result")
20 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
21 | public class TransactionResult {
22 | @Id
23 | @Column(name = "id")
24 | @GeneratedValue(generator = "uuid2")
25 | @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
26 | private String id;
27 | private String requestId;
28 | @Column(name = "response", length = 5000)
29 | private String response;
30 | @CreationTimestamp
31 | private Timestamp createTime;
32 | private long kafkaOffset = 0L;
33 | private Integer kafkaPartition = 0;
34 | private boolean success;
35 |
36 | /**
37 | * @return id, for database key, not particularly useful
38 | */
39 | public String getId() {
40 | return id;
41 | }
42 |
43 | public void setId(String id) {
44 | this.id = id;
45 | }
46 |
47 | /**
48 | * @return response which is usually a well-formed JSON string representing the result of processing of {@link TransactionEvent}
49 | */
50 | public String getResponse() {
51 | return response;
52 | }
53 |
54 | public void setResponse(String response) {
55 | this.response = response;
56 | }
57 |
58 | /**
59 | * @return request event id which originates this result
60 | */
61 | public String getRequestId() {
62 | return requestId;
63 | }
64 |
65 | public void setRequestId(String requestId) {
66 | this.requestId = requestId;
67 | }
68 |
69 | /**
70 | * @return create time. Form the sequence of the result queue.
71 | * the order shall be 100% identical with the order of the {@link TransactionEvent} queue
72 | */
73 | public Timestamp getCreateTime() {
74 | return createTime;
75 | }
76 |
77 | public void setCreateTime(Timestamp createTime) {
78 | this.createTime = createTime;
79 | }
80 |
81 | /**
82 | * @return whether the processing of {@link TransactionEvent} is successful or not.
83 | */
84 | public boolean isSuccess() {
85 | return success;
86 | }
87 |
88 | public void setSuccess(boolean success) {
89 | this.success = success;
90 | }
91 |
92 | public long getKafkaOffset() {
93 | return kafkaOffset;
94 | }
95 |
96 | public void setKafkaOffset(long kafkaOffset) {
97 | this.kafkaOffset = kafkaOffset;
98 | }
99 |
100 | public Integer getKafkaPartition() {
101 | return kafkaPartition;
102 | }
103 |
104 | public void setKafkaPartition(Integer kafkaPartition) {
105 | this.kafkaPartition = kafkaPartition;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/exception/InvalidBusinessRuleException.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.exception;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/ledger/committer/Committer.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.committer;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.db.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 kafkaOffset
19 | * @param kafkaPartition
20 | * @return successfully committed group of {@link JournalEntry}
21 | */
22 | Iterable commit(Iterable logsToCommit, Long kafkaOffset, Integer kafkaPartition);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/ledger/committer/impl/BaseCommitter.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.committer.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.db.ledger.committer.Committer;
7 | import com.industrieit.ledger.clientledger.core.db.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.db.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 |
22 | public BaseCommitter(JournalEntryRepository journalEntryRepository, AccountRepository accountRepository) {
23 | this.journalEntryRepository = journalEntryRepository;
24 | this.accountRepository = accountRepository;
25 | }
26 | @Transactional(isolation = Isolation.SERIALIZABLE)
27 | public Iterable commit(Iterable logsToCommit, Long kafkaOffset, Integer kafkaPartition) {
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 = journalEntry.getAccount();
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/db/ledger/validator/Validator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.validator;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
5 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Itemizable;
7 | import com.industrieit.ledger.clientledger.core.db.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/db/ledger/validator/impl/P2PValidator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
6 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Itemizable;
7 | import com.industrieit.ledger.clientledger.core.db.model.ledger.impl.P2PItemizable;
8 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
9 | import com.industrieit.ledger.clientledger.core.db.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 | Optional sourceAccountOptional = accountRepository.findById(request.getFromCustomerAccount());
38 | if (!sourceAccountOptional.isPresent()) {
39 | throw new InvalidBusinessRuleException("Source account not found");
40 | } else {
41 | if (!sourceAccountOptional.get().getCurrency().equals(request.getCurrency())) {
42 | throw new InvalidBusinessRuleException("Currency exchange not supported for source account");
43 | }
44 | }
45 | Optional destinationAccountOptional = accountRepository.findById(request.getToCustomerAccount());
46 | if (!destinationAccountOptional.isPresent()) {
47 | throw new InvalidBusinessRuleException("Destination account not found");
48 | } else {
49 | if (!destinationAccountOptional.get().getCurrency().equals(request.getCurrency())) {
50 | throw new InvalidBusinessRuleException("Currency exchange not supported for destination account");
51 | }
52 | }
53 | Optional feeAccountOptional = accountRepository.findById(request.getFeeAccount());
54 | if (!feeAccountOptional.isPresent()) {
55 | throw new InvalidBusinessRuleException("Fee account not found");
56 | } else {
57 | if (!feeAccountOptional.get().getCurrency().equals(request.getCurrency())) {
58 | throw new InvalidBusinessRuleException("Currency exchange not supported for fee account");
59 | }
60 | }
61 | Optional taxAccountOptional = accountRepository.findById(request.getTaxAccount());
62 | if (!taxAccountOptional.isPresent()) {
63 | throw new InvalidBusinessRuleException("Tax account not found");
64 | } else {
65 | if (!taxAccountOptional.get().getCurrency().equals(request.getCurrency())) {
66 | throw new InvalidBusinessRuleException("Currency exchange not supported for tax account");
67 | }
68 | }
69 | BigDecimal sourceFinalBalance = sourceAccountOptional.get().getBalance();
70 | BigDecimal totalFundNeeded = request.getAmount().add(request.getFee()).add(request.getTax());
71 | if (sourceFinalBalance.compareTo(totalFundNeeded) < 0) {
72 | throw new InvalidBusinessRuleException("Not enough fund to P2P");
73 | }
74 | return new P2PItemizable(sourceAccountOptional.get(), destinationAccountOptional.get(), feeAccountOptional.get(),
75 | taxAccountOptional.get(), request.getAmount(), request.getFee(), request.getTax(), request.getCurrency(), requestId);
76 | }
77 |
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/ledger/validator/impl/TopUpValidator.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Itemizable;
4 | import com.industrieit.ledger.clientledger.core.db.model.ledger.impl.TopUpItemizable;
5 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
6 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
7 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
9 | import com.industrieit.ledger.clientledger.core.db.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/db/model/ledger/Itemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.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/db/model/ledger/Type.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/model/ledger/impl/P2PItemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger.impl;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.db.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, currency, amount.negate(), getRequestId()));
40 | journalEntries.add(new JournalEntry(destination, currency, amount,getRequestId()));
41 | journalEntries.add(new JournalEntry(source, currency, fee.negate(), getRequestId()));
42 | journalEntries.add(new JournalEntry(feeAccount, currency, fee, getRequestId()));
43 | journalEntries.add(new JournalEntry(source, currency, tax.negate(), getRequestId()));
44 | journalEntries.add(new JournalEntry(taxAccount, 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/db/model/ledger/impl/TopUpItemizable.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.db.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, currency, amount.negate(), getRequestId()));
31 | journalEntries.add(new JournalEntry(topUp, 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/db/model/request/EventRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.request;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/model/request/impl/CreateAccountRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/model/request/impl/P2PRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/model/request/impl/SnapshotRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/model/request/impl/TopUpRequest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.request.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.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/db/repository/AccountRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import org.springframework.data.repository.CrudRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.Optional;
8 |
9 | @Repository
10 | public interface AccountRepository extends CrudRepository {
11 | @Override
12 | Optional findById(String s);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/repository/JournalEntryRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.repository;
2 |
3 |
4 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
6 | import org.springframework.data.repository.CrudRepository;
7 | import org.springframework.stereotype.Repository;
8 |
9 | import java.sql.Timestamp;
10 |
11 |
12 | @Repository
13 | public interface JournalEntryRepository extends CrudRepository {
14 | Iterable findAllByAccount(Account account);
15 |
16 | Iterable findAllByAccountAndCreateTimeGreaterThanEqual(Account account, Timestamp timestamp);
17 |
18 | Iterable findAllByAccountAndCreateTimeGreaterThanEqualAndCreateTimeLessThan(Account account, Timestamp currentTimestamp, Timestamp lastTimestamp);
19 |
20 | @Override
21 | Iterable saveAll(Iterable iterable);
22 |
23 | Iterable findAllByRequestId(String requestId);
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/repository/TransactionEventRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
4 | import org.springframework.data.repository.CrudRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.Optional;
8 |
9 | @Repository
10 | public interface TransactionEventRepository extends CrudRepository {
11 | Optional findTop1ByOrderByKafkaOffsetDesc();
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/repository/TransactionResultRepository.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.repository;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionResult;
4 | import org.springframework.data.repository.CrudRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.Optional;
8 |
9 | @Repository
10 | public interface TransactionResultRepository extends CrudRepository {
11 | Optional findByRequestId(String requestId);
12 |
13 | Optional findTop1ByOrderByKafkaOffsetDesc();
14 |
15 | boolean existsByRequestId(String requestId);
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.CreateAccountRequest;
4 | import com.industrieit.ledger.clientledger.core.db.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/db/service/JournalService.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
4 | import com.industrieit.ledger.clientledger.core.db.model.request.EventRequest;
5 | import com.industrieit.ledger.clientledger.core.db.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 kafkaOffset
17 | * @param kafkaPartition
18 | * @return the atomic group of {@link JournalEntry} committed to the ledger
19 | */
20 | Iterable journal(String requestId, T request, Long kafkaOffset, Integer kafkaPartition);
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/service/impl/AccountServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.CreateAccountRequest;
6 | import com.industrieit.ledger.clientledger.core.db.repository.AccountRepository;
7 | import com.industrieit.ledger.clientledger.core.db.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 | return save;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/service/impl/P2PServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.ledger.committer.Committer;
4 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
5 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Itemizable;
6 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
7 | import com.industrieit.ledger.clientledger.core.db.service.JournalService;
8 | import com.industrieit.ledger.clientledger.core.db.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, Long kafkaOffset, Integer kafkaPartition) {
26 | Itemizable itemizable = validator.validate(requestId, request);
27 | return committer.commit(itemizable.itemize(), kafkaOffset, kafkaPartition);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/industrieit/ledger/clientledger/core/db/service/impl/TopUpServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.ledger.committer.Committer;
4 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
5 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Itemizable;
6 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
7 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
8 | import com.industrieit.ledger.clientledger.core.db.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, Long kafkaOffset, Integer kafkaPartition) {
27 | Itemizable itemizable = validator.validate(requestId, request);
28 | return committer.commit(itemizable.itemize(), kafkaOffset, kafkaPartition);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/resources/application-docker.properties:
--------------------------------------------------------------------------------
1 | spring.datasource.url=jdbc:postgresql://db:5432/ledger
2 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=3001
2 | spring.datasource.url=jdbc:postgresql://localhost:5432/ledger
3 | spring.datasource.username=postgres
4 | spring.datasource.password=pitest
5 | logging.level.org.hibernate.type=ERROR
6 | spring.jpa.show-sql=false
7 | spring.jpa.properties.hibernate.show-sql=false
8 | spring.jpa.properties.hibernate.format-sql=false
9 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
10 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
11 | spring.jpa.hibernate.ddl-auto=update
12 |
13 | # Transaction data audit history configuration
14 | spring.jpa.properties.org.hibernate.envers.audit_table_suffix=_AUDIT
15 | spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
16 | spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
17 |
18 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/ApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | public class ApplicationTests {
7 |
8 | @Test
9 | public void testRun() {
10 | Application clientLedgerServiceApplication = new Application();
11 | clientLedgerServiceApplication.run();
12 | }
13 |
14 | @Test
15 | public void testRunWithParam() {
16 | try {
17 | Application clientLedgerServiceApplication = new Application();
18 | clientLedgerServiceApplication.run("exitcode");
19 | } catch (Application.ExitException e) {
20 | Assert.assertEquals(10, e.getExitCode());
21 | }
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/config/LedgerConfigTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.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/db/consumer/impl/ConsumerImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.consumer.Processor;
5 |
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionEventRepository;
8 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionResultRepository;
9 | import org.apache.kafka.clients.consumer.ConsumerRecord;
10 | import org.apache.kafka.clients.consumer.ConsumerRecords;
11 | import org.apache.kafka.common.TopicPartition;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.ArgumentMatchers;
15 | import org.mockito.Mock;
16 | import org.mockito.Mockito;
17 | import org.mockito.MockitoAnnotations;
18 | import org.slf4j.Logger;
19 |
20 | import java.io.IOException;
21 | import java.time.Duration;
22 | import java.util.Collections;
23 | import java.util.concurrent.ExecutorService;
24 |
25 | import static org.mockito.ArgumentMatchers.nullable;
26 |
27 | public class ConsumerImplTest {
28 | @Mock
29 | private Processor processor;
30 | @Mock
31 | private TransactionEventRepository transactionEventRepository;
32 | @Mock
33 | private TransactionResultRepository transactionResultRepository;
34 | @Mock
35 | private ExecutorService executorService;
36 | @Mock
37 | private ObjectMapper objectMapper;
38 | @Mock
39 | private Logger logger;
40 | @Mock
41 | private org.apache.kafka.clients.consumer.Consumer kafkaConsumer;
42 |
43 | private ConsumerImpl consumer;
44 |
45 | @Before
46 | public void before() {
47 | MockitoAnnotations.initMocks(this);
48 | consumer = new ConsumerImpl(Collections.singletonList(processor), objectMapper, transactionEventRepository, transactionResultRepository, kafkaConsumer, executorService, logger);
49 | Mockito.when(processor.getType()).thenReturn("P2P");
50 | }
51 |
52 | @Test
53 | public void testConsume_match() throws IOException {
54 | TransactionEvent transactionEvent = new TransactionEvent();
55 | transactionEvent.setId("abcd1234");
56 | transactionEvent.setRequest("{}");
57 | transactionEvent.setType("P2P");
58 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
59 | ConsumerRecord consumerRecord =
60 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
61 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
62 | consumer.consume(consumerRecord);
63 | Mockito.verify(processor).process(nullable(TransactionEvent.class));
64 | }
65 |
66 | @Test
67 | public void testConsume_noMatch() throws IOException {
68 | TransactionEvent transactionEvent = new TransactionEvent();
69 | transactionEvent.setId("abcd1234");
70 | transactionEvent.setRequest("{}");
71 | transactionEvent.setType("create-account");
72 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
73 | ConsumerRecord consumerRecord =
74 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
75 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
76 | consumer.consume(consumerRecord);
77 | Mockito.verify(processor, Mockito.never()).process(nullable(TransactionEvent.class));
78 | }
79 |
80 | @Test
81 | public void testConsume_InvalidBusinessRuleException() throws IOException {
82 | TransactionEvent transactionEvent = new TransactionEvent();
83 | transactionEvent.setId("abcd1234");
84 | transactionEvent.setRequest("{}");
85 | transactionEvent.setType("P2P");
86 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
87 | Mockito.when(transactionEventRepository.existsById(nullable(String.class))).thenReturn(false);
88 | ConsumerRecord consumerRecord =
89 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
90 | consumer.consume(consumerRecord);
91 | }
92 |
93 | @Test
94 | public void testInit() {
95 | Mockito.when(executorService.submit(nullable(Runnable.class))).thenReturn(null);
96 | consumer.init();
97 | }
98 |
99 | @Test
100 | public void testDestory() {
101 | Mockito.when(executorService.shutdownNow()).thenReturn(null);
102 | consumer.destroy();
103 | }
104 |
105 | @Test
106 | public void testRun_notRunning() {
107 | consumer.running = false;
108 | consumer.run();
109 | }
110 |
111 | @Test
112 | public void testRun_running() throws IOException {
113 | consumer.running = true;
114 | TransactionEvent transactionEvent = new TransactionEvent();
115 | transactionEvent.setId("abcd1234");
116 | transactionEvent.setRequest("{}");
117 | transactionEvent.setType("P2P");
118 | ConsumerRecord consumerRecord =
119 | new ConsumerRecord<>(ConsumerImpl.TOPIC, 0, 0, null, new ObjectMapper().writeValueAsString(transactionEvent));
120 | Mockito.when(kafkaConsumer.poll(nullable(Duration.class)))
121 | .thenReturn(new ConsumerRecords<>(Collections.singletonMap(new TopicPartition("hello", 0), Collections.singletonList(consumerRecord))));
122 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TransactionEvent.class))).thenReturn(transactionEvent);
123 | Thread t1 = new Thread(() -> {
124 | consumer.run();
125 | });
126 | t1.start();
127 | t1.interrupt();
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/CreateAccountProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
6 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
7 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
8 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.CreateAccountRequest;
9 | import com.industrieit.ledger.clientledger.core.db.service.AccountService;
10 | import org.junit.Assert;
11 | import org.junit.Before;
12 | import org.junit.Test;
13 | import org.mockito.*;
14 |
15 | import java.io.IOException;
16 |
17 | import static org.mockito.ArgumentMatchers.nullable;
18 |
19 | public class CreateAccountProcessorTest {
20 | @Mock
21 | private ObjectMapper objectMapper;
22 | @Mock
23 | private AccountService accountService;
24 | @Mock
25 | private ProducerImpl resultProcessor;
26 | @InjectMocks
27 | private CreateAccountProcessor createAccountProcessor;
28 |
29 | @Before
30 | public void before() {
31 | MockitoAnnotations.initMocks(this);
32 | }
33 |
34 | @Test
35 | public void testProcess() throws IOException {
36 | CreateAccountRequest createAccountRequest = new CreateAccountRequest("112233", "JPY", "test", "test-group");
37 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenReturn(createAccountRequest);
38 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenReturn(new Account());
39 | TransactionEvent transactionEvent = new TransactionEvent();
40 | transactionEvent.setId("abcd1234");
41 | transactionEvent.setRequest("{}");
42 | transactionEvent.setKafkaPartition(0);
43 | transactionEvent.setKafkaOffset(0L);
44 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
45 | createAccountProcessor.process(transactionEvent);
46 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
47 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
48 | }
49 |
50 | @Test
51 | public void testProcess_cannotRead() throws IOException {
52 | CreateAccountRequest createAccountRequest = new CreateAccountRequest();
53 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenThrow(new IOException());
54 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenReturn(new Account());
55 | TransactionEvent transactionEvent = new TransactionEvent();
56 | transactionEvent.setId("abcd1234");
57 | transactionEvent.setRequest("{}");
58 | transactionEvent.setKafkaPartition(0);
59 | transactionEvent.setKafkaOffset(0L);
60 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
61 | createAccountProcessor.process(transactionEvent);
62 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
63 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
64 | }
65 |
66 | @Test
67 | public void testProcess_serviceFail() throws IOException {
68 | CreateAccountRequest createAccountRequest = new CreateAccountRequest("112233", "JPY", "test", "test-group");
69 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(CreateAccountRequest.class))).thenReturn(createAccountRequest);
70 | Mockito.when(accountService.createAccount(nullable(CreateAccountRequest.class))).thenThrow(new InvalidBusinessRuleException("service fails"));
71 | TransactionEvent transactionEvent = new TransactionEvent();
72 | transactionEvent.setId("abcd1234");
73 | transactionEvent.setRequest("{}");
74 | transactionEvent.setKafkaPartition(0);
75 | transactionEvent.setKafkaOffset(0L);
76 | transactionEvent.setType(Type.CREATE_ACCOUNT.toString());
77 | createAccountProcessor.process(transactionEvent);
78 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
79 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
80 | }
81 |
82 | @Test
83 | public void testGetType() {
84 | Assert.assertEquals(Type.CREATE_ACCOUNT.toString(), createAccountProcessor.getType());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/P2PProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
9 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
10 | import com.industrieit.ledger.clientledger.core.db.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(long.class), nullable(Integer.class)))
43 | .thenReturn(journalEntries);
44 | TransactionEvent transactionEvent = new TransactionEvent();
45 | transactionEvent.setId("abcd1234");
46 | transactionEvent.setRequest("{}");
47 | transactionEvent.setKafkaPartition(0);
48 | transactionEvent.setKafkaOffset(0L);
49 | transactionEvent.setType(Type.P2P.toString());
50 | p2PProcessor.process(transactionEvent);
51 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
52 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
53 | }
54 |
55 | @Test
56 | public void testProcess_cannotRead() throws IOException {
57 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(P2PRequest.class))).thenThrow(new IOException());
58 | List journalEntries = new ArrayList<>();
59 | Mockito.when(p2PService.journal(nullable(String.class), nullable(P2PRequest.class), nullable(long.class), nullable(Integer.class)))
60 | .thenReturn(journalEntries);
61 | TransactionEvent transactionEvent = new TransactionEvent();
62 | transactionEvent.setId("abcd1234");
63 | transactionEvent.setRequest("{}");
64 | transactionEvent.setKafkaPartition(0);
65 | transactionEvent.setKafkaOffset(0L);
66 | transactionEvent.setType(Type.P2P.toString());
67 | p2PProcessor.process(transactionEvent);
68 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
69 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
70 | }
71 |
72 | @Test
73 | public void testProcess_serviceFail() throws IOException {
74 | P2PRequest p2PRequest = new P2PRequest();
75 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(P2PRequest.class))).thenReturn(p2PRequest);
76 | Mockito.when(p2PService.journal(nullable(String.class), nullable(P2PRequest.class), nullable(long.class), nullable(Integer.class)))
77 | .thenThrow(new InvalidBusinessRuleException("service fails"));
78 | TransactionEvent transactionEvent = new TransactionEvent();
79 | transactionEvent.setId("abcd1234");
80 | transactionEvent.setRequest("{}");
81 | transactionEvent.setKafkaPartition(0);
82 | transactionEvent.setKafkaOffset(0L);
83 | transactionEvent.setType(Type.P2P.toString());
84 | p2PProcessor.process(transactionEvent);
85 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
86 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
87 | }
88 |
89 | @Test
90 | public void testGetType() {
91 | Assert.assertEquals(Type.P2P.toString(), p2PProcessor.getType());
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/consumer/impl/ProducerImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.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.db.entity.TransactionResult;
8 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
9 | import com.industrieit.ledger.clientledger.core.db.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, 0);
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, 0);
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, 0);
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, 0);
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/db/consumer/impl/TopUpProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.consumer.impl;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
7 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
8 | import com.industrieit.ledger.clientledger.core.db.model.ledger.Type;
9 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
10 | import com.industrieit.ledger.clientledger.core.db.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(long.class), nullable(Integer.class)))
43 | .thenReturn(journalEntries);
44 | TransactionEvent transactionEvent = new TransactionEvent();
45 | transactionEvent.setId("abcd1234");
46 | transactionEvent.setRequest("{}");
47 | transactionEvent.setKafkaPartition(0);
48 | transactionEvent.setKafkaOffset(0L);
49 | transactionEvent.setType(Type.TOP_UP.toString());
50 | topUpProcessor.process(transactionEvent);
51 | Mockito.verify(resultProcessor).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
52 | Mockito.verify(resultProcessor, Mockito.never()).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
53 | }
54 |
55 | @Test
56 | public void testProcess_cannotRead() throws IOException {
57 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TopUpRequest.class))).thenThrow(new IOException());
58 | List journalEntries = new ArrayList<>();
59 | Mockito.when(topUpService.journal(nullable(String.class), nullable(TopUpRequest.class), nullable(long.class), nullable(Integer.class)))
60 | .thenReturn(journalEntries);
61 | TransactionEvent transactionEvent = new TransactionEvent();
62 | transactionEvent.setId("abcd1234");
63 | transactionEvent.setRequest("{}");
64 | transactionEvent.setKafkaPartition(0);
65 | transactionEvent.setKafkaOffset(0L);
66 | transactionEvent.setType(Type.TOP_UP.toString());
67 | topUpProcessor.process(transactionEvent);
68 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
69 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
70 | }
71 |
72 | @Test
73 | public void testProcess_serviceFail() throws IOException {
74 | TopUpRequest topUpRequest = new TopUpRequest();
75 | Mockito.when(objectMapper.readValue(nullable(String.class), ArgumentMatchers.eq(TopUpRequest.class))).thenReturn(topUpRequest);
76 | List journalEntries = new ArrayList<>();
77 | Mockito.when(topUpService.journal(nullable(String.class), nullable(TopUpRequest.class), nullable(long.class), nullable(Integer.class)))
78 | .thenThrow(new InvalidBusinessRuleException("service fails"));
79 | TransactionEvent transactionEvent = new TransactionEvent();
80 | transactionEvent.setId("abcd1234");
81 | transactionEvent.setRequest("{}");
82 | transactionEvent.setKafkaPartition(0);
83 | transactionEvent.setKafkaOffset(0L);
84 | transactionEvent.setType(Type.TOP_UP.toString());
85 | topUpProcessor.process(transactionEvent);
86 | Mockito.verify(resultProcessor, Mockito.never()).produceSuccess(nullable(String.class), nullable(Object.class), nullable(long.class), nullable(Integer.class));
87 | Mockito.verify(resultProcessor).produceError(nullable(String.class), nullable(InvalidBusinessRuleException.class), nullable(long.class), nullable(Integer.class));
88 | }
89 |
90 | @Test
91 | public void testGetType(){
92 | Assert.assertEquals(Type.TOP_UP.toString(), topUpProcessor.getType());
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/controller/AccountControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.db.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/db/controller/JournalEntryControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.db.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/db/controller/TransactionControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.controller;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.google.common.collect.Iterables;
5 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionEvent;
6 | import com.industrieit.ledger.clientledger.core.db.entity.TransactionResult;
7 | import com.industrieit.ledger.clientledger.core.db.repository.TransactionEventRepository;
8 | import com.industrieit.ledger.clientledger.core.db.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/db/entity/AccountTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.sql.Timestamp;
7 |
8 | public class AccountTest {
9 | @Test
10 | public void testGetSet(){
11 | Account account = new Account();
12 | account.setCurrency("USD");
13 | account.setAccountName("Hello");
14 | account.setAccountGroup("Settlement");
15 | account.setId("1234");
16 | account.setCreateTime(new Timestamp(10000));
17 | Assert.assertEquals("USD", account.getCurrency());
18 | Assert.assertEquals("Hello", account.getAccountName());
19 | Assert.assertEquals("Settlement", account.getAccountGroup());
20 | Assert.assertEquals("1234", account.getId());
21 | Assert.assertEquals(new Timestamp(10000), account.getCreateTime());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/entity/JournalEntryTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.math.BigDecimal;
7 | import java.sql.Timestamp;
8 |
9 | public class JournalEntryTest {
10 | @Test
11 | public void testGetSet(){
12 | JournalEntry journalEntry = new JournalEntry();
13 | journalEntry.setAmount(BigDecimal.TEN);
14 | journalEntry.setAccount(new Account());
15 | journalEntry.setCreateTime(new Timestamp(10000));
16 | journalEntry.setCurrency("USD");
17 | journalEntry.setId("12345");
18 | journalEntry.setRequestId("1234567");
19 | Assert.assertEquals(BigDecimal.TEN, journalEntry.getAmount());
20 | Assert.assertNotNull(journalEntry.getAccount());
21 | Assert.assertEquals(new Timestamp(10000), journalEntry.getCreateTime());
22 | Assert.assertEquals("USD", journalEntry.getCurrency());
23 | Assert.assertEquals("12345", journalEntry.getId());
24 | Assert.assertEquals("1234567", journalEntry.getRequestId());
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/entity/TransactionEventTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.sql.Timestamp;
7 |
8 | public class TransactionEventTest {
9 | @Test
10 | public void testGetSet() {
11 | TransactionEvent transactionEvent = new TransactionEvent();
12 | transactionEvent.setType("P2P");
13 | transactionEvent.setId("12345");
14 | transactionEvent.setRequest("{}");
15 | transactionEvent.setCreateTime(new Timestamp(10000));
16 | Assert.assertEquals("P2P", transactionEvent.getType());
17 | Assert.assertEquals("12345", transactionEvent.getId());
18 | Assert.assertEquals("{}", transactionEvent.getRequest());
19 | Assert.assertEquals(new Timestamp(10000), transactionEvent.getCreateTime());
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/entity/TransactionResultTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.entity;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.sql.Timestamp;
7 |
8 | public class TransactionResultTest {
9 | @Test
10 | public void testGetSet(){
11 | TransactionResult transactionResult = new TransactionResult();
12 | transactionResult.setSuccess(true);
13 | transactionResult.setResponse("{}");
14 | transactionResult.setId("12345");
15 | transactionResult.setRequestId("444");
16 | transactionResult.setCreateTime(new Timestamp(1999));
17 | Assert.assertEquals("{}", transactionResult.getResponse());
18 | Assert.assertEquals("12345", transactionResult.getId());
19 | Assert.assertEquals("444", transactionResult.getRequestId());
20 | Assert.assertEquals(new Timestamp(1999), transactionResult.getCreateTime());
21 | Assert.assertTrue(transactionResult.isSuccess());
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/ledger/committer/impl/BaseCommitterTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.committer.impl;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
5 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
6 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
7 | import com.industrieit.ledger.clientledger.core.db.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.db.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 |
20 | import static org.mockito.ArgumentMatchers.anyIterable;
21 | import static org.mockito.ArgumentMatchers.nullable;
22 |
23 | public class BaseCommitterTest {
24 | @Rule
25 | public ExpectedException expectedException = ExpectedException.none();
26 | @Mock
27 | private JournalEntryRepository journalEntryRepository;
28 | @Mock
29 | private AccountRepository accountRepository;
30 | @InjectMocks
31 | private BaseCommitter baseCommitter;
32 |
33 | @Before
34 | public void before() {
35 | MockitoAnnotations.initMocks(this);
36 | }
37 |
38 | @Test
39 | public void testCommit_unBalanced() {
40 | expectedException.expect(InvalidBusinessRuleException.class);
41 | expectedException.expectMessage("Unbalanced journal entries");
42 | List journalEntries = new ArrayList<>();
43 | JournalEntry journalEntry = new JournalEntry();
44 | journalEntry.setAmount(BigDecimal.valueOf(10));
45 | journalEntries.add(journalEntry);
46 | journalEntries.add(journalEntry);
47 | Mockito.when(journalEntryRepository.saveAll(anyIterable())).thenReturn(journalEntries);
48 | Iterable commit = baseCommitter.commit(journalEntries, 0L, 0);
49 | }
50 |
51 | @Test
52 | public void testCommit_balanced() {
53 | List journalEntries = new ArrayList<>();
54 | Account account = new Account();
55 | account.setBalance(BigDecimal.TEN);
56 | JournalEntry journalEntry = new JournalEntry();
57 | journalEntry.setAmount(BigDecimal.valueOf(10));
58 | journalEntry.setAccount(account);
59 | journalEntries.add(journalEntry);
60 | JournalEntry balancingJournalEntry = new JournalEntry();
61 | balancingJournalEntry.setAmount(BigDecimal.valueOf(-10));
62 | balancingJournalEntry.setAccount(account);
63 | journalEntries.add(balancingJournalEntry);
64 | Mockito.when(journalEntryRepository.saveAll(anyIterable())).thenReturn(journalEntries);
65 | Mockito.when(accountRepository.save(nullable(Account.class))).thenReturn(new Account());
66 | Iterable commit = baseCommitter.commit(journalEntries, 0L, 0);
67 | Assert.assertNotNull(commit);
68 | Assert.assertEquals(2, Iterables.size(commit));
69 | }
70 |
71 | @Test
72 | public void testCommit_null() {
73 | Iterable commit = baseCommitter.commit(null, 0L, 0);
74 | Assert.assertNotNull(commit);
75 | Assert.assertEquals(0, Iterables.size(commit));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/ledger/validator/impl/P2PValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
5 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
6 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
7 | import com.industrieit.ledger.clientledger.core.db.repository.AccountRepository;
8 | import com.industrieit.ledger.clientledger.core.db.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.sql.Timestamp;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 | import java.util.Optional;
23 |
24 | import static org.mockito.ArgumentMatchers.nullable;
25 |
26 | public class P2PValidatorTest {
27 |
28 | @Rule
29 | public ExpectedException thrown = ExpectedException.none();
30 | @Mock
31 | private AccountRepository accountRepository;
32 | @Mock
33 | private JournalEntryRepository journalEntryRepository;
34 |
35 | @InjectMocks
36 | private P2PValidator p2PValidator;
37 |
38 | @Before
39 | public void before() {
40 | MockitoAnnotations.initMocks(this);
41 | }
42 |
43 | @Test
44 | public void testValidate() {
45 | Account fromAccount = new Account();
46 | fromAccount.setCurrency("USD");
47 | fromAccount.setBalance(BigDecimal.valueOf(1000L));
48 | Account toAccount = new Account();
49 | toAccount.setCurrency("USD");
50 | Account feeAccount = new Account();
51 | feeAccount.setCurrency("USD");
52 | Account taxAccount = new Account();
53 | taxAccount.setCurrency("USD");
54 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
55 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
56 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
57 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
58 | List journalEntries = new ArrayList<>();
59 | JournalEntry journalEntry = new JournalEntry();
60 | journalEntry.setAmount(BigDecimal.valueOf(-3));
61 | journalEntries.add(journalEntry);
62 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
63 | .thenReturn(journalEntries);
64 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
65 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
66 | p2PValidator.validate("1234567", p2PTransaction);
67 | }
68 |
69 | @Test
70 | public void testValidate_notEnoughFund() {
71 | thrown.expect(InvalidBusinessRuleException.class);
72 | thrown.expectMessage("Not enough fund to P2P");
73 | Account fromAccount = new Account();
74 | fromAccount.setCurrency("USD");
75 | fromAccount.setBalance(BigDecimal.ZERO);
76 | Account toAccount = new Account();
77 | toAccount.setCurrency("USD");
78 | Account feeAccount = new Account();
79 | feeAccount.setCurrency("USD");
80 | Account taxAccount = new Account();
81 | taxAccount.setCurrency("USD");
82 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
83 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
84 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
85 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
86 | List journalEntries = new ArrayList<>();
87 | JournalEntry journalEntry = new JournalEntry();
88 | journalEntry.setAmount(BigDecimal.valueOf(-7));
89 | journalEntries.add(journalEntry);
90 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
91 | .thenReturn(journalEntries);
92 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
93 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
94 | p2PValidator.validate("1234567", p2PTransaction);
95 | }
96 |
97 | @Test
98 | public void testValidate_notSupportCurrencyExchangeSource() {
99 | thrown.expect(InvalidBusinessRuleException.class);
100 | thrown.expectMessage("Currency exchange not supported for source account");
101 | Account fromAccount = new Account();
102 | fromAccount.setCurrency("JPY");
103 | Account toAccount = new Account();
104 | toAccount.setCurrency("USD");
105 | Account feeAccount = new Account();
106 | feeAccount.setCurrency("USD");
107 | Account taxAccount = new Account();
108 | taxAccount.setCurrency("USD");
109 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
110 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
111 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
112 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
113 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
114 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
115 | p2PValidator.validate("1234567", p2PTransaction);
116 | }
117 |
118 | @Test
119 | public void testValidate_notSupportCurrencyExchangeDestination() {
120 | thrown.expect(InvalidBusinessRuleException.class);
121 | thrown.expectMessage("Currency exchange not supported for destination account");
122 | Account fromAccount = new Account();
123 | fromAccount.setCurrency("USD");
124 | Account toAccount = new Account();
125 | toAccount.setCurrency("JPY");
126 | Account feeAccount = new Account();
127 | feeAccount.setCurrency("USD");
128 | Account taxAccount = new Account();
129 | taxAccount.setCurrency("USD");
130 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
131 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
132 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
133 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
134 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
135 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
136 | p2PValidator.validate("1234567", p2PTransaction);
137 | }
138 |
139 | @Test
140 | public void testValidate_notSupportCurrencyExchangeFee() {
141 | thrown.expect(InvalidBusinessRuleException.class);
142 | thrown.expectMessage("Currency exchange not supported for fee account");
143 | Account fromAccount = new Account();
144 | fromAccount.setCurrency("USD");
145 | Account toAccount = new Account();
146 | toAccount.setCurrency("USD");
147 | Account feeAccount = new Account();
148 | feeAccount.setCurrency("JPY");
149 | Account taxAccount = new Account();
150 | taxAccount.setCurrency("USD");
151 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
152 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
153 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
154 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
155 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
156 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
157 | p2PValidator.validate("1234567", p2PTransaction);
158 | }
159 |
160 | @Test
161 | public void testValidate_notSupportCurrencyExchangeTax() {
162 | thrown.expect(InvalidBusinessRuleException.class);
163 | thrown.expectMessage("Currency exchange not supported for tax account");
164 | Account fromAccount = new Account();
165 | fromAccount.setCurrency("USD");
166 | Account toAccount = new Account();
167 | toAccount.setCurrency("USD");
168 | Account feeAccount = new Account();
169 | feeAccount.setCurrency("USD");
170 | Account taxAccount = new Account();
171 | taxAccount.setCurrency("JPY");
172 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
173 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
174 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
175 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
176 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
177 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
178 | p2PValidator.validate("1234567", p2PTransaction);
179 | }
180 |
181 | @Test
182 | public void testValidate_negativeAmount() {
183 | thrown.expect(InvalidBusinessRuleException.class);
184 | thrown.expectMessage("Negative amount not supported");
185 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
186 | "23456", "34567", "45678", BigDecimal.valueOf(-100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
187 | p2PValidator.validate("1234567", p2PTransaction);
188 | }
189 |
190 | @Test
191 | public void testValidate_negativeFee() {
192 | thrown.expect(InvalidBusinessRuleException.class);
193 | thrown.expectMessage("Negative fee not supported");
194 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
195 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(-10), BigDecimal.valueOf(5));
196 | p2PValidator.validate("1234567", p2PTransaction);
197 | }
198 |
199 | @Test
200 | public void testValidate_negativeTax() {
201 | thrown.expect(InvalidBusinessRuleException.class);
202 | thrown.expectMessage("Negative tax not supported");
203 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
204 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(-5));
205 | p2PValidator.validate("1234567", p2PTransaction);
206 | }
207 |
208 | @Test
209 | public void testValidate_noSourceAccount() {
210 | thrown.expect(InvalidBusinessRuleException.class);
211 | thrown.expectMessage("Source account not found");
212 | Account toAccount = new Account();
213 | toAccount.setCurrency("USD");
214 | Account feeAccount = new Account();
215 | feeAccount.setCurrency("USD");
216 | Account taxAccount = new Account();
217 | taxAccount.setCurrency("USD");
218 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.empty());
219 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
220 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
221 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
222 | List journalEntries = new ArrayList<>();
223 | JournalEntry journalEntry = new JournalEntry();
224 | journalEntry.setAmount(BigDecimal.valueOf(-7));
225 | journalEntries.add(journalEntry);
226 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
227 | .thenReturn(journalEntries);
228 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
229 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
230 | p2PValidator.validate("1234567", p2PTransaction);
231 | }
232 |
233 | @Test
234 | public void testValidate_noDestinationAccount() {
235 | thrown.expect(InvalidBusinessRuleException.class);
236 | thrown.expectMessage("Destination account not found");
237 | Account fromAccount = new Account();
238 | fromAccount.setCurrency("USD");
239 | Account feeAccount = new Account();
240 | feeAccount.setCurrency("USD");
241 | Account taxAccount = new Account();
242 | taxAccount.setCurrency("USD");
243 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
244 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.empty());
245 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
246 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
247 | List journalEntries = new ArrayList<>();
248 | JournalEntry journalEntry = new JournalEntry();
249 | journalEntry.setAmount(BigDecimal.valueOf(-7));
250 | journalEntries.add(journalEntry);
251 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
252 | .thenReturn(journalEntries);
253 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
254 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
255 | p2PValidator.validate("1234567", p2PTransaction);
256 | }
257 |
258 | @Test
259 | public void testValidate_noFeeAccount() {
260 | thrown.expect(InvalidBusinessRuleException.class);
261 | thrown.expectMessage("Fee account not found");
262 | Account fromAccount = new Account();
263 | fromAccount.setCurrency("USD");
264 | Account toAccount = new Account();
265 | toAccount.setCurrency("USD");
266 | Account taxAccount = new Account();
267 | taxAccount.setCurrency("USD");
268 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
269 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
270 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.empty());
271 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
272 | List journalEntries = new ArrayList<>();
273 | JournalEntry journalEntry = new JournalEntry();
274 | journalEntry.setAmount(BigDecimal.valueOf(-7));
275 | journalEntries.add(journalEntry);
276 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
277 | .thenReturn(journalEntries);
278 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
279 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
280 | p2PValidator.validate("1234567", p2PTransaction);
281 | }
282 |
283 | @Test
284 | public void testValidate_noTaxAccount() {
285 | thrown.expect(InvalidBusinessRuleException.class);
286 | thrown.expectMessage("Tax account not found");
287 | Account fromAccount = new Account();
288 | fromAccount.setCurrency("USD");
289 | Account toAccount = new Account();
290 | toAccount.setCurrency("USD");
291 | Account feeAccount = new Account();
292 | feeAccount.setCurrency("USD");
293 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
294 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
295 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
296 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.empty());
297 | List journalEntries = new ArrayList<>();
298 | JournalEntry journalEntry = new JournalEntry();
299 | journalEntry.setAmount(BigDecimal.valueOf(-7));
300 | journalEntries.add(journalEntry);
301 | Mockito.when(journalEntryRepository.findAllByAccountAndCreateTimeGreaterThanEqual(nullable(Account.class), nullable(Timestamp.class)))
302 | .thenReturn(journalEntries);
303 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
304 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
305 | p2PValidator.validate("1234567", p2PTransaction);
306 | }
307 |
308 | @Test
309 | public void testValidate_noAccountBalance() {
310 | thrown.expect(InvalidBusinessRuleException.class);
311 | thrown.expectMessage("Not enough fund to P2P");
312 | Account fromAccount = new Account();
313 | fromAccount.setCurrency("USD");
314 | fromAccount.setBalance(BigDecimal.TEN);
315 | Account toAccount = new Account();
316 | toAccount.setCurrency("USD");
317 | Account feeAccount = new Account();
318 | feeAccount.setCurrency("USD");
319 | Account taxAccount = new Account();
320 | taxAccount.setCurrency("USD");
321 | Mockito.when(accountRepository.findById("12345")).thenReturn(Optional.of(fromAccount));
322 | Mockito.when(accountRepository.findById("23456")).thenReturn(Optional.of(toAccount));
323 | Mockito.when(accountRepository.findById("34567")).thenReturn(Optional.of(feeAccount));
324 | Mockito.when(accountRepository.findById("45678")).thenReturn(Optional.of(taxAccount));
325 | List journalEntries = new ArrayList<>();
326 | JournalEntry journalEntry = new JournalEntry();
327 | journalEntry.setAmount(BigDecimal.valueOf(150));
328 | journalEntries.add(journalEntry);
329 | Mockito.when(journalEntryRepository.findAllByAccount(nullable(Account.class)))
330 | .thenReturn(journalEntries);
331 | P2PRequest p2PTransaction = new P2PRequest("USD", "12345",
332 | "23456", "34567", "45678", BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5));
333 | p2PValidator.validate("1234567", p2PTransaction);
334 | }
335 | }
336 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/ledger/validator/impl/TopUpValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.ledger.validator.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
6 | import com.industrieit.ledger.clientledger.core.db.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/db/model/ledger/impl/P2PItemizableTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.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/db/model/ledger/impl/TopUpItemizableTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.model.ledger.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.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/db/service/impl/AccountServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.Account;
4 | import com.industrieit.ledger.clientledger.core.db.exception.InvalidBusinessRuleException;
5 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.CreateAccountRequest;
6 | import com.industrieit.ledger.clientledger.core.db.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 | @InjectMocks
25 | private AccountServiceImpl accountService;
26 |
27 | @Before
28 | public void before() {
29 | MockitoAnnotations.initMocks(this);
30 | }
31 |
32 | @Test
33 | public void testCreateAccount() {
34 | Mockito.when(accountRepository.existsById(nullable(String.class))).thenReturn(false);
35 | accountService.createAccount(new CreateAccountRequest("1234567", "USD", "Andrew", "Customer"));
36 | Mockito.verify(accountRepository).save(nullable(Account.class));
37 | }
38 |
39 | @Test
40 | public void testCreateAccount_accountExist() {
41 | thrown.expect(InvalidBusinessRuleException.class);
42 | thrown.expectMessage("Account already existed");
43 | Mockito.when(accountRepository.existsById(nullable(String.class))).thenReturn(true);
44 | accountService.createAccount(new CreateAccountRequest("1234567", "USD", "Andrew", "Customer"));
45 | Mockito.verify(accountRepository, never()).save(nullable(Account.class));
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/service/impl/P2PServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
4 | import com.industrieit.ledger.clientledger.core.db.ledger.committer.Committer;
5 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
6 | import com.industrieit.ledger.clientledger.core.db.model.ledger.impl.P2PItemizable;
7 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.P2PRequest;
8 | import org.junit.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
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.ArrayList;
18 | import java.util.List;
19 |
20 | import static org.mockito.ArgumentMatchers.anyList;
21 | import static org.mockito.ArgumentMatchers.nullable;
22 |
23 | public class P2PServiceImplTest {
24 | @Mock
25 | private Validator p2PTransactionValidator;
26 | @Mock
27 | private Committer committer;
28 | @InjectMocks
29 | private P2PServiceImpl p2PService;
30 |
31 | @Before
32 | public void before() {
33 | MockitoAnnotations.initMocks(this);
34 | }
35 |
36 | @Test
37 | public void testJournal() {
38 | Mockito.when(p2PTransactionValidator.validate(nullable(String.class), nullable(P2PRequest.class)))
39 | .thenReturn(new P2PItemizable(null, null, null, null, BigDecimal.TEN, BigDecimal.TEN, BigDecimal.TEN, "USD", "1234567"));
40 | List journalEntries = new ArrayList<>();
41 | JournalEntry journalEntry = new JournalEntry();
42 | journalEntry.setAmount(BigDecimal.valueOf(10));
43 | journalEntries.add(journalEntry);
44 | journalEntries.add(journalEntry);
45 | Mockito.when(committer.commit(anyList(), nullable(long.class), nullable(Integer.class))).thenReturn(journalEntries);
46 | Iterable journal = p2PService.journal("123", new P2PRequest("USD", "123456789", "34556903003",
47 | "67890", "89777",
48 | BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(5)), 0L, 0);
49 | Assert.assertNotNull(journal);
50 | for (JournalEntry journalEntry1 : journal) {
51 | Assert.assertEquals(BigDecimal.valueOf(10), journalEntry1.getAmount());
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/com/industrieit/ledger/clientledger/core/db/service/impl/TopUpServiceImplTest.java:
--------------------------------------------------------------------------------
1 | package com.industrieit.ledger.clientledger.core.db.service.impl;
2 |
3 | import com.industrieit.ledger.clientledger.core.db.entity.JournalEntry;
4 | import com.industrieit.ledger.clientledger.core.db.ledger.committer.Committer;
5 | import com.industrieit.ledger.clientledger.core.db.ledger.validator.Validator;
6 | import com.industrieit.ledger.clientledger.core.db.model.ledger.impl.TopUpItemizable;
7 | import com.industrieit.ledger.clientledger.core.db.model.request.impl.TopUpRequest;
8 | import org.junit.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
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.ArrayList;
18 | import java.util.List;
19 |
20 | import static org.mockito.ArgumentMatchers.anyList;
21 | import static org.mockito.ArgumentMatchers.nullable;
22 |
23 | public class TopUpServiceImplTest {
24 | @Mock
25 | private Validator validator;
26 | @Mock
27 | private Committer committer;
28 | @InjectMocks
29 | private TopUpServiceImpl topUpService;
30 |
31 | @Before
32 | public void before() {
33 | MockitoAnnotations.initMocks(this);
34 | }
35 |
36 | @Test
37 | public void testJournal(){
38 | List journalEntries = new ArrayList<>();
39 | JournalEntry journalEntry = new JournalEntry();
40 | journalEntry.setAmount(BigDecimal.valueOf(10));
41 | journalEntries.add(journalEntry);
42 | journalEntries.add(journalEntry);
43 | Mockito.when(validator.validate(nullable(String.class), nullable(TopUpRequest.class)))
44 | .thenReturn(new TopUpItemizable(null, null, BigDecimal.TEN, null, "1234567"));
45 | Mockito.when(committer.commit(anyList(), nullable(long.class), nullable(Integer.class))).thenReturn(journalEntries);
46 | TopUpRequest topUpRequest = new TopUpRequest();
47 | Iterable journal = topUpService.journal("123", topUpRequest, 0L, 0);
48 | Assert.assertNotNull(journal);
49 | for (JournalEntry journalEntry1 : journal) {
50 | Assert.assertEquals(BigDecimal.valueOf(10), journalEntry1.getAmount());
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------