Added project

This commit is contained in:
2023-06-11 00:29:45 +03:00
parent 35971990e6
commit 27cf303639
56 changed files with 4125 additions and 24 deletions

52
.gitignore vendored
View File

@@ -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/

View File

@@ -1,4 +1,65 @@
# dfops-rest-service
# Задание
Тестовое задание. Напишите Spring-приложение, предоставляющее REST-сервис для приема и регистрации операций в базе данных по расчетам с водителями. У
каждого водителя может быть несколько лицевых счетов.
Напишите 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

101
dfops_linux.sh Executable file
View File

@@ -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

5
docker/db/Dockerfile Normal file
View File

@@ -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/*

View File

@@ -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);

View File

@@ -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);

15
docker/docker-compose.yml Normal file
View File

@@ -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

184
gradlew vendored Executable file
View File

@@ -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" "$@"

104
gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
CREATE DATABASE test;
CREATE USER "user" WITH password 'password';
GRANT ALL PRIVILEGES ON DATABASE test TO "user";

View File

@@ -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);

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'dfops'

View File

@@ -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);
}
}

View File

@@ -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-сервис для приема и регистрации операций в базе данных по расчетам с водителями."));
}
}

View File

@@ -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<ResponseDto<Account>> 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<Account> 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<Account> 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<Account> 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<Void> 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);
}
}

View File

@@ -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<ResponseDto<Employee>> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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);
}
}

View File

@@ -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<ResponseDto<ru.resprojects.dfops.model.Operation>> 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<ru.resprojects.dfops.model.Operation> 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<ResponseDto<ru.resprojects.dfops.model.Operation>> 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<ru.resprojects.dfops.model.Operation> 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<ResponseDto<OperationAmountDto>> 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<OperationAmountDto> result = operationService.getOperationsAmountBetween(account.getId(), startDateTime, endDateTime);
ResponseDto<OperationAmountDto> 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<OperationAmountDto> 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<OperationAmountDto> 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<AccountBalanceDto> 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<ru.resprojects.dfops.model.Operation> 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<ru.resprojects.dfops.model.Operation> 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<Void> 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");
}
}
}

View File

@@ -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<ErrorMessage> resourceNotFound(HttpServletRequest request, ResourceNotFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {BadResourceException.class})
public ResponseEntity<ErrorMessage> badResource(HttpServletRequest request, BadResourceException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {BadRequestException.class})
public ResponseEntity<ErrorMessage> badRequest(HttpServletRequest request, BadRequestException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {ResourceAlreadyExistsException.class})
public ResponseEntity<ErrorMessage> alreadyExistResource(HttpServletRequest request, ResourceAlreadyExistsException exception) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(exception.getErrorMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorMessage> handleError(HttpServletRequest requesr, Exception exception) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorMessage("Unexpected error: " + exception.getMessage()));
}
}

View File

@@ -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<T> implements Serializable {
private static final long serialVersionUID = -3654370454854576301L;
@Schema(description = "Список элементов")
@JsonProperty("elements")
private Collection<T> 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<T> response) {
this.elements = response.getContent();
this.currentPage = response.getNumber() + 1;
this.totalItems = response.getTotalElements();
this.pageCount = response.getTotalPages();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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<Operation> 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 +
'}';
}
}

View File

@@ -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<Account> 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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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<Account, Long> {
Page<Account> 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<Account> getWithEmployee(@Param("id") long id);
}

View File

@@ -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<Employee, Long> {
Optional<Employee> findByEmail(String email);
boolean existsByEmail(String email);
}

View File

@@ -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<Operation, Long> {
Page<Operation> 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<Operation> 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<OperationAmountDto> 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<OperationAmountDto> 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<BigDecimal> getOperationAmountBetween(
@Param("accountId") long accountId,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
@Param("operationType") Operation.OperationType operationType
);
}

View File

@@ -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<Account> getAll(long employeeId, int pageNumber, int rowPerPage);
}

View File

@@ -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<Account> getAll(long employeeId, int pageNumber, int rowPerPage) {
return accountRepository.findAllByEmployee_Id(employeeId, PageRequest.of(pageNumber - 1, rowPerPage));
}
}

View File

@@ -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<Employee> getAll(int pageNumber, int rowPerPage);
}

View File

@@ -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<Employee> getAll(int pageNumber, int rowPerPage) {
return employeeRepository.findAll(PageRequest.of(pageNumber - 1, rowPerPage));
}
}

View File

@@ -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<OperationAmountDto> getOperationsAmountBetween(long accountId, LocalDateTime start, LocalDateTime end);
Page<Operation> getAllByAccountId(long accountId, int pageNumber, int rowPerPage);
Page<Operation> getAllByAccountIdBetween(long accountId, LocalDateTime start, LocalDateTime end, int pageNumber, int rowPerPage);
void transfer(Account accountFrom, Account accountTo, double amount) throws BadResourceException;
}

View File

@@ -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<OperationAmountDto> 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<OperationAmountDto> getOperationsAmountBetween(long accountId, LocalDateTime start, LocalDateTime end) {
return operationRepository.getOperationsAmountBetween(accountId, start, end);
}
@Override
public Page<Operation> getAllByAccountId(long accountId, int pageNumber, int rowPerPage) {
return operationRepository.getAllByAccount_Id(accountId, PageRequest.of(pageNumber - 1, rowPerPage));
}
@Override
public Page<Operation> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<ResponseDto<Account>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Account>> 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<ResponseDto<Account>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Account>> response = restTemplate.exchange("/rest/v1/account/" + employee.getId(), HttpMethod.GET, null, responseType);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
}
@Test
public void whenRequestGetAllAccountsWithIncorrectEmployeeIdThenReturnStatus404() {
ParameterizedTypeReference<ResponseDto<Account>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Account>> 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<Account> 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<Account> 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<Account> 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<Account> response = restTemplate.postForEntity("/rest/v1/account/create", accountDto, Account.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
}
@Test
public void whenRequestAccountByIdThenReturnStatus200AndAccount() {
ResponseEntity<Account> 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<Account> 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<Void> response = restTemplate.exchange("/rest/v1/account/1010", HttpMethod.DELETE, null, Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
}

View File

@@ -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<ResponseDto<Employee>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Employee>> 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<ResponseDto<Employee>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Employee>> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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<Employee> 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);
}
}

View File

@@ -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<Void> 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<Void> 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<Void> 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<Void> 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<Operation> 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<Operation> 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<Operation> 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<Operation> 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<Operation> 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<Operation> 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<Operation> 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<Operation> response = restTemplate.postForEntity("/rest/v1/operation/deposit", operationDto, Operation.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void whenGetAllOperationsForAccountThenReturnStatus200AndNotNullOperationList() {
ParameterizedTypeReference<ResponseDto<Operation>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Operation>> 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<ResponseDto<Operation>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Operation>> response = restTemplate.exchange("/rest/v1/operation/" + account.getId(), HttpMethod.GET, null, responseType);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
}
@Test
public void whenTryGetAllOperationsForNonexistentAccountThenReturnStatus404() {
ParameterizedTypeReference<ResponseDto<Operation>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<Operation>> 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<AccountBalanceDto> 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<OperationAmountDto> 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<ErrorMessage> 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<ErrorMessage> 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<OperationAmountDto> 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<ResponseDto<OperationAmountDto>> responseType = new ParameterizedTypeReference<>() { };
List<OperationAmountDto> operationAmountDtos = operationService.getOperationsAmountBetween(5002, start, end);
ResponseEntity<ResponseDto<OperationAmountDto>> 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<ResponseDto<OperationAmountDto>> responseType = new ParameterizedTypeReference<>() { };
ResponseEntity<ResponseDto<OperationAmountDto>> 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();
}
}

View File

@@ -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<Account> 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<Account> 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);
}
}

View File

@@ -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<Employee> employees = employeeService.getAll(1, 10);
assertThat(employees.hasContent()).isTrue();
assertThat(employees.getContent().contains(savedEmployee)).isFalse();
}
@Test
public void whenGetAllEmployeesThenReturnNotEmptyListOfEmployees() {
Page<Employee> 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");
}
}

View File

@@ -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<Operation> 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<Operation> 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<Operation> 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<Operation> 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<OperationAmountDto> result = operationService.getOperationsAmountBetween(ACCOUNT_ONE_EMPLOYEE_ONE_ID, start, end);
assertThat(result).isNotEmpty();
}
}