├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .validationconfig ├── Dockerfile ├── LICENSE.md ├── docker-compose.yml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── readme.md └── src ├── main ├── java │ └── com │ │ └── bankofspring │ │ ├── BankOfSpringApplication.java │ │ ├── api │ │ ├── .gitkeep │ │ └── v1 │ │ │ ├── controller │ │ │ ├── AccountController.java │ │ │ └── CustomerController.java │ │ │ └── request │ │ │ ├── account │ │ │ ├── CreateAccountRequest.java │ │ │ ├── DepositRequest.java │ │ │ ├── TransferFundRequest.java │ │ │ └── WithdrawalRequest.java │ │ │ └── customer │ │ │ └── CreateCustomerRequest.java │ │ ├── configuration │ │ └── SwaggerConfig.java │ │ ├── domain │ │ ├── model │ │ │ ├── Account.java │ │ │ ├── BaseDomainObject.java │ │ │ ├── Branch.java │ │ │ └── Customer.java │ │ └── repository │ │ │ ├── AccountRepository.java │ │ │ ├── BranchRepository.java │ │ │ └── CustomerRepository.java │ │ ├── dto │ │ ├── AccountDto.java │ │ └── CustomerDto.java │ │ ├── exception │ │ ├── ApiError.java │ │ ├── BankException.java │ │ ├── DuplicateEntityException.java │ │ ├── EntityException.java │ │ ├── EntityNotFoundException.java │ │ └── handler │ │ │ └── RestExceptionHandler.java │ │ └── service │ │ ├── account │ │ ├── AccountService.java │ │ ├── AccountServiceImpl.java │ │ └── exception │ │ │ ├── AccountException.java │ │ │ └── InsufficientFundsException.java │ │ └── customer │ │ ├── CustomerService.java │ │ └── CustomerServiceImpl.java └── resources │ ├── application.properties │ ├── data-h2.sql │ └── images │ ├── BankOfSpring.png │ └── logo.png └── test └── java └── com └── bankofspring ├── BankOfSpringApplicationTests.java ├── BankOfSpringApplicationUnitTests.java ├── integrationtest ├── .gitkeep └── controller │ ├── AccountControllerTest.java │ └── CustomerControllerTest.java └── unittest ├── .gitkeep └── controller └── CustomerControllerMockMvcStandaloneTest.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 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ 25 | 26 | # User-specific stuff: 27 | .idea/**/workspace.xml 28 | .idea/**/tasks.xml 29 | .idea/dictionaries 30 | 31 | # Sensitive or high-churn files: 32 | .idea/**/dataSources/ 33 | .idea/**/dataSources.ids 34 | .idea/**/dataSources.xml 35 | .idea/**/dataSources.local.xml 36 | .idea/**/sqlDataSources.xml 37 | .idea/**/dynamic.xml 38 | .idea/**/uiDesigner.xml 39 | 40 | # Gradle: 41 | .idea/**/gradle.xml 42 | .idea/**/libraries 43 | 44 | # CMake 45 | cmake-build-debug/ 46 | cmake-build-release/ 47 | 48 | # Mongo Explorer plugin: 49 | .idea/**/mongoSettings.xml 50 | 51 | ## File-based project format: 52 | *.iws 53 | 54 | ## Plugin-specific files: 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | # Mac OS 75 | .DS_Store 76 | 77 | # Logs 78 | logs/ 79 | .log -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip 2 | -------------------------------------------------------------------------------- /.validationconfig: -------------------------------------------------------------------------------- 1 | PR_TITLE_REGEX=/((?:[a-z][a-z]+))(\d{3})(:)([a-z0-9])/i 2 | COMMIT_MESSAGE_REGEX=/(#)(#)((?:[a-z][a-z]+))(\d{3})(:)([a-z0-9])/i 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre-alpine 2 | WORKDIR /usr/spring/bank 3 | COPY ./target/bankofspring-0.0.1-SNAPSHOT.jar /usr/spring/bank 4 | EXPOSE 8080 5 | CMD ["java", "-jar", "bankofspring-0.0.1-SNAPSHOT.jar"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Systango Technologies] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | bankofspring: 4 | build: 5 | context: ./ 6 | dockerfile: Dockerfile 7 | ports: 8 | - '8080:8080' 9 | image: bankofspring 10 | mem_limit: "256m" 11 | cpu_count: 1 12 | cpu_percent: 50 13 | -------------------------------------------------------------------------------- /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 Migwn, 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 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /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 enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.systango 7 | bankofspring 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | BankOfSpring 12 | Production ready SpringBoot application with Txn management. 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.3.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-jpa 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-rest 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | org.springframework.data 46 | spring-data-rest-hal-browser 47 | 48 | 49 | 50 | com.h2database 51 | h2 52 | runtime 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-test 57 | test 58 | 59 | 60 | org.springframework.restdocs 61 | spring-restdocs-mockmvc 62 | test 63 | 64 | 65 | 66 | 67 | org.apache.commons 68 | commons-lang3 69 | 3.5 70 | 71 | 72 | org.projectlombok 73 | lombok 74 | true 75 | 76 | 77 | org.modelmapper 78 | modelmapper 79 | 2.0.0 80 | 81 | 82 | io.springfox 83 | springfox-swagger2 84 | 2.9.2 85 | 86 | 87 | io.springfox 88 | springfox-swagger-ui 89 | 2.9.2 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-maven-plugin 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![BankOfSpring](https://github.com/SystangoTechnologies/BankOfSpring/blob/master/src/main/resources/images/logo.png) 2 | 3 | ## BankOfSpring 4 | Production ready maven based Spring Boot starter kit application with example cases of handling transactions with Spring. 5 | 6 | ## Description 7 | Starter kit for booting up the development of a API oriented and transaction based spring Java server. It contains the best practices and latest tools that a spring boot developer should opt for in a fresh development. Since JPA is used, developers are free to opt for any SQL based DB engine for persistence (H2 has been used as an example with this project). The preferred IDE for development is IntelliJ which comes with a plethora of useful JAVA tools to support Spring Boot development, but developers are free to opt for Eclipse or STS as well. The focus in this project is solely upon the SpringBoot development with business cases involving transactions and writting proper unit and integration tests for them. 8 | 9 | ## Technology 10 | 11 | - **Spring Boot** - Server side framework 12 | - **JPA** - Entity framework 13 | - **Lombok** - Provides automated getter/setters 14 | - **Actuator** - Application insights on the fly 15 | - **Spring Security** - Spring's security layer 16 | - **Thymeleaf** - Template Engine 17 | - **Devtools** - Support Hot-Code Swapping with live browser reload 18 | - **JJWT** - JWT tokens for API authentication 19 | - **Swagger** - In-built swagger2 documentation support 20 | - **Docker** - Docker containers 21 | - **Junit** - Unit testing framework 22 | - **H2** - H2 database embedded version 23 | 24 | ## Application Structure 25 | 26 | ## Running the server locally 27 | The BankOfSpring application can be started using your favourite IDE and its run configuration support. If you are a terminal savvy, please use the following command - 28 | 29 | ```` 30 | mvn spring-boot:run 31 | ```` 32 | 33 | ## Docker 34 | BankOfSpring supports docker container out of the box. This boilerplate is meant to cater to both web based applications as well as scalable micro services written in Java. Please select one of the following two ways to use docker to build and run the application - 35 | 36 | **Dockerfile** 37 | 38 | To build a fresh image, use - 39 | ```` 40 | docker build -t bankofspring . 41 | ```` 42 | To run the new image, use - 43 | ```` 44 | docker run -p 8080:8080 bankofspring 45 | ```` 46 | 47 | **Docker-Compose** 48 | 49 | To build a fresh image, use - 50 | ```` 51 | docker-compose build 52 | ```` 53 | To run the new image, use - 54 | ```` 55 | docker-compose up 56 | ```` 57 | 58 | ## Swagger Documentation 59 | Swagger documentation is in-built in this starter-kit and can be accessed at the following URL - 60 | ```` 61 | http://:8080/swagger-ui.html 62 | ```` 63 | 64 | ## Unit test cases 65 | There are multiple unit test cases written to cover the different components of the application. However there is a global application test suite file _**BankOfSpringApplicationUnitTests.java**_ that combines all the test cases in a logical manner to create a complete suite. It can be run from command prompt using the following command - 66 | 67 | ```` 68 | mvn clean test -Dtest=BankOfSpringApplicationUnitTests 69 | ```` 70 | 71 | ## Integration test cases 72 | There are multiple integration test cases written to cover the different components of the application. However there is a global application test suite file _**BankOfSpringApplicationTests.java**_ that combines all the test cases in a logical manner to create a complete suite. It can be run from command prompt using the following command - 73 | 74 | ```` 75 | mvn clean test -Dtest=BankOfSpringApplicationTests 76 | ```` 77 | 78 | ## Contributors 79 | [Arpit Khandelwal](https://www.linkedin.com/in/arpitkhandelwal1984/) 80 | 81 | ## License 82 | This project is licensed under the terms of the MIT license. -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/BankOfSpringApplication.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring; 2 | 3 | import org.modelmapper.ModelMapper; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | @SpringBootApplication 9 | public class BankOfSpringApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(BankOfSpringApplication.class, args); 13 | } 14 | 15 | @Bean 16 | public ModelMapper modelMapper() { 17 | return new ModelMapper(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/src/main/java/com/bankofspring/api/.gitkeep -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.controller; 2 | 3 | import com.bankofspring.api.v1.request.account.CreateAccountRequest; 4 | import com.bankofspring.api.v1.request.account.DepositRequest; 5 | import com.bankofspring.api.v1.request.account.TransferFundRequest; 6 | import com.bankofspring.api.v1.request.account.WithdrawalRequest; 7 | import com.bankofspring.domain.model.Account; 8 | import com.bankofspring.dto.AccountDto; 9 | import com.bankofspring.exception.EntityException; 10 | import com.bankofspring.exception.EntityNotFoundException; 11 | import com.bankofspring.service.account.AccountService; 12 | import com.bankofspring.service.account.exception.InsufficientFundsException; 13 | import org.modelmapper.ModelMapper; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.validation.Valid; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | /** 23 | * Created by Arpit Khandelwal. 24 | */ 25 | @RestController 26 | @RequestMapping("/v1/account") 27 | public class AccountController { 28 | 29 | @Autowired 30 | private AccountService accountService; 31 | 32 | @Autowired 33 | private ModelMapper modelMapper; 34 | 35 | @GetMapping(value = "/") 36 | public List getAccounts() { 37 | List accounts = accountService.getAllAccounts(); 38 | if (accounts != null && !accounts.isEmpty()) { 39 | return accounts 40 | .stream() 41 | .map(account -> modelMapper.map(account, AccountDto.class)) 42 | .collect(Collectors.toList()); 43 | } 44 | return Collections.emptyList(); 45 | } 46 | 47 | @GetMapping(value = "/{accountNumber}") 48 | public AccountDto getAccountByNumber(@PathVariable("accountNumber") Long accountNumber) throws EntityNotFoundException { 49 | Account account = accountService.getAccount(accountNumber); 50 | if (account != null) { 51 | return modelMapper.map(account, AccountDto.class); 52 | } 53 | return null; 54 | } 55 | 56 | @PostMapping("/create") 57 | public AccountDto createAccount(@RequestBody @Valid CreateAccountRequest createAccountRequest) throws EntityException { 58 | AccountDto accountDto = modelMapper.map(createAccountRequest, AccountDto.class); 59 | Account account = accountService.createAccount(accountDto); 60 | if (account != null) { 61 | return modelMapper.map(account, AccountDto.class); 62 | } 63 | return null; 64 | } 65 | 66 | @PostMapping("/deposit") 67 | public AccountDto depositMoney(@RequestBody @Valid DepositRequest depositRequest) throws EntityNotFoundException { 68 | AccountDto accountDto = modelMapper.map(depositRequest, AccountDto.class); 69 | Account account = accountService.creditAmount(accountDto, depositRequest.getDepositAmt()); 70 | if (account != null) { 71 | return modelMapper.map(account, AccountDto.class); 72 | } 73 | return null; 74 | } 75 | 76 | @PostMapping("/withdraw") 77 | public AccountDto withdrawMoney(@RequestBody @Valid WithdrawalRequest withdrawalRequest) throws EntityNotFoundException, InsufficientFundsException { 78 | AccountDto accountDto = modelMapper.map(withdrawalRequest, AccountDto.class); 79 | Account account = accountService.debitAmount(accountDto, withdrawalRequest.getWithdrawlAmt()); 80 | if (account != null) { 81 | return modelMapper.map(account, AccountDto.class); 82 | } 83 | return null; 84 | } 85 | 86 | @PostMapping("/transfer") 87 | public List transferMoney(@RequestBody @Valid TransferFundRequest transferFundRequest) throws InsufficientFundsException, EntityNotFoundException { 88 | AccountDto debitAccountDto = new AccountDto().setAccountNumber(transferFundRequest.getDebitAccountNumber()); 89 | AccountDto creditAccountDto = new AccountDto().setAccountNumber(transferFundRequest.getCreditAccountNumber()); 90 | List accounts = accountService.transferFunds(debitAccountDto,creditAccountDto,transferFundRequest.getAmount()); 91 | return accounts 92 | .stream() 93 | .map(account -> modelMapper.map(account, AccountDto.class)) 94 | .collect(Collectors.toList()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.controller; 2 | 3 | import com.bankofspring.api.v1.request.customer.CreateCustomerRequest; 4 | import com.bankofspring.domain.model.Customer; 5 | import com.bankofspring.dto.CustomerDto; 6 | import com.bankofspring.exception.EntityException; 7 | import com.bankofspring.exception.EntityNotFoundException; 8 | import com.bankofspring.service.customer.CustomerService; 9 | import org.modelmapper.ModelMapper; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import javax.validation.Valid; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * Created by Arpit Khandelwal. 20 | */ 21 | @RestController 22 | @RequestMapping("/v1/customer") 23 | public class CustomerController { 24 | @Autowired 25 | private CustomerService customerService; 26 | 27 | @Autowired 28 | private ModelMapper mapper; 29 | 30 | @GetMapping(value = "/") 31 | public List getAllCustomers() { 32 | List customers = customerService.getAllCustomers(); 33 | if (customers != null && !customers.isEmpty()) { 34 | return customers 35 | .stream() 36 | .map(customer -> mapper.map(customer, CustomerDto.class)) 37 | .collect(Collectors.toList()); 38 | } 39 | return Collections.emptyList(); 40 | } 41 | 42 | @GetMapping(value = "/{ssn}") 43 | public CustomerDto getCustomer(@PathVariable("ssn") String ssn) throws EntityNotFoundException { 44 | Customer customer = customerService.getCustomer(ssn); 45 | if (customer != null) { 46 | return mapper.map(customer, CustomerDto.class); 47 | } 48 | return null; 49 | } 50 | 51 | @PostMapping("/create") 52 | public CustomerDto createCustomer(@RequestBody @Valid CreateCustomerRequest createCustomerRequest) throws EntityException { 53 | CustomerDto customerDto = mapper.map(createCustomerRequest, CustomerDto.class); 54 | Customer customer = customerService.createCustomer(customerDto); 55 | if (customer != null) { 56 | return mapper.map(customer, CustomerDto.class); 57 | } 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/request/account/CreateAccountRequest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.request.account; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.experimental.Accessors; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.math.BigDecimal; 12 | 13 | /** 14 | * Created by Arpit Khandelwal. 15 | */ 16 | @Getter 17 | @Setter 18 | @Accessors(chain = true) 19 | @NoArgsConstructor 20 | @JsonIgnoreProperties(ignoreUnknown = true) 21 | public class CreateAccountRequest { 22 | 23 | @NotNull(message = "{constraints.NotEmpty.message}") 24 | private Long customerId; 25 | 26 | @NotNull(message = "{constraints.NotEmpty.message}") 27 | private Account.AccountType type; 28 | 29 | @NotNull(message = "{constraints.NotEmpty.message}") 30 | private Long branchId; 31 | 32 | @NotNull(message = "{constraints.NotEmpty.message}") 33 | private BigDecimal balance; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/request/account/DepositRequest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.request.account; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import java.math.BigDecimal; 11 | 12 | /** 13 | * Created by Arpit Khandelwal. 14 | */ 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | @NoArgsConstructor 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | public class DepositRequest { 21 | @NotNull(message = "{constraints.NotEmpty.message}") 22 | private Long accountNumber; 23 | 24 | @NotNull(message = "{constraints.NotEmpty.message}") 25 | private BigDecimal depositAmt; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/request/account/TransferFundRequest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.request.account; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import java.math.BigDecimal; 11 | 12 | /** 13 | * Created by Arpit Khandelwal. 14 | */ 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | @NoArgsConstructor 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | public class TransferFundRequest { 21 | 22 | @NotNull(message = "{constraints.NotEmpty.message}") 23 | private Long debitAccountNumber; 24 | 25 | @NotNull(message = "{constraints.NotEmpty.message}") 26 | private Long creditAccountNumber; 27 | 28 | @NotNull(message = "{constraints.NotEmpty.message}") 29 | private BigDecimal amount; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/request/account/WithdrawalRequest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.request.account; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import java.math.BigDecimal; 11 | 12 | /** 13 | * Created by Arpit Khandelwal. 14 | */ 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | @NoArgsConstructor 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | public class WithdrawalRequest { 21 | @NotNull(message = "{constraints.NotEmpty.message}") 22 | private Long accountNumber; 23 | 24 | @NotNull(message = "{constraints.NotEmpty.message}") 25 | private BigDecimal withdrawlAmt; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/api/v1/request/customer/CreateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.api.v1.request.customer; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | 9 | import javax.validation.constraints.NotNull; 10 | 11 | /** 12 | * Created by Arpit Khandelwal. 13 | */ 14 | @Getter 15 | @Setter 16 | @Accessors(chain = true) 17 | @NoArgsConstructor 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class CreateCustomerRequest { 20 | @NotNull(message = "{constraints.NotEmpty.message}") 21 | private String name; 22 | 23 | @NotNull(message = "{constraints.NotEmpty.message}") 24 | private String ssn; 25 | 26 | private String address1; 27 | private String address2; 28 | private String city; 29 | private String contactNumber; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/configuration/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.ApiInfoBuilder; 6 | import springfox.documentation.builders.PathSelectors; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.service.ApiInfo; 9 | import springfox.documentation.service.Contact; 10 | import springfox.documentation.spi.DocumentationType; 11 | import springfox.documentation.spring.web.plugins.Docket; 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 13 | 14 | /** 15 | * Created by Arpit Khandelwal. 16 | */ 17 | @Configuration 18 | @EnableSwagger2 19 | public class SwaggerConfig { 20 | @Bean 21 | public Docket api() { 22 | return new Docket(DocumentationType.SWAGGER_2) 23 | .apiInfo(getApiInfo()) 24 | .select() 25 | .apis(RequestHandlerSelectors.basePackage("com.bankofspring.api.v1.controller")) 26 | .paths(PathSelectors.any()) 27 | .build(); 28 | } 29 | 30 | private ApiInfo getApiInfo() { 31 | Contact contact = new Contact("Arpit Khandelwal", "http://www.systango.com", "arpit@systango.com"); 32 | return new ApiInfoBuilder() 33 | .title("SpringBoot Bank") 34 | .description("Bank Api Definition") 35 | .version("1.0.0") 36 | .license("MIT") 37 | .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0") 38 | .contact(contact) 39 | .build(); 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/model/Account.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.model; 2 | 3 | import com.bankofspring.service.account.exception.InsufficientFundsException; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | import org.hibernate.annotations.OnDelete; 9 | import org.hibernate.annotations.OnDeleteAction; 10 | 11 | import javax.persistence.*; 12 | import java.math.BigDecimal; 13 | 14 | /** 15 | * Created by Arpit Khandelwal. 16 | */ 17 | @Setter 18 | @Getter 19 | @Accessors(chain = true) 20 | @NoArgsConstructor 21 | @Entity 22 | public class Account extends BaseDomainObject { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | private Long accountNumber; 27 | 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @JoinColumn(name = "branchId", nullable = false) 30 | @OnDelete(action = OnDeleteAction.CASCADE) 31 | private Branch coreBranch; 32 | 33 | @ManyToOne(fetch = FetchType.LAZY) 34 | @JoinColumn(name = "customerId", nullable = false) 35 | @OnDelete(action = OnDeleteAction.CASCADE) 36 | private Customer accountOwner; 37 | 38 | private BigDecimal balance; 39 | 40 | private AccountType type; 41 | 42 | /** 43 | * Debits the given amount from current account balance. 44 | * @param debitAmount 45 | * @throws InsufficientFundsException 46 | */ 47 | public void debit(BigDecimal debitAmount) throws InsufficientFundsException { 48 | if(this.balance.compareTo(debitAmount) >= 0){ 49 | this.balance = this.balance.subtract(debitAmount); 50 | return; 51 | } 52 | throw new InsufficientFundsException(this,debitAmount); 53 | } 54 | 55 | /** 56 | * Credits the given amount to current account balance 57 | * @param creditAmount 58 | */ 59 | public void credit(BigDecimal creditAmount) { 60 | this.balance = this.balance.add(creditAmount); 61 | } 62 | 63 | public enum AccountType { 64 | SAVINGS, CURRENT, LOAN; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/model/BaseDomainObject.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.model; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import org.hibernate.annotations.CreationTimestamp; 6 | import org.hibernate.annotations.UpdateTimestamp; 7 | 8 | import javax.persistence.MappedSuperclass; 9 | import java.io.Serializable; 10 | import java.util.Date; 11 | 12 | /** 13 | * Represents all common properties of standard domain objects 14 | * 15 | * @author Arpit Khandelwal 16 | */ 17 | @MappedSuperclass 18 | @Data 19 | @NoArgsConstructor 20 | public abstract class BaseDomainObject implements Serializable { 21 | 22 | @CreationTimestamp 23 | protected Date createTimestamp = new Date(); 24 | 25 | @UpdateTimestamp 26 | protected Date lastEditTimestamp; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/model/Branch.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.model; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.experimental.Accessors; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import java.math.BigDecimal; 13 | 14 | /** 15 | * Created by Arpit Khandelwal. 16 | */ 17 | @Getter 18 | @Setter 19 | @Accessors(chain = true) 20 | @NoArgsConstructor 21 | @Entity 22 | public class Branch extends BaseDomainObject { 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | private Long branchId; 26 | 27 | private String name; 28 | private String city; 29 | private BigDecimal assets; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/model/Customer.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | 7 | import javax.persistence.*; 8 | import java.util.Set; 9 | 10 | /** 11 | * Created by Arpit Khandelwal. 12 | */ 13 | @Getter 14 | @Setter 15 | @Accessors(chain = true) 16 | @Entity 17 | public class Customer extends BaseDomainObject { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | protected Long customerId; 22 | 23 | @Column(unique = true) 24 | private String ssn; 25 | 26 | @OneToMany(cascade = CascadeType.ALL, 27 | fetch = FetchType.LAZY, 28 | mappedBy = "accountOwner") 29 | private Set accounts; 30 | 31 | private String name; 32 | private String address1; 33 | private String address2; 34 | private String city; 35 | private String contactNumber; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.repository; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by Arpit Khandelwal. 9 | */ 10 | @Repository 11 | public interface AccountRepository extends CrudRepository { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/repository/BranchRepository.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.repository; 2 | 3 | import com.bankofspring.domain.model.Branch; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by Arpit Khandelwal. 9 | */ 10 | @Repository 11 | public interface BranchRepository extends CrudRepository { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/domain/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.domain.repository; 2 | 3 | import com.bankofspring.domain.model.Customer; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by Arpit Khandelwal. 9 | */ 10 | @Repository 11 | public interface CustomerRepository extends CrudRepository { 12 | Customer findBySsn(String ssn); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/dto/AccountDto.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.dto; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | import lombok.experimental.Accessors; 11 | 12 | import java.math.BigDecimal; 13 | 14 | /** 15 | * Created by Arpit Khandelwal. 16 | */ 17 | @Getter 18 | @Setter 19 | @Accessors(chain = true) 20 | @NoArgsConstructor 21 | @ToString 22 | @JsonInclude(value = JsonInclude.Include.NON_NULL) 23 | @JsonIgnoreProperties(ignoreUnknown = true) 24 | public class AccountDto { 25 | private Long accountNumber; 26 | private Long customerId; 27 | private Long branchId; 28 | private Account.AccountType type; 29 | private BigDecimal balance; 30 | private CustomerDto accountOwner; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/dto/CustomerDto.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | import lombok.experimental.Accessors; 10 | 11 | /** 12 | * Created by Arpit Khandelwal. 13 | */ 14 | @Getter 15 | @Setter 16 | @Accessors(chain = true) 17 | @NoArgsConstructor 18 | @ToString 19 | @JsonInclude(value = JsonInclude.Include.NON_NULL) 20 | @JsonIgnoreProperties(ignoreUnknown = true) 21 | public class CustomerDto { 22 | private Long customerId; 23 | private String ssn; 24 | private String name; 25 | private String address1; 26 | private String address2; 27 | private String city; 28 | private String contactNumber; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/ApiError.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonFormat; 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | import com.fasterxml.jackson.databind.DatabindContext; 7 | import com.fasterxml.jackson.databind.JavaType; 8 | import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; 9 | import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; 10 | import com.fasterxml.jackson.databind.type.SimpleType; 11 | import com.fasterxml.jackson.databind.type.TypeFactory; 12 | import lombok.AllArgsConstructor; 13 | import lombok.Data; 14 | import lombok.EqualsAndHashCode; 15 | import org.hibernate.validator.internal.engine.path.PathImpl; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.validation.FieldError; 18 | import org.springframework.validation.ObjectError; 19 | 20 | import javax.validation.ConstraintViolation; 21 | import java.io.IOException; 22 | import java.time.LocalDateTime; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Set; 26 | 27 | /** 28 | * Created by Arpit Khandelwal. 29 | */ 30 | @Data 31 | @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CUSTOM, property = "error", visible = true) 32 | @JsonTypeIdResolver(LowerCaseClassNameResolver.class) 33 | public class ApiError { 34 | 35 | private HttpStatus status; 36 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") 37 | private LocalDateTime timestamp; 38 | private String message; 39 | private String debugMessage; 40 | private List subErrors; 41 | 42 | private ApiError() { 43 | timestamp = LocalDateTime.now(); 44 | } 45 | 46 | public ApiError(HttpStatus status) { 47 | this(); 48 | this.status = status; 49 | } 50 | 51 | public ApiError(HttpStatus status, Throwable ex) { 52 | this(); 53 | this.status = status; 54 | this.message = "Unexpected error"; 55 | this.debugMessage = ex.getLocalizedMessage(); 56 | } 57 | 58 | public ApiError(HttpStatus status, String message, Throwable ex) { 59 | this(); 60 | this.status = status; 61 | this.message = message; 62 | this.debugMessage = ex.getLocalizedMessage(); 63 | } 64 | 65 | private void addSubError(ApiSubError subError) { 66 | if (subErrors == null) { 67 | subErrors = new ArrayList<>(); 68 | } 69 | subErrors.add(subError); 70 | } 71 | 72 | private void addValidationError(String object, String field, Object rejectedValue, String message) { 73 | addSubError(new ApiValidationError(object, field, rejectedValue, message)); 74 | } 75 | 76 | private void addValidationError(String object, String message) { 77 | addSubError(new ApiValidationError(object, message)); 78 | } 79 | 80 | private void addValidationError(FieldError fieldError) { 81 | this.addValidationError( 82 | fieldError.getObjectName(), 83 | fieldError.getField(), 84 | fieldError.getRejectedValue(), 85 | fieldError.getDefaultMessage()); 86 | } 87 | 88 | public void addValidationErrors(List fieldErrors) { 89 | fieldErrors.forEach(this::addValidationError); 90 | } 91 | 92 | private void addValidationError(ObjectError objectError) { 93 | this.addValidationError( 94 | objectError.getObjectName(), 95 | objectError.getDefaultMessage()); 96 | } 97 | 98 | public void addValidationError(List globalErrors) { 99 | globalErrors.forEach(this::addValidationError); 100 | } 101 | 102 | /** 103 | * Utility method for adding error of ConstraintViolation. Usually when a @Validated validation fails. 104 | * 105 | * @param cv the ConstraintViolation 106 | */ 107 | private void addValidationError(ConstraintViolation cv) { 108 | this.addValidationError( 109 | cv.getRootBeanClass().getSimpleName(), 110 | ((PathImpl) cv.getPropertyPath()).getLeafNode().asString(), 111 | cv.getInvalidValue(), 112 | cv.getMessage()); 113 | } 114 | 115 | public void addValidationErrors(Set> constraintViolations) { 116 | constraintViolations.forEach(this::addValidationError); 117 | } 118 | 119 | 120 | abstract class ApiSubError { 121 | 122 | } 123 | 124 | @Data 125 | @EqualsAndHashCode(callSuper = false) 126 | @AllArgsConstructor 127 | class ApiValidationError extends ApiSubError { 128 | private String object; 129 | private String field; 130 | private Object rejectedValue; 131 | private String message; 132 | 133 | ApiValidationError(String object, String message) { 134 | this.object = object; 135 | this.message = message; 136 | } 137 | } 138 | } 139 | 140 | class LowerCaseClassNameResolver extends TypeIdResolverBase { 141 | 142 | @Override 143 | public String idFromValue(Object value) { 144 | return value.getClass().getSimpleName().toLowerCase(); 145 | } 146 | 147 | @Override 148 | public String idFromValueAndType(Object value, Class suggestedType) { 149 | return idFromValue(value); 150 | } 151 | 152 | @Override 153 | public JsonTypeInfo.Id getMechanism() { 154 | return JsonTypeInfo.Id.CUSTOM; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/BankException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception; 2 | 3 | /** 4 | * Created by Arpit Khandelwal. 5 | */ 6 | public class BankException extends Exception { 7 | public BankException (String message){ 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/DuplicateEntityException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception; 2 | 3 | /** 4 | * Created by Arpit Khandelwal. 5 | */ 6 | public class DuplicateEntityException extends EntityException { 7 | public DuplicateEntityException(Class clazz, String... searchParamsMap) { 8 | super(clazz, " was already found for parameters ", searchParamsMap); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/EntityException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.stream.IntStream; 8 | 9 | /** 10 | * Created by Arpit Khandelwal. 11 | */ 12 | public class EntityException extends Exception { 13 | 14 | public EntityException(Class clazz, String indicator, String... searchParamsMap) { 15 | super(EntityException.generateMessage(clazz.getSimpleName(), indicator, toMap(String.class, String.class, searchParamsMap))); 16 | } 17 | 18 | private static String generateMessage(String entity, String indicator, Map searchParams) { 19 | return StringUtils.capitalize(entity) + indicator + searchParams; 20 | } 21 | 22 | private static Map toMap( 23 | Class keyType, Class valueType, Object... entries) { 24 | if (entries.length % 2 == 1) 25 | throw new IllegalArgumentException("Invalid entries"); 26 | return IntStream.range(0, entries.length / 2).map(i -> i * 2) 27 | .collect(HashMap::new, 28 | (m, i) -> m.put(keyType.cast(entries[i]), valueType.cast(entries[i + 1])), 29 | Map::putAll); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/EntityNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception; 2 | 3 | /** 4 | * Created by Arpit Khandelwal. 5 | */ 6 | public class EntityNotFoundException extends EntityException { 7 | 8 | public EntityNotFoundException(Class clazz, String... searchParamsMap) { 9 | super(clazz, " was not found for parameters ", searchParamsMap); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/exception/handler/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.exception.handler; 2 | 3 | 4 | import com.bankofspring.exception.ApiError; 5 | import com.bankofspring.exception.BankException; 6 | import com.bankofspring.exception.DuplicateEntityException; 7 | import com.bankofspring.exception.EntityNotFoundException; 8 | import org.hibernate.exception.ConstraintViolationException; 9 | import org.springframework.core.Ordered; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.dao.DataIntegrityViolationException; 12 | import org.springframework.http.HttpHeaders; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.http.converter.HttpMessageNotReadableException; 16 | import org.springframework.http.converter.HttpMessageNotWritableException; 17 | import org.springframework.web.HttpMediaTypeNotSupportedException; 18 | import org.springframework.web.bind.MethodArgumentNotValidException; 19 | import org.springframework.web.bind.MissingServletRequestParameterException; 20 | import org.springframework.web.bind.annotation.ControllerAdvice; 21 | import org.springframework.web.bind.annotation.ExceptionHandler; 22 | import org.springframework.web.context.request.ServletWebRequest; 23 | import org.springframework.web.context.request.WebRequest; 24 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 25 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 26 | 27 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 28 | import static org.springframework.http.HttpStatus.NOT_FOUND; 29 | 30 | /** 31 | * Created by Arpit Khandelwal. 32 | */ 33 | @Order(Ordered.HIGHEST_PRECEDENCE) 34 | @ControllerAdvice 35 | public class RestExceptionHandler extends ResponseEntityExceptionHandler { 36 | 37 | /** 38 | * Handle MissingServletRequestParameterException. Triggered when a 'required' request parameter is missing. 39 | * 40 | * @param ex MissingServletRequestParameterException 41 | * @param headers HttpHeaders 42 | * @param status HttpStatus 43 | * @param request WebRequest 44 | * @return the ApiError object 45 | */ 46 | @Override 47 | protected ResponseEntity handleMissingServletRequestParameter( 48 | MissingServletRequestParameterException ex, HttpHeaders headers, 49 | HttpStatus status, WebRequest request) { 50 | String error = ex.getParameterName() + " parameter is missing"; 51 | return buildResponseEntity(new ApiError(BAD_REQUEST, error, ex)); 52 | } 53 | 54 | 55 | /** 56 | * Handle HttpMediaTypeNotSupportedException. This one triggers when JSON is invalid as well. 57 | * 58 | * @param ex HttpMediaTypeNotSupportedException 59 | * @param headers HttpHeaders 60 | * @param status HttpStatus 61 | * @param request WebRequest 62 | * @return the ApiError object 63 | */ 64 | @Override 65 | protected ResponseEntity handleHttpMediaTypeNotSupported( 66 | HttpMediaTypeNotSupportedException ex, 67 | HttpHeaders headers, 68 | HttpStatus status, 69 | WebRequest request) { 70 | StringBuilder builder = new StringBuilder(); 71 | builder.append(ex.getContentType()); 72 | builder.append(" media type is not supported. Supported media types are "); 73 | ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); 74 | return buildResponseEntity(new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex)); 75 | } 76 | 77 | /** 78 | * Handle MethodArgumentNotValidException. Triggered when an object fails @Valid validation. 79 | * 80 | * @param ex the MethodArgumentNotValidException that is thrown when @Valid validation fails 81 | * @param headers HttpHeaders 82 | * @param status HttpStatus 83 | * @param request WebRequest 84 | * @return the ApiError object 85 | */ 86 | @Override 87 | protected ResponseEntity handleMethodArgumentNotValid( 88 | MethodArgumentNotValidException ex, 89 | HttpHeaders headers, 90 | HttpStatus status, 91 | WebRequest request) { 92 | ApiError apiError = new ApiError(BAD_REQUEST); 93 | apiError.setMessage("Validation error"); 94 | apiError.addValidationErrors(ex.getBindingResult().getFieldErrors()); 95 | apiError.addValidationError(ex.getBindingResult().getGlobalErrors()); 96 | return buildResponseEntity(apiError); 97 | } 98 | 99 | /** 100 | * Handles javax.validation.ConstraintViolationException. Thrown when @Validated fails. 101 | * 102 | * @param ex the ConstraintViolationException 103 | * @return the ApiError object 104 | */ 105 | @ExceptionHandler(javax.validation.ConstraintViolationException.class) 106 | protected ResponseEntity handleConstraintViolation( 107 | javax.validation.ConstraintViolationException ex) { 108 | ApiError apiError = new ApiError(BAD_REQUEST); 109 | apiError.setMessage("Validation error"); 110 | apiError.addValidationErrors(ex.getConstraintViolations()); 111 | return buildResponseEntity(apiError); 112 | } 113 | 114 | /** 115 | * Handles EntityNotFoundException. Created to encapsulate errors with more detail than javax.persistence.EntityNotFoundException. 116 | * 117 | * @param ex the EntityNotFoundException 118 | * @return the ApiError object 119 | */ 120 | @ExceptionHandler(EntityNotFoundException.class) 121 | protected ResponseEntity handleEntityNotFound( 122 | EntityNotFoundException ex) { 123 | ApiError apiError = new ApiError(NOT_FOUND); 124 | apiError.setMessage(ex.getMessage()); 125 | return buildResponseEntity(apiError); 126 | } 127 | 128 | /** 129 | * Handles DuplicateEntityException. Created to encapsulate errors for multiple entities. 130 | * 131 | * @param ex the DuplicateEntityException 132 | * @return the ApiError object 133 | */ 134 | @ExceptionHandler(DuplicateEntityException.class) 135 | protected ResponseEntity handleDuplicateEntityFound( 136 | DuplicateEntityException ex) { 137 | ApiError apiError = new ApiError(BAD_REQUEST); 138 | apiError.setMessage(ex.getMessage()); 139 | return buildResponseEntity(apiError); 140 | } 141 | 142 | /** 143 | * Handles all the generic exceptions thrown by SpringBootBank 144 | * @param ex the BankException 145 | * @return the ApiError object 146 | */ 147 | @ExceptionHandler(BankException.class) 148 | protected ResponseEntity handleBankException( 149 | BankException ex) { 150 | ApiError apiError = new ApiError(BAD_REQUEST); 151 | apiError.setMessage(ex.getMessage()); 152 | return buildResponseEntity(apiError); 153 | } 154 | 155 | /** 156 | * Handle HttpMessageNotReadableException. Happens when request JSON is malformed. 157 | * 158 | * @param ex HttpMessageNotReadableException 159 | * @param headers HttpHeaders 160 | * @param status HttpStatus 161 | * @param request WebRequest 162 | * @return the ApiError object 163 | */ 164 | @Override 165 | protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 166 | ServletWebRequest servletWebRequest = (ServletWebRequest) request; 167 | //log.info("{} to {}", servletWebRequest.getHttpMethod(), servletWebRequest.getRequest().getServletPath()); 168 | String error = "Malformed JSON request"; 169 | return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); 170 | } 171 | 172 | /** 173 | * Handle HttpMessageNotWritableException. 174 | * 175 | * @param ex HttpMessageNotWritableException 176 | * @param headers HttpHeaders 177 | * @param status HttpStatus 178 | * @param request WebRequest 179 | * @return the ApiError object 180 | */ 181 | @Override 182 | protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 183 | String error = "Error writing JSON output"; 184 | return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, error, ex)); 185 | } 186 | 187 | /** 188 | * Handle javax.persistence.EntityNotFoundException 189 | */ 190 | @ExceptionHandler(javax.persistence.EntityNotFoundException.class) 191 | protected ResponseEntity handleEntityNotFound(javax.persistence.EntityNotFoundException ex) { 192 | return buildResponseEntity(new ApiError(HttpStatus.NOT_FOUND, ex)); 193 | } 194 | 195 | /** 196 | * Handle DataIntegrityViolationException, inspects the cause for different DB causes. 197 | * 198 | * @param ex the DataIntegrityViolationException 199 | * @return the ApiError object 200 | */ 201 | @ExceptionHandler(DataIntegrityViolationException.class) 202 | protected ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException ex, 203 | WebRequest request) { 204 | if (ex.getCause() instanceof ConstraintViolationException) { 205 | return buildResponseEntity(new ApiError(HttpStatus.CONFLICT, "Database error", ex.getCause())); 206 | } 207 | return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex)); 208 | } 209 | 210 | /** 211 | * Handle Exception, handle generic Exception.class 212 | * 213 | * @param ex the Exception 214 | * @return the ApiError object 215 | */ 216 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 217 | protected ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, 218 | WebRequest request) { 219 | ApiError apiError = new ApiError(BAD_REQUEST); 220 | apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName())); 221 | apiError.setDebugMessage(ex.getMessage()); 222 | return buildResponseEntity(apiError); 223 | } 224 | 225 | 226 | private ResponseEntity buildResponseEntity(ApiError apiError) { 227 | return new ResponseEntity<>(apiError, apiError.getStatus()); 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/account/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.account; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | import com.bankofspring.dto.AccountDto; 5 | import com.bankofspring.exception.EntityException; 6 | import com.bankofspring.exception.EntityNotFoundException; 7 | import com.bankofspring.service.account.exception.InsufficientFundsException; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by Arpit Khandelwal. 14 | */ 15 | public interface AccountService { 16 | Account createAccount(AccountDto accountDto) throws EntityException; 17 | 18 | List getAllAccounts(); 19 | 20 | Account getAccount(Long accountNumber) throws EntityNotFoundException; 21 | 22 | Account creditAmount(AccountDto accountDto, BigDecimal depositAmt) throws EntityNotFoundException; 23 | 24 | Account debitAmount(AccountDto accountDto, BigDecimal withdrawalAmt) throws EntityNotFoundException, InsufficientFundsException; 25 | 26 | List transferFunds(AccountDto fromAccountDto, AccountDto toAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/account/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.account; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | import com.bankofspring.domain.model.Branch; 5 | import com.bankofspring.domain.model.Customer; 6 | import com.bankofspring.domain.repository.AccountRepository; 7 | import com.bankofspring.domain.repository.BranchRepository; 8 | import com.bankofspring.domain.repository.CustomerRepository; 9 | import com.bankofspring.dto.AccountDto; 10 | import com.bankofspring.exception.EntityException; 11 | import com.bankofspring.exception.EntityNotFoundException; 12 | import com.bankofspring.service.account.exception.InsufficientFundsException; 13 | import org.modelmapper.ModelMapper; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Isolation; 17 | import org.springframework.transaction.annotation.Propagation; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import java.math.BigDecimal; 21 | import java.util.*; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | import java.util.stream.StreamSupport; 25 | 26 | /** 27 | * Created by Arpit Khandelwal. 28 | */ 29 | @Service 30 | public class AccountServiceImpl implements AccountService { 31 | 32 | @Autowired 33 | AccountRepository accountRepository; 34 | 35 | @Autowired 36 | CustomerRepository customerRepository; 37 | 38 | @Autowired 39 | BranchRepository branchRepository; 40 | 41 | @Autowired 42 | ModelMapper mapper; 43 | 44 | @Override 45 | public List getAllAccounts() { 46 | Iterator iteratorToCollection = accountRepository.findAll().iterator(); 47 | return StreamSupport.stream( 48 | Spliterators 49 | .spliteratorUnknownSize(iteratorToCollection, Spliterator.ORDERED), false) 50 | .collect(Collectors.toList() 51 | ); 52 | } 53 | 54 | @Override 55 | public Account getAccount(Long accountNumber) throws EntityNotFoundException { 56 | Optional accountInDb = accountRepository.findById(accountNumber); 57 | if (accountInDb.isPresent()) { 58 | return accountInDb.get(); 59 | } 60 | throw new EntityNotFoundException(Account.class, "accountNumber", accountNumber.toString()); 61 | } 62 | 63 | @Override 64 | @Transactional 65 | public Account createAccount(AccountDto accountDto) throws EntityException { 66 | Optional customer = customerRepository.findById(accountDto.getCustomerId()); 67 | Optional branch = branchRepository.findById(accountDto.getBranchId()); 68 | if (customer.isPresent()) { 69 | if (branch.isPresent()) { 70 | Account account = mapper.map(accountDto, Account.class); 71 | account.setCoreBranch(branch.get()); 72 | account.setAccountOwner(customer.get()); 73 | return accountRepository.save(account); 74 | } 75 | throw new EntityNotFoundException(Branch.class, "branchId", accountDto.getBranchId().toString()); 76 | } 77 | throw new EntityNotFoundException(Customer.class, "customerId", accountDto.getCustomerId().toString()); 78 | } 79 | 80 | @Override 81 | @Transactional(propagation=Propagation.REQUIRED, rollbackFor = EntityNotFoundException.class, isolation= Isolation.READ_COMMITTED) 82 | public Account creditAmount(AccountDto accountDto, BigDecimal creditAmt) throws EntityNotFoundException { 83 | assert(creditAmt.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 84 | Optional accountInDb = accountRepository.findById(accountDto.getAccountNumber()); 85 | if (accountInDb.isPresent()) { 86 | Account account = accountInDb.get(); 87 | account.credit(creditAmt); 88 | return accountRepository.save(account); 89 | } 90 | throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString()); 91 | } 92 | 93 | @Override 94 | @Transactional(propagation= Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED) 95 | public Account debitAmount(AccountDto accountDto, BigDecimal debitAmt) throws EntityNotFoundException, InsufficientFundsException { 96 | assert(debitAmt.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 97 | Optional accountInDb = accountRepository.findById(accountDto.getAccountNumber()); 98 | if (accountInDb.isPresent()) { 99 | Account account = accountInDb.get(); 100 | account.debit(debitAmt); 101 | return accountRepository.save(account); 102 | } 103 | throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString()); 104 | } 105 | 106 | @Override 107 | @Transactional(propagation=Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED) 108 | public List transferFunds(AccountDto debitAccountDto, AccountDto creditAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException { 109 | assert(amount.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 110 | Account debitAccount = debitAmount(debitAccountDto, amount); 111 | Account creditAccount = creditAmount(creditAccountDto, amount); 112 | return Stream 113 | .of(debitAccount, creditAccount) 114 | .collect(Collectors.toList()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/account/exception/AccountException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.account.exception; 2 | 3 | import com.bankofspring.exception.BankException; 4 | 5 | /** 6 | * Created by Arpit Khandelwal. 7 | */ 8 | public class AccountException extends BankException { 9 | public AccountException(String message){ 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/account/exception/InsufficientFundsException.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.account.exception; 2 | 3 | import com.bankofspring.domain.model.Account; 4 | 5 | import java.math.BigDecimal; 6 | 7 | /** 8 | * Created by Arpit Khandelwal. 9 | */ 10 | public class InsufficientFundsException extends AccountException { 11 | public InsufficientFundsException(Account account, BigDecimal withdrawalAmt){ 12 | super("Insufficient funds in account number - " + account.getAccountNumber() + ". Cannot allow withdrawal of $" + withdrawalAmt + "."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/customer/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.customer; 2 | 3 | import com.bankofspring.domain.model.Customer; 4 | import com.bankofspring.dto.CustomerDto; 5 | import com.bankofspring.exception.EntityException; 6 | import com.bankofspring.exception.EntityNotFoundException; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Created by Arpit Khandelwal. 12 | */ 13 | public interface CustomerService { 14 | Customer createCustomer(CustomerDto customerDto) throws EntityException; 15 | 16 | Customer getCustomer(String ssn) throws EntityNotFoundException; 17 | 18 | List getAllCustomers(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/bankofspring/service/customer/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.service.customer; 2 | 3 | import com.bankofspring.domain.model.Customer; 4 | import com.bankofspring.domain.repository.CustomerRepository; 5 | import com.bankofspring.dto.CustomerDto; 6 | import com.bankofspring.exception.DuplicateEntityException; 7 | import com.bankofspring.exception.EntityException; 8 | import com.bankofspring.exception.EntityNotFoundException; 9 | import org.modelmapper.ModelMapper; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.*; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.StreamSupport; 16 | 17 | /** 18 | * Created by Arpit Khandelwal. 19 | */ 20 | @Service 21 | public class CustomerServiceImpl implements CustomerService { 22 | @Autowired 23 | private CustomerRepository customerRepository; 24 | 25 | @Autowired 26 | private ModelMapper mapper; 27 | 28 | @Override 29 | public List getAllCustomers() { 30 | Iterator iteratorToCollection = customerRepository.findAll().iterator(); 31 | return StreamSupport.stream( 32 | Spliterators 33 | .spliteratorUnknownSize(iteratorToCollection, Spliterator.ORDERED), false) 34 | .collect(Collectors.toList() 35 | ); 36 | } 37 | 38 | @Override 39 | public Customer getCustomer(String ssn) throws EntityNotFoundException { 40 | Customer customer = customerRepository.findBySsn(ssn); 41 | if (customer != null) { 42 | return customer; 43 | } 44 | throw new EntityNotFoundException(Customer.class, "ssn", ssn.toString()); 45 | 46 | } 47 | 48 | @Override 49 | public Customer createCustomer(CustomerDto customerDto) throws EntityException { 50 | Optional customerInDb = Optional.ofNullable(customerRepository.findBySsn(customerDto.getSsn())); 51 | if (customerInDb.isPresent()) { 52 | throw new DuplicateEntityException(Customer.class, "ssn", customerDto.getSsn().toString()); 53 | } 54 | Customer customer = mapper.map(customerDto, Customer.class); 55 | return customerRepository.save(customer); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # H2 2 | spring.h2.console.enabled=true 3 | spring.h2.console.path=/h2 4 | spring.datasource.platform=h2 5 | spring.datasource.url=jdbc:h2:mem:bankdb 6 | spring.datasource.driverClassName=org.h2.Driver 7 | spring.datasource.username=sa 8 | spring.datasource.password= 9 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -------------------------------------------------------------------------------- /src/main/resources/data-h2.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO branch VALUES (1, '2018-01-01', '2018-01-01', 1000.0, 'Miniland', 'IND-1'); 2 | INSERT INTO branch VALUES (2, '2018-01-02', '2018-01-02', 2000.0, 'Miniland', 'IND-2'); 3 | INSERT INTO branch VALUES (3, '2018-01-03', '2018-01-03', 3000.0, 'Miniland', 'IND-3'); 4 | INSERT INTO branch VALUES (4, '2018-01-04', '2018-01-04', 4000.0, 'Miniland', 'IND-4'); 5 | INSERT INTO branch VALUES (5, '2018-01-05', '2018-01-05', 5000.0, 'Miniland', 'IND-5'); 6 | 7 | INSERT INTO customer VALUES (1, '2018-01-01', '2018-01-01', 'VT1', 'Marine Bay1', 'Indore', '9425094250', 'Arpit K', 'AK01'); 8 | INSERT INTO customer VALUES (2, '2018-01-01', '2018-01-01', 'VT2', 'Marine Bay2', 'France', '9425094251', 'Jinedin Jidan', 'JJ01'); 9 | INSERT INTO customer VALUES (3, '2018-01-01', '2018-01-01', 'VT3', 'Marine Bay3', 'UK', '9425094252', 'Harry Kane', 'HK01'); 10 | INSERT INTO customer VALUES (4, '2018-01-01', '2018-01-01', 'VT4', 'Marine Bay4', 'Brasil', '9425094253', 'Nemar Jr', 'NJ01'); 11 | INSERT INTO customer VALUES (5, '2018-01-01', '2018-01-01', 'VT5', 'Marine Bay5', 'Germany', '9425094254', 'Thomas Muller', 'TM01'); 12 | 13 | INSERT INTO account VALUES (1, '2018-01-01', '2018-01-01', 100.0, 0, 1, 1); 14 | INSERT INTO account VALUES (2, '2018-01-01', '2018-01-01', 200.0, 0, 2, 1); 15 | INSERT INTO account VALUES (3, '2018-01-01', '2018-01-01', 300.0, 0, 3, 1); 16 | INSERT INTO account VALUES (4, '2018-01-01', '2018-01-01', 400.0, 0, 4, 1); 17 | INSERT INTO account VALUES (5, '2018-01-01', '2018-01-01', 500.0, 0, 5, 1); 18 | INSERT INTO account VALUES (6, '2018-01-01', '2018-01-01', 100.0, 1, 1, 2); 19 | INSERT INTO account VALUES (7, '2018-01-01', '2018-01-01', 200.0, 1, 2, 2); 20 | INSERT INTO account VALUES (8, '2018-01-01', '2018-01-01', 300.0, 1, 3, 2); 21 | INSERT INTO account VALUES (9, '2018-01-01', '2018-01-01', 400.0, 1, 4, 2); 22 | INSERT INTO account VALUES (10, '2018-01-01', '2018-01-01', 500.0, 1, 5, 2); -------------------------------------------------------------------------------- /src/main/resources/images/BankOfSpring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/src/main/resources/images/BankOfSpring.png -------------------------------------------------------------------------------- /src/main/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/src/main/resources/images/logo.png -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/BankOfSpringApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring; 2 | 3 | import com.bankofspring.integrationtest.controller.AccountControllerTest; 4 | import com.bankofspring.integrationtest.controller.CustomerControllerTest; 5 | import org.junit.runner.RunWith; 6 | import org.junit.runners.Suite; 7 | 8 | /*@RunWith(Suite.class) 9 | @Suite.SuiteClasses({ 10 | CustomerControllerTest.class, 11 | AccountControllerTest.class 12 | })*/ 13 | public class BankOfSpringApplicationTests { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/BankOfSpringApplicationUnitTests.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring; 2 | 3 | 4 | import com.bankofspring.unittest.controller.CustomerControllerMockMvcStandaloneTest; 5 | import org.junit.runner.RunWith; 6 | import org.junit.runners.Suite; 7 | 8 | 9 | /*@RunWith(Suite.class) 10 | @Suite.SuiteClasses({ 11 | CustomerControllerMockMvcStandaloneTest.class 12 | })*/ 13 | /** 14 | * Created by Arpit Khandelwal. 15 | */ 16 | public class BankOfSpringApplicationUnitTests { 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/integrationtest/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/src/test/java/com/bankofspring/integrationtest/.gitkeep -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/integrationtest/controller/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.integrationtest.controller; 2 | 3 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import org.junit.Before; 9 | import org.junit.FixMethodOrder; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.MethodSorters; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.context.ContextConfiguration; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | import org.springframework.web.context.WebApplicationContext; 22 | import com.bankofspring.BankOfSpringApplication; 23 | 24 | /** 25 | * Created by Arpit Khandelwal. 26 | */ 27 | @RunWith(SpringRunner.class) 28 | @ContextConfiguration(classes = BankOfSpringApplication.class) 29 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 30 | @SpringBootTest 31 | public class AccountControllerTest { 32 | private MockMvc mockMvc; 33 | 34 | @Autowired 35 | private WebApplicationContext wac; 36 | 37 | @Before 38 | public void setup() { 39 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 40 | } 41 | 42 | /** 43 | * Get all accounts integrationtest. 44 | * All the accounts in the database should be returned as a result of this integrationtest and validation of total 10 accounts should pass. 45 | * 46 | * @throws Exception 47 | */ 48 | @Test 49 | public void ut1_GetAccounts() throws Exception{ 50 | mockMvc 51 | .perform(MockMvcRequestBuilders.get("/v1/account/").accept(MediaType.APPLICATION_JSON)) 52 | .andExpect(status().isOk()) 53 | .andExpect(jsonPath("$", hasSize(10))) 54 | .andDo(print()); 55 | } 56 | 57 | /** 58 | * Get Account details integrationtest. 59 | * Fetching the details for account with id 1 should be allowed and valid details should be returned. 60 | * 61 | * @throws Exception 62 | */ 63 | @Test 64 | public void ut2_GetAccountByNumber() throws Exception { 65 | mockMvc 66 | .perform(MockMvcRequestBuilders.get("/v1/account/1").accept(MediaType.APPLICATION_JSON)) 67 | .andExpect(status().isOk()) 68 | .andExpect(jsonPath("$.accountNumber").exists()) 69 | .andExpect(jsonPath("$.branchId").exists()) 70 | .andExpect(jsonPath("$.type").exists()) 71 | .andExpect(jsonPath("$.balance").exists()) 72 | .andExpect(jsonPath("$.accountOwner.customerId").exists()) 73 | .andExpect(jsonPath("$.accountOwner.name").exists()) 74 | .andExpect(jsonPath("$.accountOwner.ssn").exists()) 75 | .andExpect(jsonPath("$.accountOwner.address1").exists()) 76 | .andExpect(jsonPath("$.accountOwner.address2").exists()) 77 | .andExpect(jsonPath("$.accountOwner.city").exists()) 78 | .andExpect(jsonPath("$.accountOwner.contactNumber").exists()) 79 | .andExpect(jsonPath("$.accountNumber").value(1)) 80 | .andExpect(jsonPath("$.branchId").value(1)) 81 | .andExpect(jsonPath("$.type").value("SAVINGS")) 82 | .andExpect(jsonPath("$.balance").value(100)) 83 | .andExpect(jsonPath("$.accountOwner.customerId").value(1)) 84 | .andExpect(jsonPath("$.accountOwner.name").value("Arpit K")) 85 | .andExpect(jsonPath("$.accountOwner.ssn").value("AK01")) 86 | .andExpect(jsonPath("$.accountOwner.address1").value("VT1")) 87 | .andExpect(jsonPath("$.accountOwner.address2").value("Marine Bay1")) 88 | .andExpect(jsonPath("$.accountOwner.city").value("Indore")) 89 | .andExpect(jsonPath("$.accountOwner.contactNumber").value("9425094250")) 90 | .andDo(print()); 91 | } 92 | 93 | /** 94 | * Get Account by number integrationtest for invalid account number. 95 | * Trying to get an account's detail with invalid account number should result in a Http 400 error. 96 | * 97 | * @throws Exception 98 | */ 99 | @Test 100 | public void ut3_GetAccountByNumber_InvalidAccount() throws Exception { 101 | mockMvc 102 | .perform(MockMvcRequestBuilders.get("/v1/account/100").accept(MediaType.APPLICATION_JSON)) 103 | .andExpect(status().isNotFound()) 104 | .andExpect(jsonPath("$.apierror.status").value("NOT_FOUND")) 105 | .andExpect(jsonPath("$.apierror.message").value("Account was not found for parameters {accountNumber=100}")) 106 | .andDo(print()); 107 | } 108 | 109 | /** 110 | * Create Account integrationtest. 111 | * Account with details given in the integrationtest is created as a result of the integrationtest execution. 112 | * 113 | * @throws Exception 114 | */ 115 | @Test 116 | public void ut4_CreateAccount() throws Exception { 117 | mockMvc 118 | .perform(MockMvcRequestBuilders.post("/v1/account/create") 119 | .contentType(MediaType.APPLICATION_JSON) 120 | .content("{\"customerId\":\"1\", \"branchId\":\"1\", \"type\":\"CURRENT\",\"balance\":\"1000\"}") 121 | .accept(MediaType.APPLICATION_JSON)) 122 | .andExpect(status().isOk()) 123 | .andExpect(jsonPath("$.accountNumber").exists()) 124 | .andExpect(jsonPath("$.branchId").exists()) 125 | .andExpect(jsonPath("$.type").exists()) 126 | .andExpect(jsonPath("$.balance").exists()) 127 | .andExpect(jsonPath("$.accountOwner.customerId").exists()) 128 | .andExpect(jsonPath("$.accountOwner.name").exists()) 129 | .andExpect(jsonPath("$.accountOwner.ssn").exists()) 130 | .andExpect(jsonPath("$.accountOwner.address1").exists()) 131 | .andExpect(jsonPath("$.accountOwner.address2").exists()) 132 | .andExpect(jsonPath("$.accountOwner.city").exists()) 133 | .andExpect(jsonPath("$.accountOwner.contactNumber").exists()) 134 | .andExpect(jsonPath("$.accountNumber").value(11)) 135 | .andExpect(jsonPath("$.branchId").value(1)) 136 | .andExpect(jsonPath("$.type").value("CURRENT")) 137 | .andExpect(jsonPath("$.balance").value(1000)) 138 | .andExpect(jsonPath("$.accountOwner.customerId").value(1)) 139 | .andExpect(jsonPath("$.accountOwner.name").value("Arpit K")) 140 | .andExpect(jsonPath("$.accountOwner.ssn").value("AK01")) 141 | .andExpect(jsonPath("$.accountOwner.address1").value("VT1")) 142 | .andExpect(jsonPath("$.accountOwner.address2").value("Marine Bay1")) 143 | .andExpect(jsonPath("$.accountOwner.city").value("Indore")) 144 | .andExpect(jsonPath("$.accountOwner.contactNumber").value("9425094250")) 145 | .andDo(print()); 146 | } 147 | 148 | /** 149 | * Create Account integrationtest for Invalid Customer. 150 | * Trying to create an account for a customer with id 100 should be disallowed. 151 | * A Http 400 should be thrown with message indicating that customer id 100 doesn't yet exist. 152 | * 153 | * @throws Exception 154 | */ 155 | @Test 156 | public void ut5_CreateAccount_InvalidCustomer() throws Exception { 157 | mockMvc 158 | .perform(MockMvcRequestBuilders.post("/v1/account/create") 159 | .contentType(MediaType.APPLICATION_JSON) 160 | .content("{\"customerId\":\"100\", \"branchId\":\"1\", \"type\":\"CURRENT\",\"balance\":\"1000\"}") 161 | .accept(MediaType.APPLICATION_JSON)) 162 | .andExpect(status().isNotFound()) 163 | .andExpect(jsonPath("$.apierror.status").value("NOT_FOUND")) 164 | .andExpect(jsonPath("$.apierror.message").value("Customer was not found for parameters {customerId=100}")) 165 | .andDo(print()); 166 | } 167 | 168 | /** 169 | * Deposit Money integrationtest. 170 | * Account # 1 : Initial amount is $100 171 | * A deposit of $50 should be allowed and resultant balance should be $150. 172 | * 173 | * @throws Exception 174 | */ 175 | @Test 176 | public void ut6_DepositMoney() throws Exception { 177 | mockMvc 178 | .perform(MockMvcRequestBuilders.post("/v1/account/deposit") 179 | .contentType(MediaType.APPLICATION_JSON) 180 | .content("{\"accountNumber\":\"1\", \"depositAmt\":\"50\"}") 181 | .accept(MediaType.APPLICATION_JSON)) 182 | .andExpect(status().isOk()) 183 | .andExpect(jsonPath("$.accountNumber").value(1)) 184 | .andExpect(jsonPath("$.balance").value(150)) 185 | .andDo(print()); 186 | } 187 | 188 | /** 189 | * Withdraw Money integrationtest. 190 | * Account # 1 : Amount after ut5 is $150 191 | * A withdrawal of 50 should be allowed and new account balance should be $100. 192 | * 193 | * @throws Exception 194 | */ 195 | @Test 196 | public void ut7_WithdrawMoney() throws Exception { 197 | mockMvc 198 | .perform(MockMvcRequestBuilders.post("/v1/account/withdraw") 199 | .contentType(MediaType.APPLICATION_JSON) 200 | .content("{\"accountNumber\":\"1\", \"withdrawlAmt\":\"50\"}") 201 | .accept(MediaType.APPLICATION_JSON)) 202 | .andExpect(status().isOk()) 203 | .andExpect(jsonPath("$.accountNumber").value(1)) 204 | .andExpect(jsonPath("$.balance").value(100)) 205 | .andDo(print()); 206 | } 207 | 208 | /** 209 | * Withdraw Money integrationtest for negative scenario. 210 | * Account # 1 : Amount after ut6 is $100 211 | * A withdrawal of $200 should raise a InsufficientFund Exception and send a Http 400 response. 212 | * 213 | * @throws Exception 214 | */ 215 | @Test 216 | public void ut8_WithdrawMoney_InsufficientFunds() throws Exception { 217 | mockMvc 218 | .perform(MockMvcRequestBuilders.post("/v1/account/withdraw") 219 | .contentType(MediaType.APPLICATION_JSON) 220 | .content("{\"accountNumber\":\"1\", \"withdrawlAmt\":\"200\"}") 221 | .accept(MediaType.APPLICATION_JSON)) 222 | .andExpect(status().isBadRequest()) 223 | .andExpect(jsonPath("$.apierror.status").value("BAD_REQUEST")) 224 | .andExpect(jsonPath("$.apierror.message").value("Insufficient funds in account number - 1. Cannot allow withdrawal of $200.")) 225 | .andDo(print()); 226 | } 227 | 228 | /** 229 | * Transfer Money integrationtest. 230 | * Account # 1 : Initial Amount 100 231 | * Account # 2 : Initial Amount 200 232 | * Once the integrationtest completes, $50 should be transferred from Account # 1 to Account # 2 233 | * 234 | * @throws Exception 235 | */ 236 | @Test 237 | public void ut9_TransferMoney() throws Exception { 238 | mockMvc 239 | .perform(MockMvcRequestBuilders.post("/v1/account/transfer") 240 | .contentType(MediaType.APPLICATION_JSON) 241 | .content("{\"debitAccountNumber\":\"1\", \"creditAccountNumber\":\"2\", \"amount\":\"50\"}") 242 | .accept(MediaType.APPLICATION_JSON)) 243 | .andExpect(status().isOk()) 244 | .andExpect(jsonPath("$.[0].accountNumber").value(1)) 245 | .andExpect(jsonPath("$.[1].accountNumber").value(2)) 246 | .andExpect(jsonPath("$.[0].balance").value(50)) 247 | .andExpect(jsonPath("$.[1].balance").value(250)) 248 | .andDo(print()); 249 | } 250 | 251 | /** 252 | * Transfer Money integrationtest to a non existent recipient. 253 | * Account # 1 : Initial Amount 100 254 | * Account # 2 : doesn't exist 255 | * Once the integrationtest completes, an Http 404 error should be returned indicating the recipient doesn't exist. 256 | * 257 | * @throws Exception 258 | */ 259 | @Test 260 | public void ut10_TransferMoney_InvalidRecipient() throws Exception { 261 | mockMvc 262 | .perform(MockMvcRequestBuilders.post("/v1/account/transfer") 263 | .contentType(MediaType.APPLICATION_JSON) 264 | .content("{\"debitAccountNumber\":\"1\", \"creditAccountNumber\":\"20\", \"amount\":\"50\"}") 265 | .accept(MediaType.APPLICATION_JSON)) 266 | .andExpect(status().isNotFound()) 267 | .andExpect(jsonPath("$.apierror.status").value("NOT_FOUND")) 268 | .andExpect(jsonPath("$.apierror.message").value("Account was not found for parameters {accountNumber=20}")) 269 | .andDo(print()); 270 | } 271 | 272 | } 273 | -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/integrationtest/controller/CustomerControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.integrationtest.controller; 2 | 3 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 7 | 8 | import org.junit.Before; 9 | import org.junit.FixMethodOrder; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.MethodSorters; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.context.ContextConfiguration; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | import org.springframework.web.context.WebApplicationContext; 22 | import com.bankofspring.BankOfSpringApplication; 23 | 24 | /** 25 | * Created by Arpit Khandelwal. 26 | */ 27 | @RunWith(SpringRunner.class) 28 | @ContextConfiguration(classes = BankOfSpringApplication.class) 29 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 30 | @SpringBootTest 31 | public class CustomerControllerTest { 32 | private MockMvc mockMvc; 33 | 34 | @Autowired 35 | private WebApplicationContext wac; 36 | 37 | @Before 38 | public void setup() { 39 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 40 | } 41 | 42 | /** 43 | * Get all customers integrationtest. 44 | * The integrationtest should result in returning the 5 customers which are present in the integrationtest database. 45 | * 46 | * @throws Exception 47 | */ 48 | @Test 49 | public void ut1_GetAllCustomers() throws Exception{ 50 | mockMvc 51 | .perform(MockMvcRequestBuilders.get("/v1/customer/").accept(MediaType.APPLICATION_JSON)) 52 | .andExpect(status().isOk()) 53 | .andExpect(jsonPath("$", hasSize(5))) 54 | .andDo(print()); 55 | } 56 | 57 | /** 58 | * Get customer by ssn integrationtest. 59 | * The integrationtest should result in returning customer's details for SSN:AK01 60 | * 61 | * @throws Exception 62 | */ 63 | @Test 64 | public void ut2_GetCustomerBySsn() throws Exception{ 65 | mockMvc 66 | .perform(MockMvcRequestBuilders.get("/v1/customer/AK01").accept(MediaType.APPLICATION_JSON)) 67 | .andExpect(status().isOk()) 68 | .andExpect(jsonPath("$.customerId").exists()) 69 | .andExpect(jsonPath("$.name").exists()) 70 | .andExpect(jsonPath("$.ssn").exists()) 71 | .andExpect(jsonPath("$.address1").exists()) 72 | .andExpect(jsonPath("$.address2").exists()) 73 | .andExpect(jsonPath("$.city").exists()) 74 | .andExpect(jsonPath("$.contactNumber").exists()) 75 | .andExpect(jsonPath("$.customerId").value(1)) 76 | .andExpect(jsonPath("$.name").value("Arpit K")) 77 | .andExpect(jsonPath("$.ssn").value("AK01")) 78 | .andExpect(jsonPath("$.address1").value("VT1")) 79 | .andExpect(jsonPath("$.address2").value("Marine Bay1")) 80 | .andExpect(jsonPath("$.city").value("Indore")) 81 | .andExpect(jsonPath("$.contactNumber").value("9425094250")) 82 | .andDo(print()); 83 | } 84 | 85 | /** 86 | * Get customer by invalid ssn integrationtest. 87 | * The integrationtest should result in returning a Http 404 for a customer with invalid ssn. 88 | * 89 | * @throws Exception 90 | */ 91 | @Test 92 | public void ut3_GetCustomerBySsn_InvalidSsn() throws Exception { 93 | mockMvc 94 | .perform(MockMvcRequestBuilders.get("/v1/customer/AK02").accept(MediaType.APPLICATION_JSON)) 95 | .andExpect(status().isNotFound()) 96 | .andExpect(jsonPath("$.apierror.status").value("NOT_FOUND")) 97 | .andExpect(jsonPath("$.apierror.message").value("Customer was not found for parameters {ssn=AK02}")) 98 | .andDo(print()); 99 | } 100 | 101 | /** 102 | * Create customer integrationtest. 103 | * The integrationtest should result in adding a new customer to the database with SSN:TK01 104 | * 105 | * @throws Exception 106 | */ 107 | @Test 108 | public void ut4_CreateCustomer() throws Exception{ 109 | mockMvc 110 | .perform(MockMvcRequestBuilders.post("/v1/customer/create") 111 | .contentType(MediaType.APPLICATION_JSON) 112 | .content("{\"name\":\"Test Customer\", \"ssn\":\"TK01\", \"contactNumber\":\"9425094255\",\"address1\":\"Unit-Test\",\"address2\":\"Spring-Boot\",\"city\":\"SpringCity\"}") 113 | .accept(MediaType.APPLICATION_JSON)) 114 | .andExpect(status().isOk()) 115 | .andExpect(jsonPath("$.customerId").exists()) 116 | .andExpect(jsonPath("$.name").exists()) 117 | .andExpect(jsonPath("$.ssn").exists()) 118 | .andExpect(jsonPath("$.address1").exists()) 119 | .andExpect(jsonPath("$.address2").exists()) 120 | .andExpect(jsonPath("$.city").exists()) 121 | .andExpect(jsonPath("$.contactNumber").exists()) 122 | .andExpect(jsonPath("$.customerId").value(6)) 123 | .andExpect(jsonPath("$.name").value("Test Customer")) 124 | .andExpect(jsonPath("$.ssn").value("TK01")) 125 | .andExpect(jsonPath("$.address1").value("Unit-Test")) 126 | .andExpect(jsonPath("$.address2").value("Spring-Boot")) 127 | .andExpect(jsonPath("$.city").value("SpringCity")) 128 | .andExpect(jsonPath("$.contactNumber").value("9425094255")) 129 | .andDo(print()); 130 | } 131 | 132 | /** 133 | * Create duplicate customer integrationtest. 134 | * Since a customer with SSN:TK01 has already been created in ut4, this integrationtest should result in Http 404. 135 | * 136 | * @throws Exception 137 | */ 138 | @Test 139 | public void ut5_CreateCustomer_Duplicate() throws Exception { 140 | mockMvc 141 | .perform(MockMvcRequestBuilders.post("/v1/customer/create") 142 | .contentType(MediaType.APPLICATION_JSON) 143 | .content("{\"name\":\"Test Customer\", \"ssn\":\"TK01\", \"contactNumber\":\"9425094255\",\"address1\":\"Unit-Test\",\"address2\":\"Spring-Boot\",\"city\":\"SpringCity\"}") 144 | .accept(MediaType.APPLICATION_JSON)) 145 | .andExpect(status().isBadRequest()) 146 | .andExpect(jsonPath("$.apierror.status").value("BAD_REQUEST")) 147 | .andExpect(jsonPath("$.apierror.message").value("Customer was already found for parameters {ssn=TK01}")) 148 | .andDo(print()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/unittest/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/BankOfSpring/e09bc0312e726bec2d555e1f4593813312c68a8a/src/test/java/com/bankofspring/unittest/.gitkeep -------------------------------------------------------------------------------- /src/test/java/com/bankofspring/unittest/controller/CustomerControllerMockMvcStandaloneTest.java: -------------------------------------------------------------------------------- 1 | package com.bankofspring.unittest.controller; 2 | 3 | import com.bankofspring.api.v1.controller.CustomerController; 4 | import com.bankofspring.domain.model.Customer; 5 | import com.bankofspring.dto.CustomerDto; 6 | import com.bankofspring.exception.EntityNotFoundException; 7 | import com.bankofspring.service.customer.CustomerService; 8 | import org.junit.Before; 9 | import org.junit.FixMethodOrder; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.MethodSorters; 13 | import org.modelmapper.ModelMapper; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 16 | import org.springframework.boot.test.mock.mockito.MockBean; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 26 | import static org.mockito.Mockito.doReturn; 27 | import static org.mockito.Mockito.doThrow; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 29 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 30 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 31 | 32 | /** 33 | * Created by Arpit Khandelwal. 34 | */ 35 | @RunWith(SpringRunner.class) 36 | @WebMvcTest(CustomerController.class) 37 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 38 | public class CustomerControllerMockMvcStandaloneTest { 39 | 40 | @Autowired 41 | private ModelMapper mapper; 42 | 43 | @Autowired 44 | private MockMvc mockMvc; 45 | 46 | @MockBean 47 | private CustomerService customerService; 48 | 49 | 50 | @Before 51 | public void setup(){} 52 | 53 | /** 54 | * Get all customers unit-test. 55 | * The unit test should result in returning the 2 customers which are provided by us as input. 56 | * 57 | * @throws Exception 58 | */ 59 | @Test 60 | public void ut1_GetAllCustomers() throws Exception{ 61 | List customers = new ArrayList<>(2); 62 | customers.add(new Customer().setSsn("ssn1").setName("Cust1").setCity("London")); 63 | customers.add(new Customer().setSsn("ssn2").setName("Cust2").setCity("London")); 64 | 65 | // given 66 | doReturn(customers).when(customerService).getAllCustomers(); 67 | 68 | // when, then 69 | mockMvc 70 | .perform(MockMvcRequestBuilders.get("/v1/customer/").accept(MediaType.APPLICATION_JSON)) 71 | .andExpect(status().isOk()) 72 | .andExpect(jsonPath("$", hasSize(2))) 73 | .andDo(print()); 74 | } 75 | 76 | /** 77 | * Get customer by ssn unit-test. 78 | * The test should result in returning customer's details for SSN:AK01 as provided by us as input. 79 | * 80 | * @throws Exception 81 | */ 82 | @Test 83 | public void ut2_GetCustomerBySsn() throws Exception{ 84 | Customer customer = new Customer() 85 | .setCustomerId(1L) 86 | .setName("Arpit K") 87 | .setSsn("AK01") 88 | .setAddress1("VT1") 89 | .setAddress2("Marine Bay1") 90 | .setCity("SpringCity") 91 | .setContactNumber("9425094250"); 92 | 93 | // given 94 | doReturn(customer).when(customerService).getCustomer("AK01"); 95 | 96 | //when, then 97 | mockMvc 98 | .perform(MockMvcRequestBuilders.get("/v1/customer/AK01").accept(MediaType.APPLICATION_JSON)) 99 | .andExpect(status().isOk()) 100 | .andExpect(jsonPath("$.customerId").exists()) 101 | .andExpect(jsonPath("$.name").exists()) 102 | .andExpect(jsonPath("$.ssn").exists()) 103 | .andExpect(jsonPath("$.address1").exists()) 104 | .andExpect(jsonPath("$.address2").exists()) 105 | .andExpect(jsonPath("$.city").exists()) 106 | .andExpect(jsonPath("$.contactNumber").exists()) 107 | .andExpect(jsonPath("$.customerId").value(1)) 108 | .andExpect(jsonPath("$.name").value("Arpit K")) 109 | .andExpect(jsonPath("$.ssn").value("AK01")) 110 | .andExpect(jsonPath("$.address1").value("VT1")) 111 | .andExpect(jsonPath("$.address2").value("Marine Bay1")) 112 | .andExpect(jsonPath("$.city").value("SpringCity")) 113 | .andExpect(jsonPath("$.contactNumber").value("9425094250")) 114 | .andDo(print()); 115 | } 116 | 117 | /** 118 | * Get customer by invalid ssn unit-test. 119 | * The test should result in returning a Http 404 for a customer with invalid ssn. 120 | * 121 | * @throws Exception 122 | */ 123 | @Test 124 | public void ut3_GetCustomerBySsn_InvalidSsn() throws Exception { 125 | // given 126 | doThrow(new EntityNotFoundException(Customer.class, "ssn", "AK02")).when(customerService).getCustomer("AK02"); 127 | 128 | // when, then 129 | mockMvc 130 | .perform(MockMvcRequestBuilders.get("/v1/customer/AK02").accept(MediaType.APPLICATION_JSON)) 131 | .andExpect(status().isNotFound()) 132 | .andExpect(jsonPath("$.apierror.status").value("NOT_FOUND")) 133 | .andExpect(jsonPath("$.apierror.message").value("Customer was not found for parameters {ssn=AK02}")) 134 | .andDo(print()); 135 | } 136 | 137 | /** 138 | * Create customer unit-test. 139 | * The test should result in adding a new customer with SSN:TK01 140 | * 141 | * @throws Exception 142 | */ 143 | @Test 144 | public void ut4_CreateCustomer() throws Exception{ 145 | Customer customer = new Customer() 146 | .setCustomerId(1L) 147 | .setName("Test Customer") 148 | .setSsn("TK01") 149 | .setAddress1("Unit-Test") 150 | .setAddress2("Spring-Boot") 151 | .setCity("SpringCity") 152 | .setContactNumber("9425094250"); 153 | 154 | // given 155 | doReturn(customer).when(customerService).createCustomer(mapper.map(customer, CustomerDto.class)); 156 | 157 | // when, then 158 | mockMvc 159 | .perform(MockMvcRequestBuilders.post("/v1/customer/create") 160 | .contentType(MediaType.APPLICATION_JSON) 161 | .content("{\"name\":\"Test Customer\", \"ssn\":\"TK01\", \"contactNumber\":\"9425094250\",\"address1\":\"Unit-Test\",\"address2\":\"Spring-Boot\",\"city\":\"SpringCity\"}") 162 | .accept(MediaType.APPLICATION_JSON)) 163 | .andExpect(status().isOk()) 164 | /*.andExpect(jsonPath("$.customerId").exists()) 165 | .andExpect(jsonPath("$.name").exists()) 166 | .andExpect(jsonPath("$.ssn").exists()) 167 | .andExpect(jsonPath("$.address1").exists()) 168 | .andExpect(jsonPath("$.address2").exists()) 169 | .andExpect(jsonPath("$.city").exists()) 170 | .andExpect(jsonPath("$.contactNumber").exists()) 171 | .andExpect(jsonPath("$.customerId").value(1)) 172 | .andExpect(jsonPath("$.name").value("Test Customer")) 173 | .andExpect(jsonPath("$.ssn").value("TK01")) 174 | .andExpect(jsonPath("$.address1").value("Unit-Test")) 175 | .andExpect(jsonPath("$.address2").value("Spring-Boot")) 176 | .andExpect(jsonPath("$.city").value("SpringCity")) 177 | .andExpect(jsonPath("$.contactNumber").value("9425094250"))*/ 178 | .andDo(print()); 179 | } 180 | 181 | 182 | } 183 | --------------------------------------------------------------------------------