diff --git a/.gitignore b/.gitignore index 9154f4c..cc0b5b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,36 @@ -# ---> Java -# Compiled class file -*.class - -# Log file +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ *.log +*.gz -# BlueJ files -*.ctxt +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +### VS Code ### +.vscode/ diff --git a/README.md b/README.md index 6c1da4c..2965527 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,65 @@ -# dfops-rest-service +# Задание -Тестовое задание. Напишите Spring-приложение, предоставляющее REST-сервис для приема и регистрации операций в базе данных по расчетам с водителями. У -каждого водителя может быть несколько лицевых счетов. \ No newline at end of file +Напишите Spring-приложение, предоставляющее REST-сервис для приема и регистрации операций в базе данных по расчетам с водителями. У каждого водителя может быть несколько лицевых счетов. + +## Требования + +СУБД PostgreSQL 10; создать структуру БД самостоятельно. +Доступ к данным реализовать с помощью Hibernate. + +Реализовать операции REST-сервиса; прием параметров и возврат данных – в JSON: +* начисление на счет водителя, +* списание со счета водителя, +* получение текущего баланса по счету, +* перевод между собственными лицевыми счетами водителя, +* получение оборота за период по отдельному лицевому счету (дебет, кредит отдельно), +* получение подробного списка операций за период. + +Для объемных операций (например, получение списка операций) реализовать постраничную выдачу. + +## Дополнительные требования + +* Создать Gradle-проект. +* Хранение исходников – в GIT (например, на gitlab.com). +* Дополнительным плюсом будет реализации unit-тестов. + +# Комментарий к выполненной работе + +Программу можно запустить в нескольких режимах используя профили spring boot + +1. **DEFAULT** - в данном режиме используется база данных postgresql с настройками по умолчанию, а именно `url:jdbc:postgresql://localhost/test, username: test, password: test`. Для запуска используем следующие параметры `java -jar dfops.jar` +1. **DEMO** - в данном режиме используется база данных H2 DB. Для запуска используем следующие параметры `java -jar dfops.jar --spring.profiles.active=demo` +1. **PRODUCTION** - в данном режиме используется база данных postgresql c альтернативными настройками прописанными в файле `application-prod.properties`, данный файл должен находиться в том же каталоге где и запускаемый jar-файл программы. Для запуска используем следующие параметры `java -jar dfops.jar --spring.profiles.active=prod` + +Пример содержимого файла `application-prod.properties` + +``` +DFOPS_PGSQL_DB_HOST=jdbc:postgresql://localhost +DFOPS_PGSQL_DB_PORT=5432 +DFOPS_PGSQL_DB_NAME=test +DFOPS_PGSQL_DB_USER=test +DFOPS_PGSQL_DB_PASSWORD=test +``` + +Так же для запуска программы в linux, можно воспользоваться скриптом `dfops_linux.sh` , при этом запускаемый jar-файл должен называться `dfops.jar` и находиться в том же каталоге, где и скрипт. Выполните `dfops_linux.sh --help` для получения помощи. При запуске в режиме `PRODUCTION` будет выполнена проверка на наличие файла `application-prod.properties`, если он не найден, то запустится интерактивный режим, где будет предложено заполнить необходимые данные. + +## Работа с программой + +### Инициализация БД + +Если работа ведётся с postgresql можно воспользуйтесь файлами `ddl-postgresql.sql`, `schema-postgresql.sql`, `data-postgresql.sql` для инициализации БД см. каталог `init_postgresql_db` + +### Прочее + +1. Точка входа `http://localhost:8080/rest/v1/` +1. Документацию по API использует OpenApi c ui. Для доступа к ui используем адрес `http://localhost:8080/rest/v1/swagger-ui/index.html?configUrl=/rest/v1/v3/api-docs/swagger-config` , в представлении json используем `http://localhost:8080/rest/v1/v3/api-docs` +1. В каталоге `docker` при наличии docker и docker compose можно запустить docker-образ с postgresql версии 10. +1. В gitlab настроен CI/CD. Есть возможность скачать последнюю версию артефакта по ссылке https://gitlab.com/Aleksandrov/dfops-rest-service/-/pipelines + +--- + +Тестовое задание выполнил + +Александров А.А. (alexandrov@resprojects.ru) + +ссылка на профиль hh.ru - https://hh.ru/resume/7cdada75ff015e78530039ed1f366c4b4a5273 \ No newline at end of file diff --git a/dfops_linux.sh b/dfops_linux.sh new file mode 100755 index 0000000..021be02 --- /dev/null +++ b/dfops_linux.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +EXECUTABLE_FILE=dfops.jar +PROPERTIES_FILE=application-prod.properties + +HELP="Usage: dfops_linux [KEY] + +Script without key is run program in DEFAULT mode. + +DEFAULT mode uses postgresql with next parameters: + + url: jdbc:postgresql://localhost/test + username: test + password: test + +Also available next switches: + +--prod - running program in PRODUCTION mode. For running in this mode needed additional +file application-prod.properties with PostgreSQL dataset information. + +--demo - running program in DEMO mode. In this mode uses H2 database (in-memory) instead postgresql. + +--help - display this is message + +Examples: + +dfops_linux - run program in DEFAULT mode + +dfops_linux --demo - run program in DEMO mode. + +dfops_linux --prod - run program in PRODUCTION mode. +" + +PROPERTIES_FILE_NOT_FOUND=" +WARNING! + +You try run program in PRODUCTION mode. For this mode need PostgreSQL but file +$PROPERTIES_FILE with dataset information is not found. Please fill next information and run program again! + +" + +if [ -f "$EXECUTABLE_FILE" ]; then + if [ -z "$1" ]; then + echo "Running program in DEFAULT mode" + java -jar "$EXECUTABLE_FILE" + else + case "$1" in + --help) + echo "$HELP" + ;; + --demo) + echo "Running program in DEMO mode with H2 DB (in-memory)" + java -jar "$EXECUTABLE_FILE" --spring.profiles.active=demo + ;; + --prod) + if [ -f "$PROPERTIES_FILE" ]; then + echo "Running program in PRODUCTION mode with PostgreSQL DB" + java -jar "$EXECUTABLE_FILE" --spring.profiles.active=prod + else + echo "$PROPERTIES_FILE_NOT_FOUND" + printf 'PostgreSQL database host name or IP address (default localhost): ' + read -r DFOPS_PGSQL_DB_HOST + if [ -z "$DFOPS_PGSQL_DB_HOST" ]; then + DFOPS_PGSQL_DB_HOST="jdbc:postgresql://localhost" + else + DFOPS_PGSQL_DB_HOST="jdbc:postgresql://$DFOPS_PGSQL_DB_HOST" + fi + printf 'PostgreSQL database port (default 5432): ' + read -r DFOPS_PGSQL_DB_PORT + if [ -z "$DFOPS_PGSQL_DB_PORT" ]; then + DFOPS_PGSQL_DB_PORT=5432 + fi + printf 'PostgreSQL database name (default test): ' + read -r DFOPS_PGSQL_DB_NAME + if [ -z "$DFOPS_PGSQL_DB_NAME" ]; then + DFOPS_PGSQL_DB_NAME="test" + fi + printf 'PostgreSQL database user name: ' + read -r DFOPS_PGSQL_DB_USER + printf 'PostgreSQL database password: ' + read -r -s DFOPS_PGSQL_DB_PASSWORD + echo + touch "$PROPERTIES_FILE" + { + echo "DFOPS_PGSQL_DB_HOST=$DFOPS_PGSQL_DB_HOST" + echo "DFOPS_PGSQL_DB_PORT=$DFOPS_PGSQL_DB_PORT" + echo "DFOPS_PGSQL_DB_NAME=$DFOPS_PGSQL_DB_NAME" + echo "DFOPS_PGSQL_DB_USER=$DFOPS_PGSQL_DB_USER" + echo "DFOPS_PGSQL_DB_PASSWORD=$DFOPS_PGSQL_DB_PASSWORD" + } > "$PROPERTIES_FILE" + fi + ;; + *) + echo "dfops_linux: unknown option $1" + echo "Try 'dfops_linux --help' for more information." + ;; + esac + fi +else + echo "Executable file dfops.jar is not found!" +fi \ No newline at end of file diff --git a/docker/db/Dockerfile b/docker/db/Dockerfile new file mode 100644 index 0000000..2a49625 --- /dev/null +++ b/docker/db/Dockerfile @@ -0,0 +1,5 @@ +FROM postgres:10-alpine +COPY scripts/*.sql /docker-entrypoint-initdb.d/ +ADD scripts/1_init_schema.sql /docker-entrypoint-initdb.d +ADD scripts/2_init_data.sql /docker-entrypoint-initdb.d +RUN chmod a+r /docker-entrypoint-initdb.d/* diff --git a/docker/db/scripts/1_init_schema.sql b/docker/db/scripts/1_init_schema.sql new file mode 100644 index 0000000..3cb8370 --- /dev/null +++ b/docker/db/scripts/1_init_schema.sql @@ -0,0 +1,32 @@ +CREATE SEQUENCE seq_employees START 5000; + +CREATE TABLE employees +( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL +); +CREATE UNIQUE INDEX employees_unique_email_idx ON employees (email); + +CREATE TABLE employee_personal_accounts ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + employee_id BIGINT NOT NULL, + personal_account VARCHAR NOT NULL, + FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX employee_personal_accounts_unique_employee_account_idx + ON employee_personal_accounts (employee_id, personal_account); + +CREATE TABLE personal_account_operations ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + personal_account_id BIGINT NOT NULL, + operation_date_time TIMESTAMP NOT NULL, + operation_type VARCHAR NOT NULL, + -- Money data on PostgreSQL using Java https://stackoverflow.com/a/18170030 + operation_value DECIMAL NOT NULL, + FOREIGN KEY (personal_account_id) REFERENCES employee_personal_accounts (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX personal_account_operations_unique_account_datetime_idx + ON personal_account_operations (personal_account_id, operation_date_time); + + diff --git a/docker/db/scripts/2_init_data.sql b/docker/db/scripts/2_init_data.sql new file mode 100644 index 0000000..f2dd235 --- /dev/null +++ b/docker/db/scripts/2_init_data.sql @@ -0,0 +1,34 @@ +INSERT INTO employees (name, email) VALUES +('Ivanov Ivan Ivanovich', 'ivanov@example.com'), +('Petrov Vasily Victorovich', 'petrov@example.com'); + +INSERT INTO employee_personal_accounts (personal_account, employee_id) +VALUES ('4154014152522741', 5000), + ('4131668358915203', 5000), + ('4281563275602455', 5000), + ('4103234971123321', 5001), + ('4132555843841699', 5001); + +INSERT INTO personal_account_operations (operation_date_time, operation_type, operation_value, personal_account_id) +VALUES ('2020-05-30 10:00:00'::timestamp, 'DEPOSIT', 840.35, 5002), + ('2020-05-28 11:05:10'::timestamp, 'DEPOSIT', 625.00, 5002), + ('2020-05-25 11:41:10'::timestamp, 'DEPOSIT', 1080.45, 5002), + ('2020-05-30 14:00:10'::timestamp, 'WITHDRAW', 652.33, 5002), + ('2020-05-26 18:10:10'::timestamp, 'WITHDRAW', 420.00, 5002), + ('2020-06-30 10:00:00'::timestamp, 'DEPOSIT', 1500.52, 5003), + ('2020-06-30 11:05:10'::timestamp, 'DEPOSIT', 800.73, 5003), + ('2020-06-30 14:00:10'::timestamp, 'WITHDRAW', 170.35, 5003), + ('2020-06-30 18:10:10'::timestamp, 'WITHDRAW', 320.00, 5003), + ('2020-07-15 12:05:10'::timestamp, 'DEPOSIT', 800.73, 5004), + ('2020-07-15 12:41:10'::timestamp, 'DEPOSIT', 350.00, 5004), + ('2020-07-15 15:00:10'::timestamp, 'WITHDRAW', 900.35, 5004), + ('2020-07-15 17:10:10'::timestamp, 'WITHDRAW', 600.00, 5004), + ('2020-05-15 11:05:10'::timestamp, 'DEPOSIT', 976.33, 5005), + ('2020-05-15 11:41:10'::timestamp, 'DEPOSIT', 850.00, 5005), + ('2020-05-15 14:00:10'::timestamp, 'WITHDRAW', 200.00, 5005), + ('2020-05-15 18:10:10'::timestamp, 'WITHDRAW', 375.85, 5005), + ('2020-04-30 09:00:00'::timestamp, 'DEPOSIT', 1200.52, 5006), + ('2020-04-30 10:35:00'::timestamp, 'DEPOSIT', 300.53, 5006), + ('2020-04-30 10:55:00'::timestamp, 'DEPOSIT', 450.60, 5006), + ('2020-04-30 12:20:10'::timestamp, 'WITHDRAW', 300.00, 5006), + ('2020-04-30 14:10:10'::timestamp, 'WITHDRAW', 402.95, 5006); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..b7ee55b --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.3' + +services: + db: + build: ./db + container_name: postgres + volumes: + - ./db_data/:/var/lib/postgresql/data/ + ports: + - "7654:5432" + restart: always + environment: + - POSTGRES_USER=test + - POSTGRES_PASSWORD=test + - POSTGRES_DB=test \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..8f89047 --- /dev/null +++ b/gradlew @@ -0,0 +1,184 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +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 +SAVED="$(pwd)" +cd "$(dirname \"$PRG\")/" >/dev/null +APP_HOME="$(pwd -P)" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=$(basename "$0") + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn() { + echo "$*" +} + +die() { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$(uname)" in +CYGWIN*) + cygwin=true + ;; +Darwin*) + darwin=true + ;; +MINGW*) + msys=true + ;; +NONSTOP*) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ]; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then + MAX_FD_LIMIT=$(ulimit -H -n) + if [ $? -eq 0 ]; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ]; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ]; then + APP_HOME=$(cygpath --path --mixed "$APP_HOME") + CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") + + JAVACMD=$(cygpath --unix "$JAVACMD") + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) + SEP="" + for dir in $ROOTDIRSRAW; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ]; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@"; do + CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) + CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition + eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") + else + eval $(echo args$i)="\"$arg\"" + fi + i=$(expr $i + 1) + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save() { + for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/init_postgresql_db/data-postgresql.sql b/init_postgresql_db/data-postgresql.sql new file mode 100644 index 0000000..1756db1 --- /dev/null +++ b/init_postgresql_db/data-postgresql.sql @@ -0,0 +1,40 @@ +DELETE FROM personal_account_operations; +DELETE FROM employee_personal_accounts; +DELETE FROM employees; +ALTER SEQUENCE seq_employees RESTART WITH 5000; + +INSERT INTO employees (name, email) VALUES +('Ivanov Ivan Ivanovich', 'ivanov@example.com'), +('Petrov Vasily Victorovich', 'petrov@example.com'); + +INSERT INTO employee_personal_accounts (personal_account, employee_id) +VALUES ('4154014152522741', 5000), + ('4131668358915203', 5000), + ('4281563275602455', 5000), + ('4103234971123321', 5001), + ('4132555843841699', 5001); + +INSERT INTO personal_account_operations (operation_date_time, operation_type, operation_value, personal_account_id) +VALUES ('2020-05-30 10:00:00'::timestamp, 'DEPOSIT', 840.35, 5002), + ('2020-05-28 11:05:10'::timestamp, 'DEPOSIT', 625.00, 5002), + ('2020-05-25 11:41:10'::timestamp, 'DEPOSIT', 1080.45, 5002), + ('2020-05-30 14:00:10'::timestamp, 'WITHDRAW', 652.33, 5002), + ('2020-05-26 18:10:10'::timestamp, 'WITHDRAW', 420.00, 5002), + ('2020-06-30 10:00:00'::timestamp, 'DEPOSIT', 1500.52, 5003), + ('2020-06-30 11:05:10'::timestamp, 'DEPOSIT', 800.73, 5003), + ('2020-06-30 14:00:10'::timestamp, 'WITHDRAW', 170.35, 5003), + ('2020-06-30 18:10:10'::timestamp, 'WITHDRAW', 320.00, 5003), + ('2020-07-15 12:05:10'::timestamp, 'DEPOSIT', 800.73, 5004), + ('2020-07-15 12:41:10'::timestamp, 'DEPOSIT', 350.00, 5004), + ('2020-07-15 15:00:10'::timestamp, 'WITHDRAW', 900.35, 5004), + ('2020-07-15 17:10:10'::timestamp, 'WITHDRAW', 600.00, 5004), + ('2020-05-15 11:05:10'::timestamp, 'DEPOSIT', 976.33, 5005), + ('2020-05-15 11:41:10'::timestamp, 'DEPOSIT', 850.00, 5005), + ('2020-05-15 14:00:10'::timestamp, 'WITHDRAW', 200.00, 5005), + ('2020-05-15 18:10:10'::timestamp, 'WITHDRAW', 375.85, 5005), + ('2020-04-30 09:00:00'::timestamp, 'DEPOSIT', 1200.52, 5006), + ('2020-04-30 10:35:00'::timestamp, 'DEPOSIT', 300.53, 5006), + ('2020-04-30 10:55:00'::timestamp, 'DEPOSIT', 450.60, 5006), + ('2020-04-30 12:20:10'::timestamp, 'WITHDRAW', 300.00, 5006), + ('2020-04-30 14:10:10'::timestamp, 'WITHDRAW', 402.95, 5006); + diff --git a/init_postgresql_db/ddl-postgresql.sql b/init_postgresql_db/ddl-postgresql.sql new file mode 100644 index 0000000..e2f46d0 --- /dev/null +++ b/init_postgresql_db/ddl-postgresql.sql @@ -0,0 +1,3 @@ +CREATE DATABASE test; +CREATE USER "user" WITH password 'password'; +GRANT ALL PRIVILEGES ON DATABASE test TO "user"; diff --git a/init_postgresql_db/schema-postgresql.sql b/init_postgresql_db/schema-postgresql.sql new file mode 100644 index 0000000..e6e574f --- /dev/null +++ b/init_postgresql_db/schema-postgresql.sql @@ -0,0 +1,35 @@ +DROP TABLE IF EXISTS personal_account_operations; +DROP TABLE IF EXISTS employee_personal_accounts; +DROP TABLE IF EXISTS employees; +DROP SEQUENCE IF EXISTS seq_employees cascade; + +CREATE SEQUENCE seq_employees START 5000; + +CREATE TABLE employees +( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL +); +CREATE UNIQUE INDEX employees_unique_email_idx ON employees (email); + +CREATE TABLE employee_personal_accounts ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + employee_id BIGINT NOT NULL, + personal_account VARCHAR NOT NULL, + FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX employee_personal_accounts_unique_employee_account_idx + ON employee_personal_accounts (employee_id, personal_account); + +CREATE TABLE personal_account_operations ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + personal_account_id BIGINT NOT NULL, + operation_date_time TIMESTAMP NOT NULL, + operation_type VARCHAR NOT NULL, + -- Money data on PostgreSQL using Java https://stackoverflow.com/a/18170030 + operation_value DECIMAL NOT NULL, + FOREIGN KEY (personal_account_id) REFERENCES employee_personal_accounts (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX personal_account_operations_unique_account_datetime_idx + ON personal_account_operations (personal_account_id, operation_date_time); \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0956402 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'dfops' diff --git a/src/main/java/ru/resprojects/dfops/DfopsApplication.java b/src/main/java/ru/resprojects/dfops/DfopsApplication.java new file mode 100644 index 0000000..1b99571 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/DfopsApplication.java @@ -0,0 +1,13 @@ +package ru.resprojects.dfops; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DfopsApplication { + + public static void main(String[] args) { + SpringApplication.run(DfopsApplication.class, args); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/config/AppConfig.java b/src/main/java/ru/resprojects/dfops/config/AppConfig.java new file mode 100644 index 0000000..a43837e --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/config/AppConfig.java @@ -0,0 +1,22 @@ +package ru.resprojects.dfops.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + public static final String DEFAULT_PAGE_LIMIT = "10"; + + @Bean + public OpenAPI customOpenApi() { + return new OpenAPI() + .components(new Components()) + .info(new Info().title("Driver Finance Operation REST Service API").description( + "REST-сервис для приема и регистрации операций в базе данных по расчетам с водителями.")); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/controller/AccountController.java b/src/main/java/ru/resprojects/dfops/controller/AccountController.java new file mode 100644 index 0000000..4051438 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/controller/AccountController.java @@ -0,0 +1,239 @@ +package ru.resprojects.dfops.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.resprojects.dfops.config.AppConfig; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.account.AccountDto; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ErrorMessage; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.service.AccountService; +import ru.resprojects.dfops.service.EmployeeService; + +@RestController +@RequestMapping("/account") +@Tag(name ="Расчётный счёт работника", description = "REST API для работы с сущностью 'Account'") +public class AccountController { + + private final EmployeeService employeeService; + + private final AccountService accountService; + + public AccountController(EmployeeService employeeService, AccountService accountService) { + this.employeeService = employeeService; + this.accountService = accountService; + } + + @Operation( + summary = "Вывод лицевых счетов работника", + description = "Возвращает список лицевых счетов выбранного работника", + tags = { "account" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ResponseDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "204", + description = "лицевые счета у выбранного работника отсутствуют", + content = @Content( + schema = @Schema(), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "не найден работник у которого необходимо вывести список лицевых счетов", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{employee_id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAll( + @Parameter(description="ID работника", required = true, + example = "5002", schema=@Schema(implementation = Long.class)) + @PathVariable("employee_id") Long employee_id, + @Parameter(description="Параметр запроса для постраничного вывода, задает номер текущей страницы", + example = "1", schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "page", defaultValue = "1") Integer pageNo, + @Parameter(description="Параметр запроса для постраничного вывода, задает количество элементов на страницу", + example = AppConfig.DEFAULT_PAGE_LIMIT, schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "limit", defaultValue = AppConfig.DEFAULT_PAGE_LIMIT) Integer limit) + { + Employee employee = employeeService.get(employee_id); + ResponseDto response = new ResponseDto<>(accountService.getAll(employee.getId(), pageNo, limit)); + if (response.getElements().isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } + return ResponseEntity.ok(response); + } + + @Operation( + summary = "Поиск лицевого счёта", + description = "Возвращает информацию о лицевом счёте", + tags = { "account" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = Account.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "запрашиваемый объект не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/get/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getById( + @Parameter(description="ID лицевого счёта", required = true, + example = "5002", schema=@Schema(implementation = Long.class)) + @PathVariable("id") Long id + ) { + return ResponseEntity.ok(accountService.get(id)); + } + + @Operation( + summary = "Создание новой записи", + description = "Создаёт новую запись и возвращает созданный объект", + tags = { "account" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = Account.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "работник для которого создаётся аккаунт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "409", + description = "элемент уже существует в базе", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create( + @Parameter(description="Создаваемый объект лицевого счёта", + required=true, schema=@Schema(implementation = AccountDto.class)) + @RequestBody AccountDto accountDto) { + if (accountDto == null) { + throw new BadResourceException("Request body is null"); + } + Employee employee = employeeService.get(accountDto.getEmployeeId()); + return ResponseEntity.ok(accountService.create(employee, accountDto.getPersonalAccount())); + } + + @Operation( + summary = "Удаление записи", + description = "Удаление записи из базы данных", + tags = { "account" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция" + ), + @ApiResponse( + responseCode = "404", + description = "элемент не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity delete( + @Parameter(description="ID лицевого счёта", required = true, + example = "5002", schema=@Schema(implementation = Long.class)) + @PathVariable("id") Long id) { + accountService.delete(id); + return ResponseEntity.status(HttpStatus.OK).body(null); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/controller/EmployeeController.java b/src/main/java/ru/resprojects/dfops/controller/EmployeeController.java new file mode 100644 index 0000000..52e7b4c --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/controller/EmployeeController.java @@ -0,0 +1,263 @@ +package ru.resprojects.dfops.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.resprojects.dfops.config.AppConfig; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.employee.EmployeeDto; +import ru.resprojects.dfops.exception.BadRequestException; +import ru.resprojects.dfops.exception.ErrorMessage; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.service.EmployeeService; + +@RestController +@RequestMapping("/employee") +@Tag(name ="Работник (водитель)", description = "REST API для работы с сущностью 'Employee'") +public class EmployeeController { + + private final EmployeeService employeeService; + + public EmployeeController(EmployeeService employeeService) { + this.employeeService = employeeService; + } + + @Operation( + summary = "Получить список работников", + description = "Возвращает список работников", + tags = { "employee" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ResponseDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "204", + description = "список работников пуст", + content = @Content( + schema = @Schema(), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAll( + @Parameter(description="Параметр запроса для постраничного вывода, задает номер текущей страницы", + example = "1", schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "page", defaultValue = "1") Integer pageNo, + @Parameter(description="Параметр запроса для постраничного вывода, задает количество элементов на страницу", + example = AppConfig.DEFAULT_PAGE_LIMIT,schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "limit", defaultValue = AppConfig.DEFAULT_PAGE_LIMIT) Integer limit) + { + ResponseDto response = new ResponseDto<>(employeeService.getAll(pageNo, limit)); + if (response.getElements().isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } + return ResponseEntity.ok(response); + } + + @Operation( + summary = "Поиск работника", + description = "Возвращает информацию о работнике", + tags = { "employee" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = Employee.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "запрашиваемый объект не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/get/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getById( + @Parameter(description="ID работника", required = true, + example = "5000", schema=@Schema(implementation = Long.class)) + @PathVariable("id") Long id) { + return ResponseEntity.ok(employeeService.get(id)); + } + + @Operation( + summary = "Поиск работника по e-mail", + description = "Возвращает информацию о работнике", + tags = { "employee" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = Employee.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "запрашиваемый объект не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/get", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getByEmail( + @Parameter(description="Email работника", required=true, + example = "ivanov@example.com", schema=@Schema(implementation = String.class)) + @RequestParam(value = "email") String email) { + if (email == null || email.isBlank()) { + throw new BadRequestException("Email is null or empty"); + } + return ResponseEntity.ok(employeeService.getByEmail(email)); + } + + @Operation( + summary = "Создание новой записи", + description = "Создаёт новую запись и возвращает созданный объект", + tags = { "employee" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = Employee.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "409", + description = "элемент уже существует в базе", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity create( + @Parameter(description="Создаваемый объект", + required=true, schema=@Schema(implementation = EmployeeDto.class)) + @RequestBody EmployeeDto employeeDto) { + if (employeeDto == null) { + throw new BadRequestException("Request body is null"); + } + Employee employee = new Employee(employeeDto.getName(), employeeDto.getEmail()); + return ResponseEntity.ok(employeeService.create(employee)); + } + + @Operation( + summary = "Удаление записи", + description = "Удаление записи из базы данных", + tags = { "employee" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция" + ), + @ApiResponse( + responseCode = "404", + description = "элемент не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity delete( + @Parameter(description="ID работника", required = true, + example = "5000", schema=@Schema(implementation = Long.class)) + @PathVariable("id") Long id) { + employeeService.delete(id); + return ResponseEntity.status(HttpStatus.OK).body(null); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/controller/OperationController.java b/src/main/java/ru/resprojects/dfops/controller/OperationController.java new file mode 100644 index 0000000..3ae77a7 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/controller/OperationController.java @@ -0,0 +1,605 @@ +package ru.resprojects.dfops.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.resprojects.dfops.config.AppConfig; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.account.AccountBalanceDto; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.dto.operation.OperationDto; +import ru.resprojects.dfops.dto.operation.OperationTransferDto; +import ru.resprojects.dfops.exception.BadRequestException; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ErrorMessage; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.service.AccountService; +import ru.resprojects.dfops.service.OperationService; +import ru.resprojects.dfops.util.DateTimeUtil; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/operation") +@Tag(name ="Операции над счётом", description = "REST API для работы с сущностью 'Operation'") +public class OperationController { + + private final AccountService accountService; + + private final OperationService operationService; + + public OperationController(AccountService accountService, OperationService operationService) { + this.accountService = accountService; + this.operationService = operationService; + } + + @Operation( + summary = "Вывод всех операций по лицевому счёту работника", + description = "Возвращает список всех операций по выбранному лицевому счету работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ResponseDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "204", + description = "список операций по счёту пуст", + content = @Content( + schema = @Schema(), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAll( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId, + @Parameter(description="Параметр запроса для постраничного вывода. Задает номер текущей страницы", + example = "1", schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "page", defaultValue = "1") Integer pageNo, + @Parameter(description="Параметр запроса для постраничного вывода. Задает количество элементов на страницу", + example = AppConfig.DEFAULT_PAGE_LIMIT, schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "limit", defaultValue = AppConfig.DEFAULT_PAGE_LIMIT) Integer limit) + { + Account account = accountService.get(accountId); + ResponseDto response = new ResponseDto<>(operationService.getAllByAccountId(account.getId(), pageNo, limit)); + if (response.getElements().isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } + return ResponseEntity.ok(response); + } + + @Operation( + summary = "Вывод всех операций за период по лицевому счёту работника", + description = "Возвращает список всех операций за период по выбранному лицевому счету работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ResponseDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "204", + description = "список операций по счёту пуст", + content = @Content( + schema = @Schema(), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAllBetween( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId, + @Parameter(description="Начальная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "01.05.2020-00:00", required = true) + @RequestParam(value = "startDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime startDateTime, + @Parameter(description="Конечная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "31.05.2020-23:59", required = true) + @RequestParam(value = "endDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime endDateTime, + @Parameter(description="Параметр запроса для постраничного вывода. Задает номер текущей страницы.", + example = "1", schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "page", defaultValue = "1") Integer pageNo, + @Parameter(description="Параметр запроса для постраничного вывода. Задает количество элементов на страницу.", + example = AppConfig.DEFAULT_PAGE_LIMIT, schema=@Schema(implementation = Integer.class)) + @RequestParam(value = "limit", defaultValue = AppConfig.DEFAULT_PAGE_LIMIT) Integer limit + ) + { + checkDateTimeFilter(startDateTime, endDateTime); + Account account = accountService.get(accountId); + ResponseDto response = new ResponseDto<>( + operationService.getAllByAccountIdBetween(account.getId(), startDateTime, endDateTime, pageNo, limit) + ); + if (response.getElements().isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } + return ResponseEntity.ok(response); + } + + @Operation( + summary = "Вывод оборота по операциям за период для лицевого счёта", + description = "Возвращает оборот по операциям за период для указанного лицевого счёта работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ResponseDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}/operations_amount/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getOperationsAmountBetween( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId, + @Parameter(description="Начальная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "01.05.2020-00:00", required = true) + @RequestParam(value = "startDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime startDateTime, + @Parameter(description="Конечная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "31.05.2020-23:59", required = true) + @RequestParam(value = "endDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime endDateTime + ) + { + checkDateTimeFilter(startDateTime, endDateTime); + Account account = accountService.get(accountId); + List result = operationService.getOperationsAmountBetween(account.getId(), startDateTime, endDateTime); + ResponseDto responseDto = new ResponseDto<>(); + responseDto.setElements(result); + responseDto.setPageCount(1); + responseDto.setCurrentPage(1); + responseDto.setTotalItems(result.size()); + return ResponseEntity.ok(responseDto); + } + + @Operation( + summary = "Вывод дебет лицевого счёта за период", + description = "Возвращает дебет лицевого счёта работника за указанный период", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = OperationAmountDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}/debit/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDebitBetween( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId, + @Parameter(description="Начальная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "01.05.2020-00:00", required = true) + @RequestParam(value = "startDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime startDateTime, + @Parameter(description="Конечная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "31.05.2020-23:59", required = true) + @RequestParam(value = "endDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime endDateTime + ) + { + checkDateTimeFilter(startDateTime, endDateTime); + Account account = accountService.get(accountId); + BigDecimal result = operationService.getDebitBetween(account.getId(), startDateTime, endDateTime); + OperationAmountDto operationAmountDto = new OperationAmountDto(result, ru.resprojects.dfops.model.Operation.OperationType.DEPOSIT); + return ResponseEntity.ok(operationAmountDto); + } + + @Operation( + summary = "Вывод кредит лицевого счёта", + description = "Возвращает кредит лицевого счёта работника за период", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = OperationAmountDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}/credit/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCreditBetween( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId, + @Parameter(description="Начальная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "01.05.2020-00:00", required = true) + @RequestParam(value = "startDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime startDateTime, + @Parameter(description="Конечная дата и время (формат dd.MM.yyyy-HH:mm)", schema=@Schema(implementation = String.class), + example = "31.05.2020-23:59", required = true) + @RequestParam(value = "endDateTime") @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN_REQUEST) LocalDateTime endDateTime + ) + { + checkDateTimeFilter(startDateTime, endDateTime); + Account account = accountService.get(accountId); + BigDecimal result = operationService.getCreditBetween(account.getId(), startDateTime, endDateTime); + OperationAmountDto operationAmountDto = new OperationAmountDto(result, ru.resprojects.dfops.model.Operation.OperationType.WITHDRAW); + return ResponseEntity.ok(operationAmountDto); + } + + @Operation( + summary = "Текущий баланс лицевого счёта", + description = "Выводит текущий баланс лицевого счёта", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = AccountBalanceDto.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @GetMapping(value = "/{account_id}/balance", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getBalance( + @Parameter(description="ID лицевого счёта", schema=@Schema(implementation = Long.class), required = true, example = "5002") + @PathVariable("account_id") Long accountId) { + Account account = accountService.get(accountId); + BigDecimal balance = operationService.getCurrentBalance(account.getId()); + AccountBalanceDto balanceDto = new AccountBalanceDto(account.getId(), account.getPersonalAccount(), balance); + return ResponseEntity.ok(balanceDto); + } + + @Operation( + summary = "Зачисление на лицевой счёт", + description = "Зачисление указанной суммы на лицевого счёта работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ru.resprojects.dfops.model.Operation.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "403", + description = "некорректные данные", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @PostMapping(value = "/deposit", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity deposit( + @Parameter(description="Объект с даннами для проведения операции по лицевому счёту", required=true, schema=@Schema(implementation = OperationDto.class)) + @RequestBody OperationDto operationDto + ) + { + if (operationDto == null) { + throw new BadRequestException("Request body is null"); + } + if (operationDto.getOperationType() == null) { + throw new BadRequestException("Unknown operation type"); + } + if (!ru.resprojects.dfops.model.Operation.OperationType.DEPOSIT.equals(operationDto.getOperationType())) { + throw new BadRequestException("Must be DEPOSIT but " + operationDto.getOperationType()); + } + Account account = accountService.get(operationDto.getAccountId()); + return ResponseEntity.ok(operationService.deposit(account, operationDto.getAmount())); + } + + @Operation( + summary = "Списание с лицевого счёта", + description = "Списание указанной суммы с лицевого счёта работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция", + content = @Content( + schema = @Schema(implementation = ru.resprojects.dfops.model.Operation.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "403", + description = "некорректные данные", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @PostMapping(value = "/withdraw", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity withdraw( + @Parameter(description="Объект с даннами для проведения операции по лицевому счёту", required=true, schema=@Schema(implementation = OperationDto.class)) + @RequestBody OperationDto operationDto + ) + { + if (operationDto == null) { + throw new BadRequestException("Request body is null"); + } + if (operationDto.getOperationType() == null) { + throw new BadRequestException("Unknown operation type"); + } + if (!ru.resprojects.dfops.model.Operation.OperationType.WITHDRAW.equals(operationDto.getOperationType())) { + throw new BadRequestException("Must be WITHDRAW but " + operationDto.getOperationType()); + } + Account account = accountService.get(operationDto.getAccountId()); + return ResponseEntity.ok(operationService.withdraw(account, operationDto.getAmount())); + } + + @Operation( + summary = "Перевод с лицевого счёта на лицевого счёта", + description = "Перевод с лицевого счёта на лицевой счёт работника", + tags = { "operation" } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "успешная операция" + ), + @ApiResponse( + responseCode = "400", + description = "некорректный запрос", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "404", + description = "лицевой счёт не найден", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "403", + description = "некорректные данные", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ), + @ApiResponse( + responseCode = "500", + description = "неизвестная ошибка", + content = @Content( + schema = @Schema(implementation = ErrorMessage.class), + mediaType = MediaType.APPLICATION_JSON_VALUE + ) + ) + }) + @PostMapping(value = "/transfer", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity transfer( + @Parameter(description="Объект с даннами для перевода суммы между счетами", required=true, schema=@Schema(implementation = OperationTransferDto.class)) + @RequestBody OperationTransferDto operationTransferDto + ) + { + if (operationTransferDto == null) { + throw new BadResourceException("Request body is null"); + } + Account from = accountService.get(operationTransferDto.getAccountIdFrom()); + Account to = accountService.get(operationTransferDto.getAccountIdTo()); + operationService.transfer(from, to, operationTransferDto.getAmount()); + return ResponseEntity.ok().body(null); + } + + private void checkDateTimeFilter(LocalDateTime startDateTime, LocalDateTime endDateTime) { + if (startDateTime == null || endDateTime == null) { + throw new BadRequestException("Start date-time or end date-time is null"); + } + if (startDateTime.compareTo(endDateTime) > 0) { + throw new BadRequestException("Start date-time later than end date-time"); + } + } + +} diff --git a/src/main/java/ru/resprojects/dfops/controller/RestExceptionHandler.java b/src/main/java/ru/resprojects/dfops/controller/RestExceptionHandler.java new file mode 100644 index 0000000..abb2b1a --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/controller/RestExceptionHandler.java @@ -0,0 +1,45 @@ +package ru.resprojects.dfops.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.resprojects.dfops.exception.BadRequestException; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ErrorMessage; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; + +import javax.servlet.http.HttpServletRequest; + +@RestControllerAdvice +public class RestExceptionHandler { + + @ExceptionHandler(value = {ResourceNotFoundException.class}) + public ResponseEntity resourceNotFound(HttpServletRequest request, ResourceNotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getErrorMessage()); + } + + @ExceptionHandler(value = {BadResourceException.class}) + public ResponseEntity badResource(HttpServletRequest request, BadResourceException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage()); + } + + @ExceptionHandler(value = {BadRequestException.class}) + public ResponseEntity badRequest(HttpServletRequest request, BadRequestException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage()); + } + + @ExceptionHandler(value = {ResourceAlreadyExistsException.class}) + public ResponseEntity alreadyExistResource(HttpServletRequest request, ResourceAlreadyExistsException exception) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(exception.getErrorMessage()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ResponseEntity handleError(HttpServletRequest requesr, Exception exception) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorMessage("Unexpected error: " + exception.getMessage())); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/ResponseDto.java b/src/main/java/ru/resprojects/dfops/dto/ResponseDto.java new file mode 100644 index 0000000..0da9d61 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/ResponseDto.java @@ -0,0 +1,47 @@ +package ru.resprojects.dfops.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.data.domain.Page; + +import java.io.Serializable; +import java.util.Collection; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@ToString +@Schema(description = "Объект для постраничного вывода набора элементов") +public class ResponseDto implements Serializable { + + private static final long serialVersionUID = -3654370454854576301L; + + @Schema(description = "Список элементов") + @JsonProperty("elements") + private Collection elements; + + @Schema(description = "Номер текущей страницы") + @JsonProperty("currentPage") + private int currentPage; + + @Schema(description = "Всего элементов") + @JsonProperty("totalItems") + private long totalItems; + + @Schema(description = "Номер всего страниц") + @JsonProperty("totalPages") + private int pageCount; + + public ResponseDto(Page response) { + this.elements = response.getContent(); + this.currentPage = response.getNumber() + 1; + this.totalItems = response.getTotalElements(); + this.pageCount = response.getTotalPages(); + } +} diff --git a/src/main/java/ru/resprojects/dfops/dto/account/AccountBalanceDto.java b/src/main/java/ru/resprojects/dfops/dto/account/AccountBalanceDto.java new file mode 100644 index 0000000..bc2bca8 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/account/AccountBalanceDto.java @@ -0,0 +1,34 @@ +package ru.resprojects.dfops.dto.account; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; +import java.math.BigDecimal; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Schema(description = "Объект для передачи данных о текущем балансе лицевого счёта") +public class AccountBalanceDto implements Serializable { + + private static final long serialVersionUID = -82064792530944046L; + + @Schema(description = "ID лицевого счёта") + @JsonProperty("account_id") + private Long accountId; + + @Schema(description = "Номер лицевого счёта") + @JsonProperty("personal_account") + private String personalAccount; + + @Schema(description = "Текущий баланс лицевого счёта") + @JsonProperty("balance") + private BigDecimal balance; + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/account/AccountDto.java b/src/main/java/ru/resprojects/dfops/dto/account/AccountDto.java new file mode 100644 index 0000000..c58da53 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/account/AccountDto.java @@ -0,0 +1,33 @@ +package ru.resprojects.dfops.dto.account; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Schema(description = "Объект для передачи данных о регистрации лицевого счёта для выбранного работника") +public class AccountDto implements Serializable { + + private static final long serialVersionUID = 8356762606534341601L; + + @Schema(description = "id работника", example = "5000", required = true) + @JsonProperty("employee_id") + @NotNull + private Long employeeId; + + @Schema(description = "лицевой счёт работника", example = "123456789", required = true) + @JsonProperty("personal_account") + @NotBlank + private String personalAccount; + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/employee/EmployeeDto.java b/src/main/java/ru/resprojects/dfops/dto/employee/EmployeeDto.java new file mode 100644 index 0000000..aead16d --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/employee/EmployeeDto.java @@ -0,0 +1,32 @@ +package ru.resprojects.dfops.dto.employee; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Schema(description = "Объект для передачи данных о регистрации работника в БД") +public class EmployeeDto implements Serializable { + + private static final long serialVersionUID = 313229127008191126L; + + @Schema(description = "Имя работника", example = "Alex", required = true) + @JsonProperty("name") + @NotBlank + private String name; + + @Schema(description = "E-mail работника", example = "alex@example.com", required = true) + @JsonProperty("email") + @NotBlank + private String email; + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/operation/OperationAmountDto.java b/src/main/java/ru/resprojects/dfops/dto/operation/OperationAmountDto.java new file mode 100644 index 0000000..d41afc1 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/operation/OperationAmountDto.java @@ -0,0 +1,29 @@ +package ru.resprojects.dfops.dto.operation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.resprojects.dfops.model.Operation; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Объект для передачи данных о сумме и типе операций по счёту") +public class OperationAmountDto implements Serializable { + + private static final long serialVersionUID = 3448183919367413609L; + + @Schema(description = "Сумма") + @JsonProperty("operation_sum") + private BigDecimal sum; + + @Schema(description = "Тип операции: WITHDRAW - списание, DEPOSIT - зачисление") + @JsonProperty("operation_type") + private Operation.OperationType operationType; + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/operation/OperationDto.java b/src/main/java/ru/resprojects/dfops/dto/operation/OperationDto.java new file mode 100644 index 0000000..b3e9c56 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/operation/OperationDto.java @@ -0,0 +1,34 @@ +package ru.resprojects.dfops.dto.operation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.resprojects.dfops.model.Operation; + +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Schema(description = "Операция по лицевому счёту") +public class OperationDto implements Serializable { + + private static final long serialVersionUID = 6502515027792967205L; + + @Schema(description = "ID лицевого счёта", example = "5002", required = true) + @JsonProperty("account_id") + private Long accountId; + + @Schema(description = "Сумма операции", example = "100.25", required = true) + @JsonProperty("operation_amount") + double amount; + + @Schema(description = "Тип операции: DEPOSIT - начисление, WITHDRAW - списание", example = "WITHDRAW", required = true) + @JsonProperty("operation_type") + Operation.OperationType operationType; + +} diff --git a/src/main/java/ru/resprojects/dfops/dto/operation/OperationTransferDto.java b/src/main/java/ru/resprojects/dfops/dto/operation/OperationTransferDto.java new file mode 100644 index 0000000..3596b02 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/dto/operation/OperationTransferDto.java @@ -0,0 +1,34 @@ +package ru.resprojects.dfops.dto.operation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Schema(description = "Перевод между лицевыми счетами работника. ID лицевых счетов " + + "откуда переводят и куда переводят должны принадлежать одному и тому же работнику") +public class OperationTransferDto implements Serializable { + + private static final long serialVersionUID = 5904536254136545140L; + + @Schema(description = "ID лицевого счёта откуда переводят", example = "5002", required = true) + @JsonProperty("account_id_from") + private Long accountIdFrom; + + @Schema(description = "ID лицевого счёта куда переводят", example = "5003", required = true) + @JsonProperty("account_id_to") + private Long accountIdTo; + + @Schema(description = "Переводимая сумма", example = "100.00", required = true) + @JsonProperty("operation_amount") + double amount; + +} diff --git a/src/main/java/ru/resprojects/dfops/exception/BadRequestException.java b/src/main/java/ru/resprojects/dfops/exception/BadRequestException.java new file mode 100644 index 0000000..05d4148 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/exception/BadRequestException.java @@ -0,0 +1,16 @@ +package ru.resprojects.dfops.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException() { + } + + public BadRequestException(String msg) { + super(msg); + } + + public ErrorMessage getErrorMessage() { + return new ErrorMessage(this.getMessage()); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/exception/BadResourceException.java b/src/main/java/ru/resprojects/dfops/exception/BadResourceException.java new file mode 100644 index 0000000..c964019 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/exception/BadResourceException.java @@ -0,0 +1,16 @@ +package ru.resprojects.dfops.exception; + +public class BadResourceException extends RuntimeException { + + public BadResourceException() { + } + + public BadResourceException(String msg) { + super(msg); + } + + public ErrorMessage getErrorMessage() { + return new ErrorMessage(this.getMessage()); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/exception/ErrorMessage.java b/src/main/java/ru/resprojects/dfops/exception/ErrorMessage.java new file mode 100644 index 0000000..cfaf435 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/exception/ErrorMessage.java @@ -0,0 +1,25 @@ +package ru.resprojects.dfops.exception; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Форма вывода ошибки") +public class ErrorMessage implements Serializable { + + private static final long serialVersionUID = 4089956800265998558L; + + @Schema(description = "Сообщение о ошибке") + @JsonProperty("msg") + private String message; + +} diff --git a/src/main/java/ru/resprojects/dfops/exception/ResourceAlreadyExistsException.java b/src/main/java/ru/resprojects/dfops/exception/ResourceAlreadyExistsException.java new file mode 100644 index 0000000..7290b4e --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/exception/ResourceAlreadyExistsException.java @@ -0,0 +1,15 @@ +package ru.resprojects.dfops.exception; + +public class ResourceAlreadyExistsException extends RuntimeException { + + public ResourceAlreadyExistsException() { + } + + public ResourceAlreadyExistsException(String msg) { + super(msg); + } + + public ErrorMessage getErrorMessage() { + return new ErrorMessage(this.getMessage()); + } +} diff --git a/src/main/java/ru/resprojects/dfops/exception/ResourceNotFoundException.java b/src/main/java/ru/resprojects/dfops/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..8a113eb --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/exception/ResourceNotFoundException.java @@ -0,0 +1,16 @@ +package ru.resprojects.dfops.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException() { + } + + public ResourceNotFoundException(String msg) { + super(msg); + } + + public ErrorMessage getErrorMessage() { + return new ErrorMessage(this.getMessage()); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/model/AbstractBaseEntity.java b/src/main/java/ru/resprojects/dfops/model/AbstractBaseEntity.java new file mode 100644 index 0000000..3cc7ce1 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/model/AbstractBaseEntity.java @@ -0,0 +1,47 @@ +package ru.resprojects.dfops.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.SequenceGenerator; + +@MappedSuperclass +@Access(AccessType.FIELD) +@Getter +@Setter +@EqualsAndHashCode +@ToString +public abstract class AbstractBaseEntity { + + public static final int START_SEQ = 5000; + + @Schema(description = "Уникальный идентификатор") + @Id + @SequenceGenerator( + name = "seq_employees", + sequenceName = "seq_employees", + allocationSize = 1, + initialValue = START_SEQ + ) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_employees") + @JsonProperty("id") + protected Long id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Long id) { + this.id = id; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/resprojects/dfops/model/Account.java b/src/main/java/ru/resprojects/dfops/model/Account.java new file mode 100644 index 0000000..59222f0 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/model/Account.java @@ -0,0 +1,72 @@ +package ru.resprojects.dfops.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +@Entity +@Table(name = "employee_personal_accounts", uniqueConstraints = { + @UniqueConstraint( + columnNames = {"employee_id", "personal_account"}, + name = "employee_personal_accounts_unique_employee_account_idx" + ) +}) +@NoArgsConstructor +@Getter +@Setter +@Schema(description = "Счёт работника") +public class Account extends AbstractBaseEntity { + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "employee_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Employee employee; + + @Schema(description = "Лицевой счёт работника") + @Column(name = "personal_account", nullable = false, unique = true) + @NotBlank + @Size(min = 2, max = 50) + @JsonProperty("personal_account") + private String personalAccount; + + @JsonIgnore + @OneToMany(fetch = FetchType.LAZY, mappedBy = "account") + private List operations; + + public Account(Employee employee, String personalAccount) { + this(null, employee, personalAccount); + } + + public Account(Long id, Employee employee, String personalAccount) { + super(id); + this.employee = employee; + this.personalAccount = personalAccount; + } + + @Override + public String toString() { + return "Account{" + + "personalAccount='" + personalAccount + '\'' + + ", id=" + id + + '}'; + } + +} diff --git a/src/main/java/ru/resprojects/dfops/model/Employee.java b/src/main/java/ru/resprojects/dfops/model/Employee.java new file mode 100644 index 0000000..4af93ed --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/model/Employee.java @@ -0,0 +1,59 @@ +package ru.resprojects.dfops.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +@Entity +@Table(name = "employees", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "employees_unique_email_idx")}) +@NoArgsConstructor +@Getter +@Setter +@ToString(of = {"name", "email"}) +@Schema(description = "Работник") +public class Employee extends AbstractBaseEntity { + + @Schema(description = "Имя работника") + @Column(name = "name", nullable = false) + @NotBlank + @Size(min = 2, max = 100) + @JsonProperty("name") + private String name; + + @Schema(description = "E-mail работника") + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 100) + @JsonProperty("email") + private String email; + + @JsonIgnore + @OneToMany(fetch = FetchType.LAZY, mappedBy = "employee") + private List accounts; + + public Employee(String name, String email) { + this(null, name, email); + } + + public Employee(Long id, String name, String email) { + super(id); + this.name = name; + this.email = email; + } +} diff --git a/src/main/java/ru/resprojects/dfops/model/Operation.java b/src/main/java/ru/resprojects/dfops/model/Operation.java new file mode 100644 index 0000000..bead280 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/model/Operation.java @@ -0,0 +1,94 @@ +package ru.resprojects.dfops.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.springframework.format.annotation.DateTimeFormat; +import ru.resprojects.dfops.util.DateTimeUtil; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "personal_account_operations", uniqueConstraints = { + @UniqueConstraint( + columnNames = {"personal_account_id", "operation_date_time"}, + name = "personal_account_operations_unique_account_datetime_idx" + ) +}) +@NoArgsConstructor +@Getter +@Setter +@Schema(description = "Операция по лицевому счёту") +public class Operation extends AbstractBaseEntity { + + //TODO найти как отобразить в OpenApi Enum + public static enum OperationType { + DEPOSIT, + WITHDRAW + } + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "personal_account_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Account account; + + @Schema(description = "Дата и время операции") + @Column(name = "operation_date_time", nullable = false) + @NotNull + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + @JsonProperty("operation_date_time") + private LocalDateTime dateTime; + + @Schema(description = "Тип операции") + @Column(name = "operation_type", nullable = false) + @Enumerated(EnumType.STRING) + @NotNull + @JsonProperty("operation_type") + private OperationType operationType; + + @Schema(description = "Сумма операции") + @Column(name = "operation_value", nullable = false) + @NotNull + @JsonProperty("operation_amount") + private BigDecimal operationValue; + + public Operation(Account account, OperationType operationType, BigDecimal operationValue) { + this(null, account, operationType, operationValue); + } + + public Operation(Long id, Account account, OperationType operationType, BigDecimal operationValue) { + super(id); + this.account = account; + this.dateTime = LocalDateTime.now(); + this.operationType = operationType; + this.operationValue = operationValue; + } + + @Override + public String toString() { + return "Operation{" + + " dateTime=" + dateTime + + ", operationType=" + operationType + + ", operationValue=" + operationValue + + ", id=" + id + + '}'; + } + +} diff --git a/src/main/java/ru/resprojects/dfops/repository/AccountRepository.java b/src/main/java/ru/resprojects/dfops/repository/AccountRepository.java new file mode 100644 index 0000000..fa5038a --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/repository/AccountRepository.java @@ -0,0 +1,26 @@ +package ru.resprojects.dfops.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.resprojects.dfops.model.Account; + +import java.util.Optional; + +@Repository +public interface AccountRepository extends JpaRepository { + + + Page findAllByEmployee_Id(long employeeId, Pageable pageable); + + boolean existsByPersonalAccount(String personalAccount); + + @EntityGraph(attributePaths = {"employee"}, type = EntityGraph.EntityGraphType.LOAD) + @Query("SELECT a FROM Account a WHERE a.id=:id") + Optional getWithEmployee(@Param("id") long id); + +} diff --git a/src/main/java/ru/resprojects/dfops/repository/EmployeeRepository.java b/src/main/java/ru/resprojects/dfops/repository/EmployeeRepository.java new file mode 100644 index 0000000..f91252b --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/repository/EmployeeRepository.java @@ -0,0 +1,16 @@ +package ru.resprojects.dfops.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.resprojects.dfops.model.Employee; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + + Optional findByEmail(String email); + + boolean existsByEmail(String email); + +} diff --git a/src/main/java/ru/resprojects/dfops/repository/OperationRepository.java b/src/main/java/ru/resprojects/dfops/repository/OperationRepository.java new file mode 100644 index 0000000..0556955 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/repository/OperationRepository.java @@ -0,0 +1,63 @@ +package ru.resprojects.dfops.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.model.Operation; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface OperationRepository extends JpaRepository { + + Page getAllByAccount_Id(long accountId, Pageable pageable); + + @SuppressWarnings("JpaQlInspection") + @Query("SELECT o " + + "FROM Operation o " + + "WHERE o.account.id=:accountId AND o.dateTime BETWEEN :startDate AND :endDate") + Page getAllByAccountIdBetween( + @Param("accountId") long accountId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + Pageable pageable + ); + + @Query("SELECT new ru.resprojects.dfops.dto.operation.OperationAmountDto(sum(o.operationValue), o.operationType) " + + "FROM Operation o " + + "WHERE o.account.id=:accountId " + + "GROUP BY o.operationType") + List getOperationsAmount(@Param("accountId") long accountId); + + @SuppressWarnings("JpaQlInspection") + @Query("SELECT new ru.resprojects.dfops.dto.operation.OperationAmountDto(sum(o.operationValue), o.operationType) " + + "FROM Operation o " + + "WHERE o.account.id=:accountId AND o.dateTime BETWEEN :startDate AND :endDate " + + "GROUP BY o.operationType") + List getOperationsAmountBetween( + @Param("accountId") long accountId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate + ); + + + @SuppressWarnings("JpaQlInspection") + @Query("SELECT sum(o.operationValue) " + + "FROM Operation o " + + "WHERE o.account.id=:accountId AND o.operationType = :operationType AND o.dateTime BETWEEN :startDate AND :endDate " + + "GROUP BY o.operationType") + Optional getOperationAmountBetween( + @Param("accountId") long accountId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("operationType") Operation.OperationType operationType + ); + +} diff --git a/src/main/java/ru/resprojects/dfops/service/AccountService.java b/src/main/java/ru/resprojects/dfops/service/AccountService.java new file mode 100644 index 0000000..b017665 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/AccountService.java @@ -0,0 +1,21 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; + + +public interface AccountService { + + Account create(Employee employee, String personalAccount) throws BadResourceException, ResourceAlreadyExistsException; + + void delete(long id) throws ResourceNotFoundException; + + Account get(long id) throws ResourceNotFoundException; + + Page getAll(long employeeId, int pageNumber, int rowPerPage); + +} diff --git a/src/main/java/ru/resprojects/dfops/service/AccountServiceImpl.java b/src/main/java/ru/resprojects/dfops/service/AccountServiceImpl.java new file mode 100644 index 0000000..aad1c28 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/AccountServiceImpl.java @@ -0,0 +1,64 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.repository.AccountRepository; + +@Service +@Transactional(readOnly = true) +public class AccountServiceImpl implements AccountService { + + private final AccountRepository accountRepository; + + public AccountServiceImpl(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Transactional + @Override + public Account create(Employee employee, String personalAccount) throws BadResourceException, ResourceAlreadyExistsException { + if (employee == null) { + throw new BadResourceException("Failed to save account. Employee is null"); + } + if (StringUtils.isEmpty(personalAccount)) { + throw new BadResourceException("Failed to save account. Personal account is null or empty"); + } + if (accountRepository.existsByPersonalAccount(personalAccount)) { + String message = String.format( + "Failed to save account. Bank account with %s for employee id %d is exists", + personalAccount, + employee.getId() + ); + throw new ResourceAlreadyExistsException(message); + } + Account account = new Account(employee, personalAccount); + return accountRepository.save(account); + } + + @Transactional + @Override + public void delete(long id) throws ResourceNotFoundException { + if (get(id) != null) { + accountRepository.deleteById(id); + } + } + + @Override + public Account get(long id) throws ResourceNotFoundException { + return accountRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Cannot find Account with id: " + id)); + } + + @Override + public Page getAll(long employeeId, int pageNumber, int rowPerPage) { + return accountRepository.findAllByEmployee_Id(employeeId, PageRequest.of(pageNumber - 1, rowPerPage)); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/service/EmployeeService.java b/src/main/java/ru/resprojects/dfops/service/EmployeeService.java new file mode 100644 index 0000000..be18ef7 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/EmployeeService.java @@ -0,0 +1,21 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Employee; + +public interface EmployeeService { + + Employee create(Employee employee) throws ResourceAlreadyExistsException, BadResourceException; + + void delete(long id) throws ResourceNotFoundException; + + Employee get(long id) throws ResourceNotFoundException; + + Employee getByEmail(String email) throws ResourceNotFoundException; + + Page getAll(int pageNumber, int rowPerPage); + +} diff --git a/src/main/java/ru/resprojects/dfops/service/EmployeeServiceImpl.java b/src/main/java/ru/resprojects/dfops/service/EmployeeServiceImpl.java new file mode 100644 index 0000000..11f11c6 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/EmployeeServiceImpl.java @@ -0,0 +1,67 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.repository.EmployeeRepository; + +@Service +@Transactional(readOnly = true) +public class EmployeeServiceImpl implements EmployeeService { + + private final EmployeeRepository employeeRepository; + + public EmployeeServiceImpl(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + + @Transactional + @Override + public Employee create(Employee employee) throws ResourceAlreadyExistsException, BadResourceException { + if (employee == null) { + throw new BadResourceException("Failed to save employee. Employee is null"); + } + String email = employee.getEmail(); + if (StringUtils.isEmpty(email)) { + throw new BadResourceException("Failed to save employee. Employee e-mail is null or empty"); + } + if (employeeRepository.existsByEmail(email)) { + throw new ResourceAlreadyExistsException("Failed to save employee. Employee with email is exists"); + } + return employeeRepository.save(employee); + } + + @Transactional + @Override + public void delete(long id) throws ResourceNotFoundException { + if (get(id) != null) { + employeeRepository.deleteById(id); + } + } + + @Override + public Employee get(long id) throws ResourceNotFoundException { + return employeeRepository.findById(id).orElseThrow( + () -> new ResourceNotFoundException("Cannot find Employee with id: " + id) + ); + } + + @Override + public Employee getByEmail(String email) throws ResourceNotFoundException { + return employeeRepository.findByEmail(email).orElseThrow( + () -> new ResourceNotFoundException("Cannot find Employee with e-mail: " + email) + ); + } + + @Override + public Page getAll(int pageNumber, int rowPerPage) { + return employeeRepository.findAll(PageRequest.of(pageNumber - 1, rowPerPage)); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/service/OperationService.java b/src/main/java/ru/resprojects/dfops/service/OperationService.java new file mode 100644 index 0000000..d60a660 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/OperationService.java @@ -0,0 +1,34 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Operation; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public interface OperationService { + + Operation deposit(Account account, double amount); + + Operation withdraw(Account account, double amount) throws BadResourceException; + + BigDecimal getCurrentBalance(long accountId); + + BigDecimal getDebitBetween(long accountId, LocalDateTime start, LocalDateTime end) throws ResourceNotFoundException; + + BigDecimal getCreditBetween(long accountId, LocalDateTime start, LocalDateTime end) throws ResourceNotFoundException; + + List getOperationsAmountBetween(long accountId, LocalDateTime start, LocalDateTime end); + + Page getAllByAccountId(long accountId, int pageNumber, int rowPerPage); + + Page getAllByAccountIdBetween(long accountId, LocalDateTime start, LocalDateTime end, int pageNumber, int rowPerPage); + + void transfer(Account accountFrom, Account accountTo, double amount) throws BadResourceException; + +} diff --git a/src/main/java/ru/resprojects/dfops/service/OperationServiceImpl.java b/src/main/java/ru/resprojects/dfops/service/OperationServiceImpl.java new file mode 100644 index 0000000..74e0dc7 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/service/OperationServiceImpl.java @@ -0,0 +1,104 @@ +package ru.resprojects.dfops.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Operation; +import ru.resprojects.dfops.repository.OperationRepository; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional(readOnly = true) +public class OperationServiceImpl implements OperationService { + + private final OperationRepository operationRepository; + + public OperationServiceImpl(OperationRepository operationRepository) { + this.operationRepository = operationRepository; + } + + @Transactional + @Override + public Operation deposit(Account account, double amount) { + BigDecimal depositAmount = new BigDecimal(String.valueOf(amount)); + Operation operation = new Operation(account, Operation.OperationType.DEPOSIT, depositAmount.setScale(2, RoundingMode.HALF_UP)); + return operationRepository.save(operation); + } + + @Transactional + @Override + public Operation withdraw(Account account, double amount) throws BadResourceException { + double currentBalance = getCurrentBalance(account.getId()).doubleValue(); + if (currentBalance < amount) { + throw new BadResourceException( + "Cannot withdraw from account = " + account.getPersonalAccount() + + " because current balance of account " + account.getPersonalAccount() + "(" + currentBalance + ")" + + " < amount = " + amount + ); + } + BigDecimal withdrawAmount = new BigDecimal(String.valueOf(amount)); + Operation operation = new Operation(account, Operation.OperationType.WITHDRAW, withdrawAmount.setScale(2, RoundingMode.HALF_UP)); + return operationRepository.save(operation); + } + + @Override + public BigDecimal getCurrentBalance(long accountId) { + List operationAmountDtos = operationRepository.getOperationsAmount(accountId); + BigDecimal sum = new BigDecimal("0.00"); + for (OperationAmountDto amountDto : operationAmountDtos) { + if (Operation.OperationType.WITHDRAW.equals(amountDto.getOperationType())) { + sum = sum.subtract(amountDto.getSum()); + } else { + sum = sum.add(amountDto.getSum()); + } + } + return sum.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public BigDecimal getDebitBetween(long accountId, LocalDateTime start, LocalDateTime end) throws ResourceNotFoundException { + BigDecimal sum = operationRepository.getOperationAmountBetween(accountId, start, end, Operation.OperationType.DEPOSIT).orElse(new BigDecimal("0.00")); + return sum.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public BigDecimal getCreditBetween(long accountId, LocalDateTime start, LocalDateTime end) throws ResourceNotFoundException { + BigDecimal sum = operationRepository.getOperationAmountBetween(accountId, start, end, Operation.OperationType.WITHDRAW).orElse(new BigDecimal("0.00")); + return sum.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public List getOperationsAmountBetween(long accountId, LocalDateTime start, LocalDateTime end) { + return operationRepository.getOperationsAmountBetween(accountId, start, end); + } + + @Override + public Page getAllByAccountId(long accountId, int pageNumber, int rowPerPage) { + return operationRepository.getAllByAccount_Id(accountId, PageRequest.of(pageNumber - 1, rowPerPage)); + } + + @Override + public Page getAllByAccountIdBetween(long accountId, LocalDateTime start, LocalDateTime end, int pageNumber, int rowPerPage) { + return operationRepository.getAllByAccountIdBetween(accountId, start, end, PageRequest.of(pageNumber - 1, rowPerPage)); + } + + @Transactional + @Override + public void transfer(Account accountFrom, Account accountTo, double amount) throws BadResourceException { + if (!accountFrom.getEmployee().getId().equals(accountTo.getEmployee().getId())) { + throw new BadResourceException("Accounts from and account to do not belong to one employee"); + } + withdraw(accountFrom, amount); + deposit(accountTo, amount); + } + +} diff --git a/src/main/java/ru/resprojects/dfops/util/DateTimeUtil.java b/src/main/java/ru/resprojects/dfops/util/DateTimeUtil.java new file mode 100644 index 0000000..600a182 --- /dev/null +++ b/src/main/java/ru/resprojects/dfops/util/DateTimeUtil.java @@ -0,0 +1,31 @@ +package ru.resprojects.dfops.util; + +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public final class DateTimeUtil { + + public static final String DATE_TIME_PATTERN = "dd.MM.yyyy HH:mm"; + public static final String DATE_TIME_PATTERN_REQUEST = "dd.MM.yyyy-HH:mm"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + + private DateTimeUtil() { + } + + public static String toString(LocalDateTime localDateTime) { + return localDateTime == null ? "" : localDateTime.format(DATE_TIME_FORMATTER); + } + + public static LocalDate parseLocalDate(String localDateString) { + return StringUtils.isEmpty(localDateString) ? null : LocalDate.parse(localDateString); + } + + public static LocalTime parseLocalTime(String localTimeString) { + return StringUtils.isEmpty(localTimeString) ? null : LocalTime.parse(localTimeString); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..679d633 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,80 @@ +spring: + profiles: + active: pgsql + mvc: + servlet: + path: /rest/v1/ +--- +spring: + profiles: pgsql, prod + jpa: + database: postgresql + generate-ddl: true + properties: + hibernate: + jdbc: + lob: + non_contextual_creation: true + database-platform: org.hibernate.dialect.PostgreSQL9Dialect + open-in-view: false + datasource: + platform: postgresql + initialization-mode: never +--- +spring: + profiles: pgsql + datasource: + url: jdbc:postgresql://localhost/test + username: test + password: test +--- +spring: + profiles: prod + datasource: + url: ${DFOPS_PGSQL_DB_HOST}:${DFOPS_PGSQL_DB_PORT}/${DFOPS_PGSQL_DB_NAME} + username: ${DFOPS_PGSQL_DB_USER} + password: ${DFOPS_PGSQL_DB_PASSWORD} +--- +spring: + profiles: test, demo + jpa: + database: h2 + open-in-view: false + hibernate: + ddl-auto: none + database-platform: org.hibernate.dialect.H2Dialect + datasource: + url: jdbc:h2:mem:restsrv;DB_CLOSE_ON_EXIT=FALSE + initialization-mode: always + platform: h2 +--- +spring: + profiles: demo, pgsql, test, prod + jpa: + properties: + hibernate: + format_sql: false + jdbc: + batch_size: 10 + order_inserts: true + order_updates: true +--- +spring: + profiles: test + jpa: + show-sql: false + properties: + hibernate: + generate_statistics: false +--- +logging: + level: + ru.resprojects: debug + org.springframework.transaction: debug + org.springframework: error + pattern: + file: "%d %p %c{1.} [%t] %m%n" + console: "%clr(%d{HH:mm:ss.SSS}){yellow} %clr(%-5p) %clr(---){faint} %clr([%t]){cyan} %clr(%logger{36}){blue} %clr(:){red} %clr(%msg){faint}%n" + file: + name: restsrv.log + max-size: 5MB \ 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..e6a668f --- /dev/null +++ b/src/main/resources/data-h2.sql @@ -0,0 +1,39 @@ +DELETE FROM personal_account_operations; +DELETE FROM employee_personal_accounts; +DELETE FROM employees; +ALTER SEQUENCE seq_employees RESTART WITH 5000; + +INSERT INTO employees (name, email) VALUES +('Ivanov Ivan Ivanovich', 'ivanov@example.com'), +('Petrov Vasily Victorovich', 'petrov@example.com'); + +INSERT INTO employee_personal_accounts (personal_account, employee_id) +VALUES ('4154014152522741', 5000), + ('4131668358915203', 5000), + ('4281563275602455', 5000), + ('4103234971123321', 5001), + ('4132555843841699', 5001); + +INSERT INTO personal_account_operations (operation_date_time, operation_type, operation_value, personal_account_id) +VALUES (parsedatetime('2020-05-30 10:00:00', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 840.35, 5002), + (parsedatetime('2020-05-28 11:05:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 625.00, 5002), + (parsedatetime('2020-05-25 11:41:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 1080.45, 5002), + (parsedatetime('2020-05-30 14:00:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 652.33, 5002), + (parsedatetime('2020-05-26 18:10:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 420.00, 5002), + (parsedatetime('2020-06-30 10:00:00', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 1500.52, 5003), + (parsedatetime('2020-06-30 11:05:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 800.73, 5003), + (parsedatetime('2020-06-30 14:00:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 170.35, 5003), + (parsedatetime('2020-06-30 18:10:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 320.00, 5003), + (parsedatetime('2020-07-15 12:05:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 800.73, 5004), + (parsedatetime('2020-07-15 12:41:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 350.00, 5004), + (parsedatetime('2020-07-15 15:00:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 900.35, 5004), + (parsedatetime('2020-07-15 17:10:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 600.00, 5004), + (parsedatetime('2020-05-15 11:05:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 976.33, 5005), + (parsedatetime('2020-05-15 11:41:10', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 850.00, 5005), + (parsedatetime('2020-05-15 14:00:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 200.00, 5005), + (parsedatetime('2020-05-15 18:10:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 375.85, 5005), + (parsedatetime('2020-04-30 09:00:00', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 1200.52, 5006), + (parsedatetime('2020-04-30 10:35:00', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 300.53, 5006), + (parsedatetime('2020-04-30 10:55:00', 'yyyy-MM-dd hh:mm:ss'), 'DEPOSIT', 450.60, 5006), + (parsedatetime('2020-04-30 12:20:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 300.00, 5006), + (parsedatetime('2020-04-30 14:10:10', 'yyyy-MM-dd hh:mm:ss'), 'WITHDRAW', 402.95, 5006); \ No newline at end of file diff --git a/src/main/resources/data-postgresql.sql b/src/main/resources/data-postgresql.sql new file mode 100644 index 0000000..1756db1 --- /dev/null +++ b/src/main/resources/data-postgresql.sql @@ -0,0 +1,40 @@ +DELETE FROM personal_account_operations; +DELETE FROM employee_personal_accounts; +DELETE FROM employees; +ALTER SEQUENCE seq_employees RESTART WITH 5000; + +INSERT INTO employees (name, email) VALUES +('Ivanov Ivan Ivanovich', 'ivanov@example.com'), +('Petrov Vasily Victorovich', 'petrov@example.com'); + +INSERT INTO employee_personal_accounts (personal_account, employee_id) +VALUES ('4154014152522741', 5000), + ('4131668358915203', 5000), + ('4281563275602455', 5000), + ('4103234971123321', 5001), + ('4132555843841699', 5001); + +INSERT INTO personal_account_operations (operation_date_time, operation_type, operation_value, personal_account_id) +VALUES ('2020-05-30 10:00:00'::timestamp, 'DEPOSIT', 840.35, 5002), + ('2020-05-28 11:05:10'::timestamp, 'DEPOSIT', 625.00, 5002), + ('2020-05-25 11:41:10'::timestamp, 'DEPOSIT', 1080.45, 5002), + ('2020-05-30 14:00:10'::timestamp, 'WITHDRAW', 652.33, 5002), + ('2020-05-26 18:10:10'::timestamp, 'WITHDRAW', 420.00, 5002), + ('2020-06-30 10:00:00'::timestamp, 'DEPOSIT', 1500.52, 5003), + ('2020-06-30 11:05:10'::timestamp, 'DEPOSIT', 800.73, 5003), + ('2020-06-30 14:00:10'::timestamp, 'WITHDRAW', 170.35, 5003), + ('2020-06-30 18:10:10'::timestamp, 'WITHDRAW', 320.00, 5003), + ('2020-07-15 12:05:10'::timestamp, 'DEPOSIT', 800.73, 5004), + ('2020-07-15 12:41:10'::timestamp, 'DEPOSIT', 350.00, 5004), + ('2020-07-15 15:00:10'::timestamp, 'WITHDRAW', 900.35, 5004), + ('2020-07-15 17:10:10'::timestamp, 'WITHDRAW', 600.00, 5004), + ('2020-05-15 11:05:10'::timestamp, 'DEPOSIT', 976.33, 5005), + ('2020-05-15 11:41:10'::timestamp, 'DEPOSIT', 850.00, 5005), + ('2020-05-15 14:00:10'::timestamp, 'WITHDRAW', 200.00, 5005), + ('2020-05-15 18:10:10'::timestamp, 'WITHDRAW', 375.85, 5005), + ('2020-04-30 09:00:00'::timestamp, 'DEPOSIT', 1200.52, 5006), + ('2020-04-30 10:35:00'::timestamp, 'DEPOSIT', 300.53, 5006), + ('2020-04-30 10:55:00'::timestamp, 'DEPOSIT', 450.60, 5006), + ('2020-04-30 12:20:10'::timestamp, 'WITHDRAW', 300.00, 5006), + ('2020-04-30 14:10:10'::timestamp, 'WITHDRAW', 402.95, 5006); + diff --git a/src/main/resources/schema-h2.sql b/src/main/resources/schema-h2.sql new file mode 100644 index 0000000..35eba20 --- /dev/null +++ b/src/main/resources/schema-h2.sql @@ -0,0 +1,35 @@ +DROP TABLE IF EXISTS personal_account_operations; +DROP TABLE IF EXISTS employee_personal_accounts; +DROP TABLE IF EXISTS employees; +DROP SEQUENCE IF EXISTS seq_employees; + +CREATE SEQUENCE seq_employees MINVALUE 5000; + +CREATE TABLE employees +( + id BIGINT DEFAULT seq_employees.nextval PRIMARY KEY, + name VARCHAR NOT NULL, + email VARCHAR NOT NULL +); +CREATE UNIQUE INDEX employees_unique_email_idx ON employees (email); + +CREATE TABLE employee_personal_accounts ( + id BIGINT DEFAULT seq_employees.nextval PRIMARY KEY, + employee_id BIGINT NOT NULL, + personal_account VARCHAR NOT NULL, + FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX employee_personal_accounts_unique_employee_account_idx + ON employee_personal_accounts (employee_id, personal_account); + +CREATE TABLE personal_account_operations ( + id BIGINT DEFAULT seq_employees.nextval PRIMARY KEY, + personal_account_id BIGINT NOT NULL, + operation_date_time TIMESTAMP NOT NULL, + operation_type VARCHAR NOT NULL, + -- Money data on PostgreSQL using Java https://stackoverflow.com/a/18170030 + operation_value DECIMAL NOT NULL, + FOREIGN KEY (personal_account_id) REFERENCES employee_personal_accounts (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX personal_account_operations_unique_account_datetime_idx + ON personal_account_operations (personal_account_id, operation_date_time); \ No newline at end of file diff --git a/src/main/resources/schema-postgresql.sql b/src/main/resources/schema-postgresql.sql new file mode 100644 index 0000000..51fbf05 --- /dev/null +++ b/src/main/resources/schema-postgresql.sql @@ -0,0 +1,35 @@ +DROP TABLE IF EXISTS personal_account_operations; +DROP TABLE IF EXISTS employee_personal_accounts; +DROP TABLE IF EXISTS employees; +DROP SEQUENCE IF EXISTS seq_employees cascade; + +CREATE SEQUENCE seq_employees START 5000; + +CREATE TABLE employees +( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL +); +CREATE UNIQUE INDEX employees_unique_email_idx ON employees (email); + +CREATE TABLE employee_personal_accounts ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + employee_id BIGINT NOT NULL, + personal_account VARCHAR NOT NULL, + FOREIGN KEY (employee_id) REFERENCES employees (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX employee_personal_accounts_unique_employee_account_idx + ON employee_personal_accounts (employee_id, personal_account); + +CREATE TABLE personal_account_operations ( + id BIGINT PRIMARY KEY DEFAULT nextval('seq_employees'), + personal_account_id BIGINT NOT NULL, + operation_date_time TIMESTAMP NOT NULL, + operation_type VARCHAR NOT NULL, + -- Money data on PostgreSQL using Java https://stackoverflow.com/a/18170030 + operation_value DECIMAL NOT NULL, + FOREIGN KEY (personal_account_id) REFERENCES employee_personal_accounts (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX personal_account_operations_unique_account_datetime_idx + ON personal_account_operations (personal_account_id, operation_date_time); \ No newline at end of file diff --git a/src/test/java/ru/resprojects/dfops/controller/AccountControllerTest.java b/src/test/java/ru/resprojects/dfops/controller/AccountControllerTest.java new file mode 100644 index 0000000..64661f1 --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/controller/AccountControllerTest.java @@ -0,0 +1,141 @@ +package ru.resprojects.dfops.controller; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.account.AccountDto; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.service.AccountService; +import ru.resprojects.dfops.service.EmployeeService; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class AccountControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private AccountService accountService; + + @Autowired + private EmployeeService employeeService; + + @Test + public void whenRequestGetAllAccountsThenReturnStatus200AndNotEmptyAccountElementList() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/account/5000", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isNotEmpty(); + } + + @Test + public void whenRequestGetAllAccountsForEmployeeIdWithoutAccountsThenReturnStatus204() { + Employee employee = employeeService.create(new Employee("Alex", "alex@example.com")); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/account/" + employee.getId(), HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + public void whenRequestGetAllAccountsWithIncorrectEmployeeIdThenReturnStatus404() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/account/1", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestCreateAccountThenReturnStatus200AccountWithId() { + AccountDto accountDto = new AccountDto(5000L, "1234"); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/account/create", accountDto, Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getId()).isNotNull(); + } + + @Test + public void whenRequestCreateAccountWithIncorrectEmployeeIdThenReturnStatus404() { + AccountDto accountDto = new AccountDto(1L, "1234"); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/account/create", accountDto, Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestCreateAccountWithIncorrectBankAccountThenReturnStatus400() { + AccountDto accountDto = new AccountDto(5000L, null); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/account/create", accountDto, Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenRequestCreateAccountWithExistentBankAccountThenReturnStatus409() { + AccountDto accountDto = new AccountDto(5000L, "4154014152522741"); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/account/create", accountDto, Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + public void whenRequestAccountByIdThenReturnStatus200AndAccount() { + ResponseEntity response = restTemplate.getForEntity("/rest/v1/account/get/5002", Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + } + + @Test + public void whenTryRequestNonexistentAccountThenReturnStatus404() { + ResponseEntity response = restTemplate.getForEntity("/rest/v1/account/get/1010", Account.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenRequestDeleteAccountByIdThenDeleteAccount() { + restTemplate.delete("/rest/v1/account/5002"); + + accountService.get(5002); + } + + @Test + public void whenTryRequestDeleteAccountWithIncorrectIdThenReturnStatus404() { + ResponseEntity response = restTemplate.exchange("/rest/v1/account/1010", HttpMethod.DELETE, null, Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + +} diff --git a/src/test/java/ru/resprojects/dfops/controller/EmployeeControllerTest.java b/src/test/java/ru/resprojects/dfops/controller/EmployeeControllerTest.java new file mode 100644 index 0000000..05ed90b --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/controller/EmployeeControllerTest.java @@ -0,0 +1,139 @@ +package ru.resprojects.dfops.controller; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.employee.EmployeeDto; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.service.EmployeeService; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class EmployeeControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private EmployeeService employeeService; + + @Test + public void whenRequestGetAllEmployeesThenReturnStatus200AndNotEmptyElementList() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/employee/", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isNotEmpty(); + } + + @Test + public void whenRequestGetAllEmployeesWithPageLimitThenReturnStatus200AndNotEmptyElementList() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/employee/?page=1&limit=1", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isNotEmpty(); + assertThat(response.getBody().getPageCount()).isEqualTo(2); + } + + @Test + public void whenRequestGetEmployeeByIdThenReturnStatus200AndEmployee() { + Employee employee = employeeService.get(5001); + + ResponseEntity response = restTemplate.getForEntity("/rest/v1/employee/get/5001", Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getName()).isEqualTo(employee.getName()); + } + + @Test + public void whenTryRequestGetEmployeeByIdWithIncorrectIdThenReturnStatus404() { + ResponseEntity response = restTemplate.getForEntity("/rest/v1/employee/get/1", Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestGetEmployeeByEmailThenReturnStatus200AndEmployee() { + Employee employee = employeeService.getByEmail("ivanov@example.com"); + + ResponseEntity response = restTemplate.getForEntity("/rest/v1/employee/get?email=ivanov@example.com", Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getId()).isEqualTo(employee.getId()); + } + + @Test + public void whenTryRequestGetEmployeeByEmailWithIncorrectEmailThenReturnStatus404() { + ResponseEntity response = restTemplate.getForEntity("/rest/v1/employee/get?email=123@example.com", Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestCreateEmployeeThenReturnStatus200AndCreatedEmployeeWithId() { + EmployeeDto employeeDto = new EmployeeDto("Alex", "alex@example.com"); + ResponseEntity response = restTemplate.postForEntity("/rest/v1/employee/create", employeeDto, Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getId()).isNotNull(); + } + + @Test + public void whenTryRequestCreateEmployeeWithIncorrectBodyThenReturnStatus403() { + //EmployeeDto employeeDto = new EmployeeDto("Alex", "alex@example.com"); + ResponseEntity response = restTemplate.postForEntity("/rest/v1/employee/create", null, Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void whenTryRequestCreateEmployeeWithEmptyNameThenReturnStatus403() { + EmployeeDto employeeDto = new EmployeeDto(null, "alex@example.com"); + ResponseEntity response = restTemplate.postForEntity("/rest/v1/employee/create", null, Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void whenTryRequestCreateEmployeeWithEmptyEmailThenReturnStatus403() { + EmployeeDto employeeDto = new EmployeeDto("Alex", null); + ResponseEntity response = restTemplate.postForEntity("/rest/v1/employee/create", null, Employee.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenRequestDeleteEmployeeThenReturnDeleteEmployeeFromDb() { + restTemplate.delete("/rest/v1/employee/5001"); + + employeeService.get(5001); + } + + +} diff --git a/src/test/java/ru/resprojects/dfops/controller/OperationControllerTest.java b/src/test/java/ru/resprojects/dfops/controller/OperationControllerTest.java new file mode 100644 index 0000000..346f52d --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/controller/OperationControllerTest.java @@ -0,0 +1,319 @@ +package ru.resprojects.dfops.controller; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.dto.ResponseDto; +import ru.resprojects.dfops.dto.account.AccountBalanceDto; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.dto.operation.OperationDto; +import ru.resprojects.dfops.dto.operation.OperationTransferDto; +import ru.resprojects.dfops.exception.ErrorMessage; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.model.Operation; +import ru.resprojects.dfops.service.AccountService; +import ru.resprojects.dfops.service.OperationService; +import ru.resprojects.dfops.util.DateTimeUtil; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class OperationControllerTest { + + private static final Logger log = LoggerFactory.getLogger(OperationControllerTest.class); + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private OperationService operationService; + + @Autowired + private AccountService accountService; + + @Test + public void whenTransferFromOneAccountToAnotherAccountEmployeeThenReturnStatus200() { + OperationTransferDto transferDto = new OperationTransferDto(5002L, 5003L, 1.0); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/transfer", transferDto, Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void whenTryTransferFromOneAccountEmployeeOneToAnotherAccountEmployeeTwoThenReturnStatus400() { + OperationTransferDto transferDto = new OperationTransferDto(5002L, 5006L, 1.0); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/transfer", transferDto, Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenTryTransferFromOneAccountToAnotherAccountWithIncorrectAccountIdThenReturnStatus400() { + OperationTransferDto transferDto = new OperationTransferDto(null, 5006L, 1.0); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/transfer", transferDto, Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void whenTryTransferFromOneAccountToAnotherAccountWithNonexistentAccountIdThenReturnStatus404() { + OperationTransferDto transferDto = new OperationTransferDto(1L, 5006L, 1.0); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/transfer", transferDto, Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestWithdrawToAccountThenReturnStatus200() { + OperationDto operationDto = new OperationDto(5002L, 1L, Operation.OperationType.WITHDRAW); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/withdraw", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void whenRequestWithdrawWithNonexistentAccountIdThenReturnStatus404() { + OperationDto operationDto = new OperationDto(1010L, 1L, Operation.OperationType.WITHDRAW); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/withdraw", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestWithdrawWithIncorrectAccountIdThenReturnStatus403() { + OperationDto operationDto = new OperationDto(null, 1L, Operation.OperationType.WITHDRAW); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/withdraw", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void whenRequestWithdrawWithIncorrectOperationTypeIdThenReturnStatus400() { + OperationDto operationDto = new OperationDto(5005L, 1L, Operation.OperationType.DEPOSIT); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/withdraw", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenRequestDepositToAccountThenReturnStatus200() { + OperationDto operationDto = new OperationDto(5002L, 1L, Operation.OperationType.DEPOSIT); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/deposit", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void whenRequestDepositWithNonexistentAccountIdThenReturnStatus404() { + OperationDto operationDto = new OperationDto(1010L, 1L, Operation.OperationType.DEPOSIT); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/deposit", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenRequestDepositWithIncorrectAccountIdThenReturnStatus403() { + OperationDto operationDto = new OperationDto(null, 1L, Operation.OperationType.DEPOSIT); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/deposit", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void whenRequestDepositWithIncorrectOperationTypeIdThenReturnStatus400() { + OperationDto operationDto = new OperationDto(null, 1L, Operation.OperationType.WITHDRAW); + + ResponseEntity response = restTemplate.postForEntity("/rest/v1/operation/deposit", operationDto, Operation.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenGetAllOperationsForAccountThenReturnStatus200AndNotNullOperationList() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/operation/5002", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isNotEmpty(); + } + + @Test + public void whenNoOperationsForAccountThenReturnStatus204() { + Account account = accountService.create(new Employee(5000L, "1", "1"), "123"); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/operation/" + account.getId(), HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + public void whenTryGetAllOperationsForNonexistentAccountThenReturnStatus404() { + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange("/rest/v1/operation/1", HttpMethod.GET, null, responseType); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void whenGetCurrentBalanceForAccountThenReturnStatus200AndCurrentBalance() { + BigDecimal balanceAccount5002 = operationService.getCurrentBalance(5002); + + ResponseEntity response = restTemplate.getForEntity("/rest/v1/operation/5002/balance", AccountBalanceDto.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getBalance()).isEqualByComparingTo(balanceAccount5002); + } + + @Test + public void whenGetCreditForPeriodForAccountThenReturnStatus200AndCredit() { + DateTimeFormatter format = DateTimeFormatter.ofPattern(DateTimeUtil.DATE_TIME_PATTERN_REQUEST); + LocalDateTime start = LocalDateTime.of(2020, Month.MAY, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2020, Month.MAY, 31, 23, 59); + String startDateTime = start.format(format); + String endDateTime = end.format(format); + BigDecimal creditAccount5002 = operationService.getCreditBetween(5002, start, end); + + ResponseEntity response = restTemplate.getForEntity( + "/rest/v1/operation/5002/credit/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + OperationAmountDto.class + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getOperationType()).isEqualTo(Operation.OperationType.WITHDRAW); + assertThat(response.getBody().getSum()).isEqualByComparingTo(creditAccount5002); + } + + @Test + public void whenGetCreditForPeriodWithEndDateTimeLaterThanStartDateTimeThenReturnStatus400() { + DateTimeFormatter format = DateTimeFormatter.ofPattern(DateTimeUtil.DATE_TIME_PATTERN_REQUEST); + LocalDateTime start = LocalDateTime.of(2020, Month.MAY, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2020, Month.MAY, 31, 23, 59); + String startDateTime = end.format(format); + String endDateTime = start.format(format); + + ResponseEntity response = restTemplate.getForEntity( + "/rest/v1/operation/5002/credit/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + ErrorMessage.class + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenGetCreditForPeriodWithEmptyStartDateTimeThenReturnStatus400() { + String startDateTime = ""; + String endDateTime = ""; + + ResponseEntity response = restTemplate.getForEntity( + "/rest/v1/operation/5002/credit/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + ErrorMessage.class + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void whenGetCreditForPeriodWhereNoOperationWithdrawThenReturnStatus200AndCreditZero() { + DateTimeFormatter format = DateTimeFormatter.ofPattern(DateTimeUtil.DATE_TIME_PATTERN_REQUEST); + LocalDateTime start = LocalDateTime.of(2020, Month.JANUARY, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2020, Month.JANUARY, 31, 23, 59); + String startDateTime = start.format(format); + String endDateTime = end.format(format); + + ResponseEntity response = restTemplate.getForEntity( + "/rest/v1/operation/5002/credit/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + OperationAmountDto.class + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getOperationType()).isEqualTo(Operation.OperationType.WITHDRAW); + assertThat(response.getBody().getSum()).isEqualByComparingTo(new BigDecimal("0.00")); + } + + @Test + public void whenGetOperationsAmountForPeriodThenReturnStatus200AndOperationsAmount() { + DateTimeFormatter format = DateTimeFormatter.ofPattern(DateTimeUtil.DATE_TIME_PATTERN_REQUEST); + LocalDateTime start = LocalDateTime.of(2020, Month.MAY, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2020, Month.MAY, 31, 23, 59); + String startDateTime = start.format(format); + String endDateTime = end.format(format); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + List operationAmountDtos = operationService.getOperationsAmountBetween(5002, start, end); + + ResponseEntity> response = restTemplate.exchange( + "/rest/v1/operation/5002/operations_amount/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + HttpMethod.GET, + null, + responseType + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isNotEmpty(); + assertThat(response.getBody().getElements()).isSubsetOf(operationAmountDtos); + } + + @Test + public void whenGetOperationsAmountForPeriodWhereNoDepositAndWithdrawThenReturnStatus200AndEmptyList() { + DateTimeFormatter format = DateTimeFormatter.ofPattern(DateTimeUtil.DATE_TIME_PATTERN_REQUEST); + LocalDateTime start = LocalDateTime.of(2020, Month.JANUARY, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2020, Month.JANUARY, 31, 23, 59); + String startDateTime = start.format(format); + String endDateTime = end.format(format); + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; + + ResponseEntity> response = restTemplate.exchange( + "/rest/v1/operation/5002/operations_amount/filter?startDateTime=" + startDateTime + "&endDateTime=" + endDateTime, + HttpMethod.GET, + null, + responseType + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getElements()).isEmpty(); + } + +} diff --git a/src/test/java/ru/resprojects/dfops/service/AccountServiceTest.java b/src/test/java/ru/resprojects/dfops/service/AccountServiceTest.java new file mode 100644 index 0000000..9735e02 --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/service/AccountServiceTest.java @@ -0,0 +1,90 @@ +package ru.resprojects.dfops.service; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.DfopsApplication; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DfopsApplication.class) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class AccountServiceTest { + + private static final long EMPLOYEE_ID = 5000L; + + @Autowired + private AccountService accountService; + + private Account savedAccount; + + @Before + public void init() { + Employee employee = new Employee(EMPLOYEE_ID, "Ivanov Ivan Ivanovich", "ivanov@example.com"); + savedAccount = accountService.create(employee, "1234567899876543"); + } + + @Test + public void whenGetAllAccountsThenReturnNotEmptyList() { + Page accounts = accountService.getAll(EMPLOYEE_ID, 1, 10); + + assertThat(accounts.hasContent()).isTrue(); + assertThat(accounts.getContent()).isNotEmpty(); + } + + @Test + public void whenGetAccountByIdThenReturnAccount() { + Account account = accountService.get(savedAccount.getId()); + + assertThat(account).isNotNull(); + assertThat(account).isEqualTo(savedAccount); + assertThat(account.getEmployee().getId()).isNotNull(); + } + + @Test + public void deleteAccountTest() { + accountService.delete(savedAccount.getId()); + + Page accounts = accountService.getAll(EMPLOYEE_ID, 1, 10); + + assertThat(accounts.hasContent()).isTrue(); + assertThat(accounts.getContent().contains(savedAccount)).isFalse(); + } + + @Test(expected = BadResourceException.class) + public void whenTrySaveNullThenException() { + accountService.create(null, null); + } + + @Test(expected = ResourceAlreadyExistsException.class) + public void whenSaveAccountWithExistingBankAccountThenException() { + accountService.create(savedAccount.getEmployee(), savedAccount.getPersonalAccount()); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenTryDeleteByNonexistentIdThenException() { + accountService.delete(5); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenGetByNonexistentIdThenException() { + accountService.get(1); + } + +} diff --git a/src/test/java/ru/resprojects/dfops/service/EmployeeServiceTest.java b/src/test/java/ru/resprojects/dfops/service/EmployeeServiceTest.java new file mode 100644 index 0000000..a556715 --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/service/EmployeeServiceTest.java @@ -0,0 +1,98 @@ +package ru.resprojects.dfops.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.DfopsApplication; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.exception.ResourceAlreadyExistsException; +import ru.resprojects.dfops.exception.ResourceNotFoundException; +import ru.resprojects.dfops.model.Employee; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DfopsApplication.class) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class EmployeeServiceTest { + + @Autowired + private EmployeeService employeeService; + + @Test + public void whenSaveEmployeeThenReturnEmployeeWithId() { + Employee employee = new Employee("Alex", "example@example.com"); + + Employee employeeWithId = employeeService.create(employee); + + assertThat(employeeWithId).isNotNull(); + assertThat(employeeWithId.getId()).isNotNull(); + } + + @Test + public void whenGetByEmailThenReturnEmploy() { + Employee employee = new Employee("Alex", "example@example.com"); + employeeService.create(employee); + + Employee existingEmployee = employeeService.getByEmail("example@example.com"); + + assertThat(existingEmployee).isNotNull(); + } + + @Test + public void deleteEmployeeTest() { + Employee employee = new Employee("Alex", "example@example.com"); + Employee savedEmployee = employeeService.create(employee); + + employeeService.delete(savedEmployee.getId()); + + Page employees = employeeService.getAll(1, 10); + assertThat(employees.hasContent()).isTrue(); + assertThat(employees.getContent().contains(savedEmployee)).isFalse(); + } + + @Test + public void whenGetAllEmployeesThenReturnNotEmptyListOfEmployees() { + Page employees = employeeService.getAll(1, 10); + + assertThat(employees.getContent()).isNotEmpty(); + } + + @Test(expected = BadResourceException.class) + public void whenTrySaveNullThenException() { + employeeService.create(null); + } + + @Test(expected = ResourceAlreadyExistsException.class) + public void whenSaveEmployeeWithExistingEmailThenException() { + Employee employee = new Employee("Alex", "example@example.com"); + employeeService.create(employee); + + employeeService.create(employee); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenTryDeleteByNonexistentIdThenException() { + employeeService.delete(5); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenGetByNonexistentIdThenException() { + employeeService.get(1); + } + + @Test(expected = ResourceNotFoundException.class) + public void whenGetByNonexistentEmailThenException() { + employeeService.getByEmail("abc@example.com"); + } + +} diff --git a/src/test/java/ru/resprojects/dfops/service/OperationServiceTest.java b/src/test/java/ru/resprojects/dfops/service/OperationServiceTest.java new file mode 100644 index 0000000..e49aad7 --- /dev/null +++ b/src/test/java/ru/resprojects/dfops/service/OperationServiceTest.java @@ -0,0 +1,223 @@ +package ru.resprojects.dfops.service; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.resprojects.dfops.DfopsApplication; +import ru.resprojects.dfops.dto.operation.OperationAmountDto; +import ru.resprojects.dfops.exception.BadResourceException; +import ru.resprojects.dfops.model.Account; +import ru.resprojects.dfops.model.Employee; +import ru.resprojects.dfops.model.Operation; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DfopsApplication.class) +@ActiveProfiles(profiles = {"test"}) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + scripts = {"classpath:schema-h2.sql", "classpath:data-h2.sql"}, + config = @SqlConfig(encoding = "UTF-8")) +public class OperationServiceTest { + + private static final long EMPLOYEE_ONE_ID = 5000L; + private static final long EMPLOYEE_TWO_ID = 5001L; + private static final long ACCOUNT_ONE_EMPLOYEE_ONE_ID = 5002L; + private static final long ACCOUNT_TWO_EMPLOYEE_ONE_ID = 5003L; + private static final long ACCOUNT_THREE_EMPLOYEE_TWO_ID = 5006L; + private static final double CURRENT_BALANCE_ACCOUNT_ONE = 1473.47; + private static final long ALL_OPERATIONS_ACCOUNT_ONE = 5; + + @Autowired + private OperationService operationService; + + private Account accountOne; + private Account accountTwo; + private Account accountThree; + + @Before + public void init() { + Employee employeeOne = new Employee(EMPLOYEE_ONE_ID, "Ivanov Ivan Ivanovich", "ivanov@example.com"); + Employee employeeTwo = new Employee(EMPLOYEE_TWO_ID, "Petrov Vasily Victorovich", "petrov@example.com"); + accountOne = new Account(ACCOUNT_ONE_EMPLOYEE_ONE_ID, employeeOne, "4154014152522741"); + accountTwo = new Account(ACCOUNT_TWO_EMPLOYEE_ONE_ID, employeeOne, "4131668358915203"); + accountThree = new Account(ACCOUNT_THREE_EMPLOYEE_TWO_ID, employeeTwo, "4132555843841699"); + } + + @Test + public void getCurrentBalanceTest() { + double currentBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + + assertThat(currentBalance).isEqualTo(CURRENT_BALANCE_ACCOUNT_ONE); + } + + @Test + public void whenDepositToAccountThenIncreaseCurrentBalance() { + double amount = 200; + double originalBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + + operationService.deposit(accountOne, amount); + + double changedBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + assertThat(changedBalance).isGreaterThan(originalBalance); + assertThat(changedBalance - originalBalance).isEqualTo(amount); + } + + @Test + public void whenWithdrawFromAccountThenDecreaseCurrentBalance() { + double amount = 200; + double originalBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + + operationService.withdraw(accountOne, amount); + + double changedBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + assertThat(changedBalance).isLessThan(originalBalance); + assertThat(originalBalance - changedBalance).isEqualTo(amount); + } + + @Test(expected = BadResourceException.class) + public void whenTryWithdrawFromAccountMoreThanCurrentBalanceThenException() { + double amount = 200; + double originalBalance = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + + operationService.withdraw(accountOne, originalBalance + amount); + } + + @Test + public void whenGetDebitBetweenThenReturnDebitAtPeriod() { + double depositAmount = 200.00; + LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23, 59)); + operationService.deposit(accountOne, depositAmount); + + double debitAtPeriod = operationService.getDebitBetween(ACCOUNT_ONE_EMPLOYEE_ONE_ID, start, end).doubleValue(); + + assertThat(debitAtPeriod).isEqualTo(depositAmount); + } + + @Test + public void whenGetDebitBetweenWithNonexistentAccountIdThenReturnZeroValue() { + LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23, 59)); + + BigDecimal result = operationService.getDebitBetween(123, start, end); + + assertThat(result).isZero(); + } + + @Test + public void whenGetCreditBetweenThenReturnCreditAtPeriod() { + double withdrawAmount = 200.00; + LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23, 59)); + operationService.withdraw(accountOne, withdrawAmount); + + double creditAtPeriod = operationService.getCreditBetween(ACCOUNT_ONE_EMPLOYEE_ONE_ID, start, end).doubleValue(); + + assertThat(creditAtPeriod).isEqualTo(withdrawAmount); + } + + @Test + public void whenGetCreditBetweenWithNonexistentAccountIdThenReturnZeroValue() { + LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23, 59)); + + BigDecimal result = operationService.getCreditBetween(123, start, end); + + assertThat(result).isZero(); + } + + @Test + public void whenGetAllByAccountIdThenReturnAllOperationsForAccountId() { + Page allOperationsForAccountOne = operationService.getAllByAccountId(ACCOUNT_ONE_EMPLOYEE_ONE_ID, 1, 10); + + assertThat(allOperationsForAccountOne.hasContent()).isTrue(); + assertThat(allOperationsForAccountOne.getContent()).isNotEmpty(); + assertThat(allOperationsForAccountOne.getContent().size()).isEqualTo(ALL_OPERATIONS_ACCOUNT_ONE); + } + + @Test + public void whenGetAllByAccountIdWithNonexistentAccountThenReturnEmptyList() { + Page allOperationsForAccountOne = operationService.getAllByAccountId(123, 1, 10); + + assertThat(allOperationsForAccountOne.hasContent()).isFalse(); + } + + @Test + public void whenGetAllByAccountIdBetweenThenReturnAllOperationsAtPeriodForAccountId() { + operationService.deposit(accountOne, 10.00); + operationService.withdraw(accountOne, 10.00); + LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23, 59)); + Page allOperationsForAccountOne = operationService.getAllByAccountIdBetween(ACCOUNT_ONE_EMPLOYEE_ONE_ID, start, end, 1, 10); + + assertThat(allOperationsForAccountOne.hasContent()).isTrue(); + assertThat(allOperationsForAccountOne.getContent()).isNotEmpty(); + assertThat(allOperationsForAccountOne.getContent().size()).isEqualTo(2); + } + + @Test + public void whenGetAllByAccountIdBetweenWithNonexistentAccountThenReturnEmptyList() { + LocalDateTime start = LocalDateTime.of(LocalDate.of(2020, Month.JANUARY, 1), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.of(2020, Month.DECEMBER, 31), LocalTime.of(23, 59)); + Page allOperationsForAccountOne = operationService.getAllByAccountIdBetween(123, start, end, 1, 10); + + assertThat(allOperationsForAccountOne.hasContent()).isFalse(); + } + + @Test + public void whenTransferFromAccountOneToAccountTwoThenWithdrawFromAccountOneAndDepositToAccountTwo() { + double transferAmount = 1.00; + double currentBalanceAccountOne = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + double currentBalanceAccountTwo = operationService.getCurrentBalance(ACCOUNT_TWO_EMPLOYEE_ONE_ID).doubleValue(); + + operationService.transfer(accountOne, accountTwo, transferAmount); + + double currentBalanceAfterTransferAccountOne = operationService.getCurrentBalance(ACCOUNT_ONE_EMPLOYEE_ONE_ID).doubleValue(); + double currentBalanceAfterTransferAccountTwo = operationService.getCurrentBalance(ACCOUNT_TWO_EMPLOYEE_ONE_ID).doubleValue(); + + assertThat(currentBalanceAfterTransferAccountOne).isLessThan(currentBalanceAccountOne); + assertThat(currentBalanceAfterTransferAccountTwo).isGreaterThan(currentBalanceAccountTwo); + assertThat(currentBalanceAccountOne - currentBalanceAfterTransferAccountOne).isEqualTo(transferAmount); + assertThat(currentBalanceAfterTransferAccountTwo - currentBalanceAccountTwo).isEqualTo(transferAmount); + } + + @Test(expected = BadResourceException.class) + public void whenTransferFromAccountOneToAccountThreeThenException() { + double transferAmount = 1.00; + + operationService.transfer(accountOne, accountThree, transferAmount); + } + + @Test(expected = BadResourceException.class) + public void whenTransferFromAccountOneToAccountTwoButTransferAmountMoreThanCurrentBalanceFromAccountOneThenException() { + double transferAmount = operationService.getCurrentBalance(accountOne.getId()).doubleValue() + 10.00; + + operationService.transfer(accountOne, accountThree, transferAmount); + } + + //TODO переделать тест + @Test + public void whenGetOperationsAmountBetweenThenReturnListOfOperationsAmount() { + LocalDateTime start = LocalDateTime.of(LocalDate.of(2020, Month.MAY, 1), LocalTime.of(0, 0)); + LocalDateTime end = LocalDateTime.of(LocalDate.of(2020, Month.MAY, 31), LocalTime.of(23, 59)); + List result = operationService.getOperationsAmountBetween(ACCOUNT_ONE_EMPLOYEE_ONE_ID, start, end); + + assertThat(result).isNotEmpty(); + } + +}