From 00750812ad58c5e5847be6d5b614a97be18323c4 Mon Sep 17 00:00:00 2001 From: "arpit.khandelwal.1984" Date: Mon, 25 Jun 2018 16:17:19 +0530 Subject: [PATCH] Initial Commit --- .gitignore | 79 ++++++ .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 47610 bytes .mvn/wrapper/maven-wrapper.properties | 1 + LICENSE.md | 21 ++ mvnw | 225 +++++++++++++++++ mvnw.cmd | 143 +++++++++++ pom.xml | 103 ++++++++ readme.md | 37 +++ .../bankofspring/BankOfSpringApplication.java | 19 ++ src/main/java/com/bankofspring/api/.gitkeep | 0 .../api/v1/controller/AccountController.java | 101 ++++++++ .../api/v1/controller/CustomerController.java | 63 +++++ .../request/account/CreateAccountRequest.java | 35 +++ .../v1/request/account/DepositRequest.java | 26 ++ .../request/account/TransferFundRequest.java | 30 +++ .../v1/request/account/WithdrawalRequest.java | 26 ++ .../customer/CreateCustomerRequest.java | 30 +++ .../configuration/SwaggerConfig.java | 43 ++++ .../bankofspring/domain/model/Account.java | 66 +++++ .../domain/model/BaseDomainObject.java | 27 ++ .../com/bankofspring/domain/model/Branch.java | 30 +++ .../bankofspring/domain/model/Customer.java | 36 +++ .../domain/repository/AccountRepository.java | 13 + .../domain/repository/BranchRepository.java | 12 + .../domain/repository/CustomerRepository.java | 13 + .../java/com/bankofspring/dto/AccountDto.java | 31 +++ .../com/bankofspring/dto/CustomerDto.java | 29 +++ .../com/bankofspring/exception/ApiError.java | 151 ++++++++++++ .../bankofspring/exception/BankException.java | 10 + .../exception/DuplicateEntityException.java | 10 + .../exception/EntityException.java | 31 +++ .../exception/EntityNotFoundException.java | 11 + .../handler/RestExceptionHandler.java | 230 ++++++++++++++++++ .../service/account/AccountService.java | 28 +++ .../service/account/AccountServiceImpl.java | 116 +++++++++ .../account/exception/AccountException.java | 12 + .../exception/InsufficientFundsException.java | 14 ++ .../service/customer/CustomerService.java | 19 ++ .../service/customer/CustomerServiceImpl.java | 57 +++++ src/main/resources/application.properties | 9 + src/main/resources/data-h2.sql | 22 ++ .../BankOfSpringApplicationTests.java | 16 ++ 42 files changed, 1975 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 LICENSE.md create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 readme.md create mode 100644 src/main/java/com/bankofspring/BankOfSpringApplication.java create mode 100644 src/main/java/com/bankofspring/api/.gitkeep create mode 100644 src/main/java/com/bankofspring/api/v1/controller/AccountController.java create mode 100644 src/main/java/com/bankofspring/api/v1/controller/CustomerController.java create mode 100644 src/main/java/com/bankofspring/api/v1/request/account/CreateAccountRequest.java create mode 100644 src/main/java/com/bankofspring/api/v1/request/account/DepositRequest.java create mode 100644 src/main/java/com/bankofspring/api/v1/request/account/TransferFundRequest.java create mode 100644 src/main/java/com/bankofspring/api/v1/request/account/WithdrawalRequest.java create mode 100644 src/main/java/com/bankofspring/api/v1/request/customer/CreateCustomerRequest.java create mode 100644 src/main/java/com/bankofspring/configuration/SwaggerConfig.java create mode 100644 src/main/java/com/bankofspring/domain/model/Account.java create mode 100644 src/main/java/com/bankofspring/domain/model/BaseDomainObject.java create mode 100644 src/main/java/com/bankofspring/domain/model/Branch.java create mode 100644 src/main/java/com/bankofspring/domain/model/Customer.java create mode 100644 src/main/java/com/bankofspring/domain/repository/AccountRepository.java create mode 100644 src/main/java/com/bankofspring/domain/repository/BranchRepository.java create mode 100644 src/main/java/com/bankofspring/domain/repository/CustomerRepository.java create mode 100644 src/main/java/com/bankofspring/dto/AccountDto.java create mode 100644 src/main/java/com/bankofspring/dto/CustomerDto.java create mode 100644 src/main/java/com/bankofspring/exception/ApiError.java create mode 100644 src/main/java/com/bankofspring/exception/BankException.java create mode 100644 src/main/java/com/bankofspring/exception/DuplicateEntityException.java create mode 100644 src/main/java/com/bankofspring/exception/EntityException.java create mode 100644 src/main/java/com/bankofspring/exception/EntityNotFoundException.java create mode 100644 src/main/java/com/bankofspring/exception/handler/RestExceptionHandler.java create mode 100644 src/main/java/com/bankofspring/service/account/AccountService.java create mode 100644 src/main/java/com/bankofspring/service/account/AccountServiceImpl.java create mode 100644 src/main/java/com/bankofspring/service/account/exception/AccountException.java create mode 100644 src/main/java/com/bankofspring/service/account/exception/InsufficientFundsException.java create mode 100644 src/main/java/com/bankofspring/service/customer/CustomerService.java create mode 100644 src/main/java/com/bankofspring/service/customer/CustomerServiceImpl.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/data-h2.sql create mode 100644 src/test/java/com/bankofspring/BankOfSpringApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22a90b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Mac OS +.DS_Store + +# Logs +logs/ +.log \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9cc84ea9b4d95453115d0c26488d6a78694e0bc6 GIT binary patch literal 47610 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 literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b573bb5 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a44990d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2018] [Systango Technologies] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..5bf251c --- /dev/null +++ b/mvnw @@ -0,0 +1,225 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +echo $MAVEN_PROJECTBASEDIR +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..019bd74 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f895579 --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.systango + bankofspring + 0.0.1-SNAPSHOT + jar + + BankOfSpring + Production ready SpringBoot application with Txn management. + + + org.springframework.boot + spring-boot-starter-parent + 2.0.3.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-rest-hal-browser + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + + + org.apache.commons + commons-lang3 + 3.5 + + + org.projectlombok + lombok + true + + + org.modelmapper + modelmapper + 2.0.0 + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..30e5aac --- /dev/null +++ b/readme.md @@ -0,0 +1,37 @@ +![BankOfSpring](https://github.com/SystangoTechnologies/Springboard/blob/master/src/main/resources/static/img/springboard-logo.png) + +## BankOfSpring +Production ready maven based Spring Boot starter kit application with example cases of handling transactions with Spring. + +## Description +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. + +## Technology + +- **Spring Boot** - Server side framework +- **JPA** - Entity framework +- **Lombok** - Provides automated getter/setters +- **Actuator** - Application insights on the fly +- **Spring Security** - Spring's security layer +- **Thymeleaf** - Template Engine +- **Devtools** - Support Hot-Code Swapping with live browser reload +- **JJWT** - JWT tokens for API authentication +- **Swagger** - In-built swagger2 documentation support +- **Docker** - Docker containers +- **Junit** - Unit testing framework + +## Application Structure + +## Running the server locally + +## Docker + +## Unit test cases + +## Integration test cases + +## Contributors +[Arpit Khandelwal](https://www.linkedin.com/in/arpitkhandelwal1984/) + +## License +This project is licensed under the terms of the MIT license. \ No newline at end of file diff --git a/src/main/java/com/bankofspring/BankOfSpringApplication.java b/src/main/java/com/bankofspring/BankOfSpringApplication.java new file mode 100644 index 0000000..ab851c6 --- /dev/null +++ b/src/main/java/com/bankofspring/BankOfSpringApplication.java @@ -0,0 +1,19 @@ +package com.bankofspring; + +import org.modelmapper.ModelMapper; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class BankOfSpringApplication { + + public static void main(String[] args) { + SpringApplication.run(BankOfSpringApplication.class, args); + } + + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } +} diff --git a/src/main/java/com/bankofspring/api/.gitkeep b/src/main/java/com/bankofspring/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/bankofspring/api/v1/controller/AccountController.java b/src/main/java/com/bankofspring/api/v1/controller/AccountController.java new file mode 100644 index 0000000..a79755f --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/controller/AccountController.java @@ -0,0 +1,101 @@ +package com.bankofspring.api.v1.controller; + +import com.bankofspring.api.v1.request.account.CreateAccountRequest; +import com.bankofspring.api.v1.request.account.DepositRequest; +import com.bankofspring.api.v1.request.account.WithdrawalRequest; +import com.bankofspring.domain.model.Account; +import com.bankofspring.dto.AccountDto; +import com.bankofspring.exception.EntityException; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.service.account.AccountService; +import com.bankofspring.service.account.exception.InsufficientFundsException; +import com.bankofspring.api.v1.request.account.TransferFundRequest; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by Arpit Khandelwal. + */ +@RestController +@RequestMapping("/v1/account") +public class AccountController { + + @Autowired + AccountService accountService; + + @Autowired + ModelMapper modelMapper; + + @GetMapping(value = "/") + public List getAccounts() { + List accounts = accountService.getAllAccounts(); + if (accounts != null && !accounts.isEmpty()) { + List accountDtos = accounts + .stream() + .map(account -> modelMapper.map(account, AccountDto.class)) + .collect(Collectors.toList()); + return accountDtos; + } + return Collections.emptyList(); + } + + @GetMapping(value = "/{accountNumber}") + public AccountDto getAccountByNumber(@PathVariable("accountNumber") Long accountNumber) throws EntityNotFoundException { + Account account = accountService.getAccount(accountNumber); + if (account != null) { + AccountDto accountDto = modelMapper.map(account, AccountDto.class); + return accountDto; + } + return null; + } + + @PostMapping("/create") + public AccountDto createAccount(@RequestBody @Valid CreateAccountRequest createAccountRequest) throws EntityException { + AccountDto accountDto = modelMapper.map(createAccountRequest, AccountDto.class); + Account account = accountService.createAccount(accountDto); + if (account != null) { + AccountDto resultAccount = modelMapper.map(account, AccountDto.class); + return resultAccount; + } + return null; + } + + @PostMapping("/deposit") + public AccountDto depositMoney(@RequestBody @Valid DepositRequest depositRequest) throws EntityNotFoundException { + AccountDto accountDto = modelMapper.map(depositRequest, AccountDto.class); + Account account = accountService.creditAmount(accountDto, depositRequest.getDepositAmt()); + if (account != null) { + AccountDto resultAccount = modelMapper.map(account, AccountDto.class); + return resultAccount; + } + return null; + } + + @PostMapping("/withdraw") + public AccountDto withdrawMoney(@RequestBody @Valid WithdrawalRequest withdrawalRequest) throws EntityNotFoundException, InsufficientFundsException { + AccountDto accountDto = modelMapper.map(withdrawalRequest, AccountDto.class); + Account account = accountService.debitAmount(accountDto, withdrawalRequest.getWithdrawlAmt()); + if (account != null) { + AccountDto resultAccount = modelMapper.map(account, AccountDto.class); + return resultAccount; + } + return null; + } + + @PostMapping("/transfer") + public List transferMoney(@RequestBody @Valid TransferFundRequest transferFundRequest) throws InsufficientFundsException, EntityNotFoundException { + AccountDto debitAccountDto = new AccountDto().setAccountNumber(transferFundRequest.getDebitAccountNumber()); + AccountDto creditAccountDto = new AccountDto().setAccountNumber(transferFundRequest.getCreditAccountNumber()); + List accounts = accountService.transferFunds(debitAccountDto,creditAccountDto,transferFundRequest.getAmount()); + return accounts + .stream() + .map(account -> modelMapper.map(account, AccountDto.class)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/bankofspring/api/v1/controller/CustomerController.java b/src/main/java/com/bankofspring/api/v1/controller/CustomerController.java new file mode 100644 index 0000000..e75c519 --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/controller/CustomerController.java @@ -0,0 +1,63 @@ +package com.bankofspring.api.v1.controller; + +import com.bankofspring.api.v1.request.customer.CreateCustomerRequest; +import com.bankofspring.domain.model.Customer; +import com.bankofspring.dto.CustomerDto; +import com.bankofspring.exception.EntityException; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.service.customer.CustomerService; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by Arpit Khandelwal. + */ +@RestController +@RequestMapping("/v1/customer") +public class CustomerController { + @Autowired + CustomerService customerService; + + @Autowired + ModelMapper mapper; + + @GetMapping(value = "/") + public List getAllCustomers() { + List customers = customerService.getAllCustomers(); + if (customers != null && !customers.isEmpty()) { + List customerDtos = customers + .stream() + .map(customer -> mapper.map(customer, CustomerDto.class)) + .collect(Collectors.toList()); + return customerDtos; + } + return Collections.emptyList(); + } + + @GetMapping(value = "/{ssn}") + public CustomerDto getCustomer(@PathVariable("ssn") String ssn) throws EntityNotFoundException { + Customer customer = customerService.getCustomer(ssn); + if (customer != null) { + CustomerDto resultCustomer = mapper.map(customer, CustomerDto.class); + return resultCustomer; + } + return null; + } + + @PostMapping("/create") + public CustomerDto createCustomer(@RequestBody @Valid CreateCustomerRequest createCustomerRequest) throws EntityException { + CustomerDto customerDto = mapper.map(createCustomerRequest, CustomerDto.class); + Customer customer = customerService.createCustomer(customerDto); + if (customer != null) { + CustomerDto resultCustomer = mapper.map(customer, CustomerDto.class); + return resultCustomer; + } + return null; + } +} diff --git a/src/main/java/com/bankofspring/api/v1/request/account/CreateAccountRequest.java b/src/main/java/com/bankofspring/api/v1/request/account/CreateAccountRequest.java new file mode 100644 index 0000000..4ee062a --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/request/account/CreateAccountRequest.java @@ -0,0 +1,35 @@ +package com.bankofspring.api.v1.request.account; + +import com.bankofspring.domain.model.Account; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateAccountRequest { + + @NotNull(message = "{constraints.NotEmpty.message}") + private Long customerId; + + @NotNull(message = "{constraints.NotEmpty.message}") + private Account.AccountType type; + + @NotNull(message = "{constraints.NotEmpty.message}") + private Long branchId; + + @NotNull(message = "{constraints.NotEmpty.message}") + private BigDecimal balance; + +} diff --git a/src/main/java/com/bankofspring/api/v1/request/account/DepositRequest.java b/src/main/java/com/bankofspring/api/v1/request/account/DepositRequest.java new file mode 100644 index 0000000..a2043e2 --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/request/account/DepositRequest.java @@ -0,0 +1,26 @@ +package com.bankofspring.api.v1.request.account; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class DepositRequest { + @NotNull(message = "{constraints.NotEmpty.message}") + private Long accountNumber; + + @NotNull(message = "{constraints.NotEmpty.message}") + private BigDecimal depositAmt; +} diff --git a/src/main/java/com/bankofspring/api/v1/request/account/TransferFundRequest.java b/src/main/java/com/bankofspring/api/v1/request/account/TransferFundRequest.java new file mode 100644 index 0000000..6af1790 --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/request/account/TransferFundRequest.java @@ -0,0 +1,30 @@ +package com.bankofspring.api.v1.request.account; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransferFundRequest { + + @NotNull(message = "{constraints.NotEmpty.message}") + private Long debitAccountNumber; + + @NotNull(message = "{constraints.NotEmpty.message}") + private Long creditAccountNumber; + + @NotNull(message = "{constraints.NotEmpty.message}") + private BigDecimal amount; +} diff --git a/src/main/java/com/bankofspring/api/v1/request/account/WithdrawalRequest.java b/src/main/java/com/bankofspring/api/v1/request/account/WithdrawalRequest.java new file mode 100644 index 0000000..37608cb --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/request/account/WithdrawalRequest.java @@ -0,0 +1,26 @@ +package com.bankofspring.api.v1.request.account; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class WithdrawalRequest { + @NotNull(message = "{constraints.NotEmpty.message}") + private Long accountNumber; + + @NotNull(message = "{constraints.NotEmpty.message}") + private BigDecimal withdrawlAmt; +} diff --git a/src/main/java/com/bankofspring/api/v1/request/customer/CreateCustomerRequest.java b/src/main/java/com/bankofspring/api/v1/request/customer/CreateCustomerRequest.java new file mode 100644 index 0000000..6df072b --- /dev/null +++ b/src/main/java/com/bankofspring/api/v1/request/customer/CreateCustomerRequest.java @@ -0,0 +1,30 @@ +package com.bankofspring.api.v1.request.customer; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateCustomerRequest { + @NotNull(message = "{constraints.NotEmpty.message}") + private String name; + + @NotNull(message = "{constraints.NotEmpty.message}") + private String ssn; + + private String address1; + private String address2; + private String city; + private String contactNumber; +} diff --git a/src/main/java/com/bankofspring/configuration/SwaggerConfig.java b/src/main/java/com/bankofspring/configuration/SwaggerConfig.java new file mode 100644 index 0000000..36efbd8 --- /dev/null +++ b/src/main/java/com/bankofspring/configuration/SwaggerConfig.java @@ -0,0 +1,43 @@ +package com.bankofspring.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * Created by Arpit Khandelwal. + */ +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(getApiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.systango.springBootBank.api.v1.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo getApiInfo() { + Contact contact = new Contact("Arpit Khandelwal", "http://www.systango.com", "arpit@systango.com"); + return new ApiInfoBuilder() + .title("SpringBoot Bank") + .description("Bank Api Definition") + .version("1.0.0") + .license("MIT") + .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0") + .contact(contact) + .build(); + } + + +} diff --git a/src/main/java/com/bankofspring/domain/model/Account.java b/src/main/java/com/bankofspring/domain/model/Account.java new file mode 100644 index 0000000..8e9a466 --- /dev/null +++ b/src/main/java/com/bankofspring/domain/model/Account.java @@ -0,0 +1,66 @@ +package com.bankofspring.domain.model; + +import com.bankofspring.service.account.exception.InsufficientFundsException; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Setter +@Getter +@Accessors(chain = true) +@NoArgsConstructor +@Entity +public class Account extends BaseDomainObject { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long accountNumber; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "branchId", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Branch coreBranch; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customerId", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Customer accountOwner; + + private BigDecimal balance; + + private AccountType type; + + /** + * Debits the given amount from current account balance. + * @param debitAmount + * @throws InsufficientFundsException + */ + public void debit(BigDecimal debitAmount) throws InsufficientFundsException { + if(this.balance.compareTo(debitAmount) >= 0){ + this.balance = this.balance.subtract(debitAmount); + return; + } + throw new InsufficientFundsException(this,debitAmount); + } + + /** + * Credits the given amount to current account balance + * @param creditAmount + */ + public void credit(BigDecimal creditAmount) { + this.balance = this.balance.add(creditAmount); + } + + public enum AccountType { + SAVINGS, CURRENT, LOAN; + } +} diff --git a/src/main/java/com/bankofspring/domain/model/BaseDomainObject.java b/src/main/java/com/bankofspring/domain/model/BaseDomainObject.java new file mode 100644 index 0000000..5e9c39b --- /dev/null +++ b/src/main/java/com/bankofspring/domain/model/BaseDomainObject.java @@ -0,0 +1,27 @@ +package com.bankofspring.domain.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import javax.persistence.MappedSuperclass; +import java.io.Serializable; +import java.util.Date; + +/** + * Represents all common properties of standard domain objects + * + * @author Arpit Khandelwal + */ +@MappedSuperclass +@Data +@NoArgsConstructor +public abstract class BaseDomainObject implements Serializable { + + @CreationTimestamp + protected Date createTimestamp = new Date(); + + @UpdateTimestamp + protected Date lastEditTimestamp; +} diff --git a/src/main/java/com/bankofspring/domain/model/Branch.java b/src/main/java/com/bankofspring/domain/model/Branch.java new file mode 100644 index 0000000..28cf43f --- /dev/null +++ b/src/main/java/com/bankofspring/domain/model/Branch.java @@ -0,0 +1,30 @@ +package com.bankofspring.domain.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@Entity +public class Branch extends BaseDomainObject { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long branchId; + + private String name; + private String city; + private BigDecimal assets; +} diff --git a/src/main/java/com/bankofspring/domain/model/Customer.java b/src/main/java/com/bankofspring/domain/model/Customer.java new file mode 100644 index 0000000..1f9042a --- /dev/null +++ b/src/main/java/com/bankofspring/domain/model/Customer.java @@ -0,0 +1,36 @@ +package com.bankofspring.domain.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import javax.persistence.*; +import java.util.Set; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@Entity +public class Customer extends BaseDomainObject { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long customerId; + + @Column(unique = true) + private String ssn; + + @OneToMany(cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + mappedBy = "accountOwner") + private Set accounts; + + private String name; + private String address1; + private String address2; + private String city; + private String contactNumber; +} diff --git a/src/main/java/com/bankofspring/domain/repository/AccountRepository.java b/src/main/java/com/bankofspring/domain/repository/AccountRepository.java new file mode 100644 index 0000000..f5b29cb --- /dev/null +++ b/src/main/java/com/bankofspring/domain/repository/AccountRepository.java @@ -0,0 +1,13 @@ +package com.bankofspring.domain.repository; + +import com.bankofspring.domain.model.Account; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * Created by Arpit Khandelwal. + */ +@Repository +public interface AccountRepository extends CrudRepository { + +} diff --git a/src/main/java/com/bankofspring/domain/repository/BranchRepository.java b/src/main/java/com/bankofspring/domain/repository/BranchRepository.java new file mode 100644 index 0000000..a1f42d4 --- /dev/null +++ b/src/main/java/com/bankofspring/domain/repository/BranchRepository.java @@ -0,0 +1,12 @@ +package com.bankofspring.domain.repository; + +import com.bankofspring.domain.model.Branch; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * Created by Arpit Khandelwal. + */ +@Repository +public interface BranchRepository extends CrudRepository { +} diff --git a/src/main/java/com/bankofspring/domain/repository/CustomerRepository.java b/src/main/java/com/bankofspring/domain/repository/CustomerRepository.java new file mode 100644 index 0000000..b2d8ec0 --- /dev/null +++ b/src/main/java/com/bankofspring/domain/repository/CustomerRepository.java @@ -0,0 +1,13 @@ +package com.bankofspring.domain.repository; + +import com.bankofspring.domain.model.Customer; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * Created by Arpit Khandelwal. + */ +@Repository +public interface CustomerRepository extends CrudRepository { + Customer findBySsn(String ssn); +} diff --git a/src/main/java/com/bankofspring/dto/AccountDto.java b/src/main/java/com/bankofspring/dto/AccountDto.java new file mode 100644 index 0000000..0bf6f0c --- /dev/null +++ b/src/main/java/com/bankofspring/dto/AccountDto.java @@ -0,0 +1,31 @@ +package com.bankofspring.dto; + +import com.bankofspring.domain.model.Account; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@ToString +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccountDto { + private Long accountNumber; + private Long customerId; + private Long branchId; + private Account.AccountType type; + private BigDecimal balance; + private CustomerDto accountOwner; +} diff --git a/src/main/java/com/bankofspring/dto/CustomerDto.java b/src/main/java/com/bankofspring/dto/CustomerDto.java new file mode 100644 index 0000000..74b677b --- /dev/null +++ b/src/main/java/com/bankofspring/dto/CustomerDto.java @@ -0,0 +1,29 @@ +package com.bankofspring.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * Created by Arpit Khandelwal. + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@ToString +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class CustomerDto { + private Long customerId; + private String name; + private String ssn; + private String address1; + private String address2; + private String city; + private String contactNumber; +} diff --git a/src/main/java/com/bankofspring/exception/ApiError.java b/src/main/java/com/bankofspring/exception/ApiError.java new file mode 100644 index 0000000..c47d0ec --- /dev/null +++ b/src/main/java/com/bankofspring/exception/ApiError.java @@ -0,0 +1,151 @@ +package com.bankofspring.exception; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import javax.validation.ConstraintViolation; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Created by Arpit Khandelwal. + */ +@Data +@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CUSTOM, property = "error", visible = true) +@JsonTypeIdResolver(LowerCaseClassNameResolver.class) +public class ApiError { + + private HttpStatus status; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") + private LocalDateTime timestamp; + private String message; + private String debugMessage; + private List subErrors; + + private ApiError() { + timestamp = LocalDateTime.now(); + } + + public ApiError(HttpStatus status) { + this(); + this.status = status; + } + + public ApiError(HttpStatus status, Throwable ex) { + this(); + this.status = status; + this.message = "Unexpected error"; + this.debugMessage = ex.getLocalizedMessage(); + } + + public ApiError(HttpStatus status, String message, Throwable ex) { + this(); + this.status = status; + this.message = message; + this.debugMessage = ex.getLocalizedMessage(); + } + + private void addSubError(ApiSubError subError) { + if (subErrors == null) { + subErrors = new ArrayList<>(); + } + subErrors.add(subError); + } + + private void addValidationError(String object, String field, Object rejectedValue, String message) { + addSubError(new ApiValidationError(object, field, rejectedValue, message)); + } + + private void addValidationError(String object, String message) { + addSubError(new ApiValidationError(object, message)); + } + + private void addValidationError(FieldError fieldError) { + this.addValidationError( + fieldError.getObjectName(), + fieldError.getField(), + fieldError.getRejectedValue(), + fieldError.getDefaultMessage()); + } + + public void addValidationErrors(List fieldErrors) { + fieldErrors.forEach(this::addValidationError); + } + + private void addValidationError(ObjectError objectError) { + this.addValidationError( + objectError.getObjectName(), + objectError.getDefaultMessage()); + } + + public void addValidationError(List globalErrors) { + globalErrors.forEach(this::addValidationError); + } + + /** + * Utility method for adding error of ConstraintViolation. Usually when a @Validated validation fails. + * + * @param cv the ConstraintViolation + */ + private void addValidationError(ConstraintViolation cv) { + this.addValidationError( + cv.getRootBeanClass().getSimpleName(), + ((PathImpl) cv.getPropertyPath()).getLeafNode().asString(), + cv.getInvalidValue(), + cv.getMessage()); + } + + public void addValidationErrors(Set> constraintViolations) { + constraintViolations.forEach(this::addValidationError); + } + + + abstract class ApiSubError { + + } + + @Data + @EqualsAndHashCode(callSuper = false) + @AllArgsConstructor + class ApiValidationError extends ApiSubError { + private String object; + private String field; + private Object rejectedValue; + private String message; + + ApiValidationError(String object, String message) { + this.object = object; + this.message = message; + } + } +} + +class LowerCaseClassNameResolver extends TypeIdResolverBase { + + @Override + public String idFromValue(Object value) { + return value.getClass().getSimpleName().toLowerCase(); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return idFromValue(value); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } +} diff --git a/src/main/java/com/bankofspring/exception/BankException.java b/src/main/java/com/bankofspring/exception/BankException.java new file mode 100644 index 0000000..b8c48f8 --- /dev/null +++ b/src/main/java/com/bankofspring/exception/BankException.java @@ -0,0 +1,10 @@ +package com.bankofspring.exception; + +/** + * Created by Arpit Khandelwal. + */ +public class BankException extends Exception { + public BankException (String message){ + super(message); + } +} diff --git a/src/main/java/com/bankofspring/exception/DuplicateEntityException.java b/src/main/java/com/bankofspring/exception/DuplicateEntityException.java new file mode 100644 index 0000000..efa534c --- /dev/null +++ b/src/main/java/com/bankofspring/exception/DuplicateEntityException.java @@ -0,0 +1,10 @@ +package com.bankofspring.exception; + +/** + * Created by Arpit Khandelwal. + */ +public class DuplicateEntityException extends EntityException { + public DuplicateEntityException(Class clazz, String... searchParamsMap) { + super(clazz, " was already found for parameters ", searchParamsMap); + } +} diff --git a/src/main/java/com/bankofspring/exception/EntityException.java b/src/main/java/com/bankofspring/exception/EntityException.java new file mode 100644 index 0000000..e13f74d --- /dev/null +++ b/src/main/java/com/bankofspring/exception/EntityException.java @@ -0,0 +1,31 @@ +package com.bankofspring.exception; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.IntStream; + +/** + * Created by Arpit Khandelwal. + */ +public class EntityException extends Exception { + + public EntityException(Class clazz, String indicator, String... searchParamsMap) { + super(EntityException.generateMessage(clazz.getSimpleName(), indicator, toMap(String.class, String.class, searchParamsMap))); + } + + private static String generateMessage(String entity, String indicator, Map searchParams) { + return StringUtils.capitalize(entity) + indicator + searchParams; + } + + private static Map toMap( + Class keyType, Class valueType, Object... entries) { + if (entries.length % 2 == 1) + throw new IllegalArgumentException("Invalid entries"); + return IntStream.range(0, entries.length / 2).map(i -> i * 2) + .collect(HashMap::new, + (m, i) -> m.put(keyType.cast(entries[i]), valueType.cast(entries[i + 1])), + Map::putAll); + } +} diff --git a/src/main/java/com/bankofspring/exception/EntityNotFoundException.java b/src/main/java/com/bankofspring/exception/EntityNotFoundException.java new file mode 100644 index 0000000..4a3b2ac --- /dev/null +++ b/src/main/java/com/bankofspring/exception/EntityNotFoundException.java @@ -0,0 +1,11 @@ +package com.bankofspring.exception; + +/** + * Created by Arpit Khandelwal. + */ +public class EntityNotFoundException extends EntityException { + + public EntityNotFoundException(Class clazz, String... searchParamsMap) { + super(clazz, " was not found for parameters ", searchParamsMap); + } +} diff --git a/src/main/java/com/bankofspring/exception/handler/RestExceptionHandler.java b/src/main/java/com/bankofspring/exception/handler/RestExceptionHandler.java new file mode 100644 index 0000000..25083d7 --- /dev/null +++ b/src/main/java/com/bankofspring/exception/handler/RestExceptionHandler.java @@ -0,0 +1,230 @@ +package com.bankofspring.exception.handler; + + +import com.bankofspring.exception.ApiError; +import com.bankofspring.exception.DuplicateEntityException; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.exception.BankException; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * Created by Arpit Khandelwal. + */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@ControllerAdvice +public class RestExceptionHandler extends ResponseEntityExceptionHandler { + + /** + * Handle MissingServletRequestParameterException. Triggered when a 'required' request parameter is missing. + * + * @param ex MissingServletRequestParameterException + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + String error = ex.getParameterName() + " parameter is missing"; + return buildResponseEntity(new ApiError(BAD_REQUEST, error, ex)); + } + + + /** + * Handle HttpMediaTypeNotSupportedException. This one triggers when JSON is invalid as well. + * + * @param ex HttpMediaTypeNotSupportedException + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleHttpMediaTypeNotSupported( + HttpMediaTypeNotSupportedException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + StringBuilder builder = new StringBuilder(); + builder.append(ex.getContentType()); + builder.append(" media type is not supported. Supported media types are "); + ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); + return buildResponseEntity(new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex)); + } + + /** + * Handle MethodArgumentNotValidException. Triggered when an object fails @Valid validation. + * + * @param ex the MethodArgumentNotValidException that is thrown when @Valid validation fails + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage("Validation error"); + apiError.addValidationErrors(ex.getBindingResult().getFieldErrors()); + apiError.addValidationError(ex.getBindingResult().getGlobalErrors()); + return buildResponseEntity(apiError); + } + + /** + * Handles javax.validation.ConstraintViolationException. Thrown when @Validated fails. + * + * @param ex the ConstraintViolationException + * @return the ApiError object + */ + @ExceptionHandler(javax.validation.ConstraintViolationException.class) + protected ResponseEntity handleConstraintViolation( + javax.validation.ConstraintViolationException ex) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage("Validation error"); + apiError.addValidationErrors(ex.getConstraintViolations()); + return buildResponseEntity(apiError); + } + + /** + * Handles EntityNotFoundException. Created to encapsulate errors with more detail than javax.persistence.EntityNotFoundException. + * + * @param ex the EntityNotFoundException + * @return the ApiError object + */ + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFound( + EntityNotFoundException ex) { + ApiError apiError = new ApiError(NOT_FOUND); + apiError.setMessage(ex.getMessage()); + return buildResponseEntity(apiError); + } + + /** + * Handles DuplicateEntityException. Created to encapsulate errors for multiple entities. + * + * @param ex the DuplicateEntityException + * @return the ApiError object + */ + @ExceptionHandler(DuplicateEntityException.class) + protected ResponseEntity handleDuplicateEntityFound( + DuplicateEntityException ex) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage(ex.getMessage()); + return buildResponseEntity(apiError); + } + + /** + * Handles all the generic exceptions thrown by SpringBootBank + * @param ex the BankException + * @return the ApiError object + */ + @ExceptionHandler(BankException.class) + protected ResponseEntity handleBankException( + BankException ex) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage(ex.getMessage()); + return buildResponseEntity(apiError); + } + + /** + * Handle HttpMessageNotReadableException. Happens when request JSON is malformed. + * + * @param ex HttpMessageNotReadableException + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + ServletWebRequest servletWebRequest = (ServletWebRequest) request; + //log.info("{} to {}", servletWebRequest.getHttpMethod(), servletWebRequest.getRequest().getServletPath()); + String error = "Malformed JSON request"; + return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); + } + + /** + * Handle HttpMessageNotWritableException. + * + * @param ex HttpMessageNotWritableException + * @param headers HttpHeaders + * @param status HttpStatus + * @param request WebRequest + * @return the ApiError object + */ + @Override + protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + String error = "Error writing JSON output"; + return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, error, ex)); + } + + /** + * Handle javax.persistence.EntityNotFoundException + */ + @ExceptionHandler(javax.persistence.EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFound(javax.persistence.EntityNotFoundException ex) { + return buildResponseEntity(new ApiError(HttpStatus.NOT_FOUND, ex)); + } + + /** + * Handle DataIntegrityViolationException, inspects the cause for different DB causes. + * + * @param ex the DataIntegrityViolationException + * @return the ApiError object + */ + @ExceptionHandler(DataIntegrityViolationException.class) + protected ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException ex, + WebRequest request) { + if (ex.getCause() instanceof ConstraintViolationException) { + return buildResponseEntity(new ApiError(HttpStatus.CONFLICT, "Database error", ex.getCause())); + } + return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex)); + } + + /** + * Handle Exception, handle generic Exception.class + * + * @param ex the Exception + * @return the ApiError object + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, + WebRequest request) { + ApiError apiError = new ApiError(BAD_REQUEST); + apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName())); + apiError.setDebugMessage(ex.getMessage()); + return buildResponseEntity(apiError); + } + + + private ResponseEntity buildResponseEntity(ApiError apiError) { + return new ResponseEntity<>(apiError, apiError.getStatus()); + } + +} diff --git a/src/main/java/com/bankofspring/service/account/AccountService.java b/src/main/java/com/bankofspring/service/account/AccountService.java new file mode 100644 index 0000000..15c278d --- /dev/null +++ b/src/main/java/com/bankofspring/service/account/AccountService.java @@ -0,0 +1,28 @@ +package com.bankofspring.service.account; + +import com.bankofspring.domain.model.Account; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.service.account.exception.InsufficientFundsException; +import com.bankofspring.dto.AccountDto; +import com.bankofspring.exception.EntityException; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Created by Arpit Khandelwal. + */ +public interface AccountService { + Account createAccount(AccountDto accountDto) throws EntityException; + + List getAllAccounts(); + + Account getAccount(Long accountNumber) throws EntityNotFoundException; + + Account creditAmount(AccountDto accountDto, BigDecimal depositAmt) throws EntityNotFoundException; + + Account debitAmount(AccountDto accountDto, BigDecimal withdrawalAmt) throws EntityNotFoundException, InsufficientFundsException; + + List transferFunds(AccountDto fromAccountDto, AccountDto toAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException; + +} diff --git a/src/main/java/com/bankofspring/service/account/AccountServiceImpl.java b/src/main/java/com/bankofspring/service/account/AccountServiceImpl.java new file mode 100644 index 0000000..4ddf7e0 --- /dev/null +++ b/src/main/java/com/bankofspring/service/account/AccountServiceImpl.java @@ -0,0 +1,116 @@ +package com.bankofspring.service.account; + +import com.bankofspring.domain.model.Account; +import com.bankofspring.domain.model.Branch; +import com.bankofspring.domain.model.Customer; +import com.bankofspring.domain.repository.CustomerRepository; +import com.bankofspring.exception.EntityException; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.service.account.exception.InsufficientFundsException; +import com.bankofspring.domain.repository.AccountRepository; +import com.bankofspring.domain.repository.BranchRepository; +import com.bankofspring.dto.AccountDto; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Created by Arpit Khandelwal. + */ +@Service +public class AccountServiceImpl implements AccountService { + + @Autowired + AccountRepository accountRepository; + + @Autowired + CustomerRepository customerRepository; + + @Autowired + BranchRepository branchRepository; + + @Autowired + ModelMapper mapper; + + @Override + public List getAllAccounts() { + Iterator iteratorToCollection = accountRepository.findAll().iterator(); + return StreamSupport.stream( + Spliterators + .spliteratorUnknownSize(iteratorToCollection, Spliterator.ORDERED), false) + .collect(Collectors.toList() + ); + } + + @Override + public Account getAccount(Long accountNumber) throws EntityNotFoundException { + Optional accountInDb = accountRepository.findById(accountNumber); + if (accountInDb.isPresent()) { + return accountInDb.get(); + } + throw new EntityNotFoundException(Account.class, "accountNumber", accountNumber.toString()); + } + + @Override + @Transactional + public Account createAccount(AccountDto accountDto) throws EntityException { + Optional customer = customerRepository.findById(accountDto.getCustomerId()); + Optional branch = branchRepository.findById(accountDto.getBranchId()); + if (customer.isPresent()) { + if (branch.isPresent()) { + Account account = mapper.map(accountDto, Account.class); + account.setCoreBranch(branch.get()); + account.setAccountOwner(customer.get()); + return accountRepository.save(account); + } + throw new EntityNotFoundException(Branch.class, "branchId", accountDto.getBranchId().toString()); + } + throw new EntityNotFoundException(Customer.class, "customerId", accountDto.getCustomerId().toString()); + } + + @Override + @Transactional(propagation=Propagation.REQUIRED, rollbackFor = EntityNotFoundException.class, isolation= Isolation.READ_COMMITTED) + public Account creditAmount(AccountDto accountDto, BigDecimal creditAmt) throws EntityNotFoundException { + assert(creditAmt.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 + Optional accountInDb = accountRepository.findById(accountDto.getAccountNumber()); + if (accountInDb.isPresent()) { + Account account = accountInDb.get(); + account.credit(creditAmt); + return accountRepository.save(account); + } + throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString()); + } + + @Override + @Transactional(propagation= Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED) + public Account debitAmount(AccountDto accountDto, BigDecimal debitAmt) throws EntityNotFoundException, InsufficientFundsException { + assert(debitAmt.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 + Optional accountInDb = accountRepository.findById(accountDto.getAccountNumber()); + if (accountInDb.isPresent()) { + Account account = accountInDb.get(); + account.debit(debitAmt); + return accountRepository.save(account); + } + throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString()); + } + + @Override + @Transactional(propagation=Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED) + public List transferFunds(AccountDto debitAccountDto, AccountDto creditAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException { + assert(amount.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0 + Account debitAccount = debitAmount(debitAccountDto, amount); + Account creditAccount = creditAmount(creditAccountDto, amount); + return Stream + .of(debitAccount, creditAccount) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/bankofspring/service/account/exception/AccountException.java b/src/main/java/com/bankofspring/service/account/exception/AccountException.java new file mode 100644 index 0000000..af73185 --- /dev/null +++ b/src/main/java/com/bankofspring/service/account/exception/AccountException.java @@ -0,0 +1,12 @@ +package com.bankofspring.service.account.exception; + +import com.bankofspring.exception.BankException; + +/** + * Created by Arpit Khandelwal. + */ +public class AccountException extends BankException { + public AccountException(String message){ + super(message); + } +} diff --git a/src/main/java/com/bankofspring/service/account/exception/InsufficientFundsException.java b/src/main/java/com/bankofspring/service/account/exception/InsufficientFundsException.java new file mode 100644 index 0000000..07524f9 --- /dev/null +++ b/src/main/java/com/bankofspring/service/account/exception/InsufficientFundsException.java @@ -0,0 +1,14 @@ +package com.bankofspring.service.account.exception; + +import com.bankofspring.domain.model.Account; + +import java.math.BigDecimal; + +/** + * Created by Arpit Khandelwal. + */ +public class InsufficientFundsException extends AccountException { + public InsufficientFundsException(Account account, BigDecimal withdrawalAmt){ + super("Insufficient funds in account number - " + account.getAccountNumber() + ". Cannot allow withdrawal of $" + withdrawalAmt + "."); + } +} diff --git a/src/main/java/com/bankofspring/service/customer/CustomerService.java b/src/main/java/com/bankofspring/service/customer/CustomerService.java new file mode 100644 index 0000000..87b1156 --- /dev/null +++ b/src/main/java/com/bankofspring/service/customer/CustomerService.java @@ -0,0 +1,19 @@ +package com.bankofspring.service.customer; + +import com.bankofspring.domain.model.Customer; +import com.bankofspring.dto.CustomerDto; +import com.bankofspring.exception.EntityNotFoundException; +import com.bankofspring.exception.EntityException; + +import java.util.List; + +/** + * Created by Arpit Khandelwal. + */ +public interface CustomerService { + Customer createCustomer(CustomerDto customerDto) throws EntityException; + + Customer getCustomer(String ssn) throws EntityNotFoundException; + + List getAllCustomers(); +} diff --git a/src/main/java/com/bankofspring/service/customer/CustomerServiceImpl.java b/src/main/java/com/bankofspring/service/customer/CustomerServiceImpl.java new file mode 100644 index 0000000..7472334 --- /dev/null +++ b/src/main/java/com/bankofspring/service/customer/CustomerServiceImpl.java @@ -0,0 +1,57 @@ +package com.bankofspring.service.customer; + +import com.bankofspring.domain.model.Customer; +import com.bankofspring.domain.repository.CustomerRepository; +import com.bankofspring.dto.CustomerDto; +import com.bankofspring.exception.DuplicateEntityException; +import com.bankofspring.exception.EntityException; +import com.bankofspring.exception.EntityNotFoundException; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Created by Arpit Khandelwal. + */ +@Service +public class CustomerServiceImpl implements CustomerService { + @Autowired + CustomerRepository customerRepository; + + @Autowired + ModelMapper mapper; + + @Override + public List getAllCustomers() { + Iterator iteratorToCollection = customerRepository.findAll().iterator(); + return StreamSupport.stream( + Spliterators + .spliteratorUnknownSize(iteratorToCollection, Spliterator.ORDERED), false) + .collect(Collectors.toList() + ); + } + + @Override + public Customer getCustomer(String ssn) throws EntityNotFoundException { + Customer customer = customerRepository.findBySsn(ssn); + if (customer != null) { + return customer; + } + throw new EntityNotFoundException(Customer.class, "ssn", ssn.toString()); + + } + + @Override + public Customer createCustomer(CustomerDto customerDto) throws EntityException { + Optional customerInDb = Optional.ofNullable(customerRepository.findBySsn(customerDto.getSsn())); + if (customerInDb.isPresent()) { + throw new DuplicateEntityException(Customer.class, "ssn", customerDto.getSsn().toString()); + } + Customer customer = mapper.map(customerDto, Customer.class); + return customerRepository.save(customer); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..50d3892 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,9 @@ +# H2 +spring.h2.console.enabled=true +spring.h2.console.path=/h2 +spring.datasource.platform=h2 +spring.datasource.url=jdbc:h2:mem:bankdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file diff --git a/src/main/resources/data-h2.sql b/src/main/resources/data-h2.sql new file mode 100644 index 0000000..2db92b0 --- /dev/null +++ b/src/main/resources/data-h2.sql @@ -0,0 +1,22 @@ +INSERT INTO branch VALUES (1, '2018-01-01', '2018-01-01', 1000.0, 'Miniland', 'IND-1'); +INSERT INTO branch VALUES (2, '2018-01-02', '2018-01-02', 2000.0, 'Miniland', 'IND-2'); +INSERT INTO branch VALUES (3, '2018-01-03', '2018-01-03', 3000.0, 'Miniland', 'IND-3'); +INSERT INTO branch VALUES (4, '2018-01-04', '2018-01-04', 4000.0, 'Miniland', 'IND-4'); +INSERT INTO branch VALUES (5, '2018-01-05', '2018-01-05', 5000.0, 'Miniland', 'IND-5'); + +INSERT INTO customer VALUES (1, '2018-01-01', '2018-01-01', 'VT1', 'Marine Bay1', 'Indore', '9425094250', 'Arpit K', 'AK01'); +INSERT INTO customer VALUES (2, '2018-01-01', '2018-01-01', 'VT2', 'Marine Bay2', 'France', '9425094251', 'Jinedin Jidan', 'JJ01'); +INSERT INTO customer VALUES (3, '2018-01-01', '2018-01-01', 'VT3', 'Marine Bay3', 'UK', '9425094252', 'Harry Kane', 'HK01'); +INSERT INTO customer VALUES (4, '2018-01-01', '2018-01-01', 'VT4', 'Marine Bay4', 'Brasil', '9425094253', 'Nemar Jr', 'NJ01'); +INSERT INTO customer VALUES (5, '2018-01-01', '2018-01-01', 'VT5', 'Marine Bay5', 'Germany', '9425094254', 'Thomas Muller', 'TM01'); + +INSERT INTO account VALUES (1, '2018-01-01', '2018-01-01', 100.0, 0, 1, 1); +INSERT INTO account VALUES (2, '2018-01-01', '2018-01-01', 200.0, 0, 2, 1); +INSERT INTO account VALUES (3, '2018-01-01', '2018-01-01', 300.0, 0, 3, 1); +INSERT INTO account VALUES (4, '2018-01-01', '2018-01-01', 400.0, 0, 4, 1); +INSERT INTO account VALUES (5, '2018-01-01', '2018-01-01', 500.0, 0, 5, 1); +INSERT INTO account VALUES (6, '2018-01-01', '2018-01-01', 100.0, 1, 1, 2); +INSERT INTO account VALUES (7, '2018-01-01', '2018-01-01', 200.0, 1, 2, 2); +INSERT INTO account VALUES (8, '2018-01-01', '2018-01-01', 300.0, 1, 3, 2); +INSERT INTO account VALUES (9, '2018-01-01', '2018-01-01', 400.0, 1, 4, 2); +INSERT INTO account VALUES (10, '2018-01-01', '2018-01-01', 500.0, 1, 5, 2); \ No newline at end of file diff --git a/src/test/java/com/bankofspring/BankOfSpringApplicationTests.java b/src/test/java/com/bankofspring/BankOfSpringApplicationTests.java new file mode 100644 index 0000000..ea78b44 --- /dev/null +++ b/src/test/java/com/bankofspring/BankOfSpringApplicationTests.java @@ -0,0 +1,16 @@ +package com.bankofspring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class BankOfSpringApplicationTests { + + @Test + public void contextLoads() { + } + +}