├── .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 | --------------------------------------------------------------------------------