() {
+ });
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ }
+}
\ No newline at end of file
diff --git a/sut-market/.dockerignore b/sut-market/.dockerignore
new file mode 100644
index 0000000..4ffb597
--- /dev/null
+++ b/sut-market/.dockerignore
@@ -0,0 +1,6 @@
+.dockerignore
+**/*.log
+**/*.err
+Dockerfile
+.git
+.gitignore
\ No newline at end of file
diff --git a/sut-market/.github/workflows/maven.yml b/sut-market/.github/workflows/maven.yml
new file mode 100644
index 0000000..64d435e
--- /dev/null
+++ b/sut-market/.github/workflows/maven.yml
@@ -0,0 +1,39 @@
+name: build
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: Build Maven project
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+
+ - name: Cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.m2/repository
+ key: m2
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+
+ - name: Compile
+ run: mvn --file pom.xml clean compile
+
+ - name: Build
+ run: mvn --file pom.xml install
+
+ - name: Report coverage to Codacy
+ shell: bash
+ env:
+ CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
+ run: |
+ if [ "$CODACY_PROJECT_TOKEN" != "" ] ; then
+ bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Java -r market-coverage/target/coverage-report/coverage-report.xml
+ else
+ echo "No CODACY_PROJECT_TOKEN provided for Codacy report"
+ fi
\ No newline at end of file
diff --git a/sut-market/.github/workflows/publish.yml b/sut-market/.github/workflows/publish.yml
new file mode 100644
index 0000000..5470dfc
--- /dev/null
+++ b/sut-market/.github/workflows/publish.yml
@@ -0,0 +1,30 @@
+name: publish
+
+on:
+ push:
+ branches:
+ - master
+ release:
+ types: [published, created, edited]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
+ settings-path: ${{ github.workspace }} # location for the settings.xml file
+
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
+
+ - name: Publish to GitHub Packages Apache Maven
+ run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
\ No newline at end of file
diff --git a/sut-market/.gitignore b/sut-market/.gitignore
new file mode 100644
index 0000000..042d1d5
--- /dev/null
+++ b/sut-market/.gitignore
@@ -0,0 +1,17 @@
+!.idea
+.idea/*
+!.idea/codeStyles
+*.iml
+.gradle
+build
+classes
+*/out
+.classpath
+.project
+.settings
+*/bin
+*.orig
+.attach_pid*
+java_pid*.hprof
+.DS_Store
+!.mvn/wrapper/maven-wrapper.jar
\ No newline at end of file
diff --git a/sut-market/.idea/codeStyles/Project.xml b/sut-market/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..1b52a67
--- /dev/null
+++ b/sut-market/.idea/codeStyles/Project.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sut-market/.idea/codeStyles/codeStyleConfig.xml b/sut-market/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/sut-market/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sut-market/.mvn/wrapper/MavenWrapperDownloader.java b/sut-market/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/sut-market/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present 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.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/sut-market/.mvn/wrapper/maven-wrapper.jar b/sut-market/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..2cc7d4a
Binary files /dev/null and b/sut-market/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/sut-market/.mvn/wrapper/maven-wrapper.properties b/sut-market/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..642d572
--- /dev/null
+++ b/sut-market/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/sut-market/Dockerfile b/sut-market/Dockerfile
new file mode 100644
index 0000000..a8184b4
--- /dev/null
+++ b/sut-market/Dockerfile
@@ -0,0 +1,26 @@
+# cache offline as much dependencies as possible
+FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c as dependencies
+WORKDIR /app
+COPY market-core/pom.xml market-core/pom.xml
+COPY market-rest/pom.xml market-rest/pom.xml
+COPY market-web/pom.xml market-web/pom.xml
+COPY market-coverage/pom.xml market-coverage/pom.xml
+COPY pom.xml .
+RUN mvn -B -e -C org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline
+
+# build the project
+FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c as build
+WORKDIR /app
+COPY --from=dependencies /root/.m2 /root/.m2
+COPY --from=dependencies /app/ /app
+COPY market-core/src /app/market-core/src
+COPY market-rest/src /app/market-rest/src
+COPY market-web/src /app/market-web/src
+RUN mvn -B -e clean install -DskipTests=true
+
+# create an image with the specified application
+FROM adoptopenjdk/openjdk11:alpine-jre@sha256:89d70c41f6642605c5a7c655969e386815f2f4c0cf923bc1d87e2eadf8669330
+WORKDIR /app
+ARG module
+COPY --from=build /app/${module}/target/*.jar ./java-app.jar
+ENTRYPOINT ["java","-jar","/app/java-app.jar"]
\ No newline at end of file
diff --git a/sut-market/LICENSE b/sut-market/LICENSE
new file mode 100644
index 0000000..d5aea4a
--- /dev/null
+++ b/sut-market/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Aleksey Lukyanets
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/sut-market/README.adoc b/sut-market/README.adoc
new file mode 100644
index 0000000..b2a2819
--- /dev/null
+++ b/sut-market/README.adoc
@@ -0,0 +1,128 @@
+= Simple internet-market
+
+image:https://api.codacy.com/project/badge/Grade/8c0bd51bdba44e04bd2cbbfd7f643e9f[link=https://www.codacy.com/manual/aleksey-lukyanets/market?utm_source=github.com&utm_medium=referral&utm_content=aleksey-lukyanets/market&utm_campaign=Badge_Grade]
+image:https://api.codacy.com/project/badge/Coverage/8c0bd51bdba44e04bd2cbbfd7f643e9f[link=https://www.codacy.com/manual/aleksey-lukyanets/market?utm_source=github.com&utm_medium=referral&utm_content=aleksey-lukyanets/market&utm_campaign=Badge_Coverage]
+image:https://github.com/aleksey-lukyanets/market/workflows/build/badge.svg[link]
+
+This project was started in 2014 as a study for Spring and related web technologies,
+partly modernized in 2020-21. Since around 2014 professionally I am focused on backend development,
+a modernization/maintenance of this project continues occasionally.
+This is not a perfectly finished project, so it may contain imperfection and some mess.
+
+== Technologies
+
+* servlet: ``Spring MVC`` ``Thymeleaf`` ``jQuery`` ``Spring Security``
+* data: ``Spring Data JPA`` ``H2`` ``Hibernate``
+* RESTful service: ``Spring MVC`` ``Spring HATEOAS``
+* tests: ``JUnit`` ``Hamcrest`` ``Mockito`` ``JSONPath``
+
+== How to
+
+The project is divided to modules and includes two applications:
+
+* Web-application, module *market-web*, default port: 8080
+** visit ``localhost:8080`` and ``localhost:8080/admin`` in your browser to enjoy the app
+** visit ``localhost:8080/h2-console/`` to access H2 console
+* REST-service, module *market-rest*, default port: 8081
+** visit ``localhost:8081/products`` to observe market products
+** visit ``localhost:8081/swagger-ui/#/`` to observe Swagger API page
+** visit ``localhost:8081/h2-console/`` to access H2 console
+
+These applications are designed to use similar database scheme and could be run together.
+
+You are able to open the project in your favourite IDE and simply run the main class of the application.
+
+By default an app is started with its own in-memory https://www.h2database.com[H2 database]
+and thus all the data is dropped after stopping the app.
+To connect to a standalone https://www.postgresql.org/[PostgreSQL] database run application with active Spring profile ``prod``.
+
+=== Running with Maven
+
+Each application could be run separately by executing the following Maven command
+against a specific module:
+
+* Web-application: ``./mvnw spring-boot:run -pl market-web``
+* REST-service: ``./mvnw spring-boot:run -pl market-rest``
+
+=== Running with Docker
+
+Both applications are packed into https://docs.docker.com/[Docker] images
+(available https://hub.docker.com/u/alukyanets[on Docker Hub]).
+
+To run the Web-application with Docker you shoukd have Docker installed and follow the steps below:
+
+* pull the image from Docker Hub: ``docker pull alukyanets/market-web``
+* run a container with application listening on port 8080: ``docker run -p 8080:8080 alukyanets/market-web``
+
+Quite similar for the REST-service:
+
+* pull the image from Docker Hub: ``docker pull alukyanets/market-rest``
+* run a container with application listening on port 8081: ``docker run -p 8081:8081 alukyanets/market-rest``
+
+=== Running with docker-compose
+
+https://docs.docker.com/compose/[Docker-compose] makes possible starting with a single command
+both an application and a standalone PostgreSQL database volume:
+
+* ``docker-compose -f docker-compose-web.yaml up`` or ``docker-compose -f docker-compose-rest.yaml up``
+
+or running both applications sharing the same standalone PostgreSQL volume:
+
+* ``docker-compose -f docker-compose.yaml up``
+
+To stop the running containers use ``Ctrl + C``. You are able to remove all the containers, e.g.:
+
+* ``docker-compose -f docker-compose.yaml down``
+
+The created PostgreSQL volume will keep database state after stopping application.
+
+== Applications attributes
+
+[%header,cols=".^2,.^3,.^3",width=90%]
+|===
+| |Web application |RESTful service
+|*module* |market-web |market-rest
+|*Java version* 2+^|11
+|*main class* |``WebApplication.java`` |``RestApplication.java``
+|*default port* |8080 |8081
+|*basic URL* |``localhost:8080``, ``localhost:8080/admin`` |``localhost:8081/products``
+|*user login* 2+|``admin/password`` ``ivan.petrov@yandex.ru/petrov``
+|*logs* 2+|``logs/``
+|*H2 console* |``localhost:8080/h2-console/`` |``localhost:8081/h2-console/``
+|*Swagger UI* |- |``localhost:8081/swagger-ui/#/``
+|===
+
+== Functionality
+
+* Visual representation of the product range
+* Customer's shopping cart
+** selecting a product: add, delete, change a quantity
+** viewing the contents of the cart
+** placing an order
+** storing in the database a shopping cart of a registered customer
+* Market control panel
+** products and categories: add, edit, delete
+** viewing information about placed orders
+** managing the availability of goods in stock
+** transfer orders from the "in progress" state to the "executed" state
+* Secured access to the application
+** registration and authorization of customers
+** restricted access to the control panel
+* Double check of form content: client-side and server-side
+
+== Building Docker images
+
+To build an image run:
+
+* ``docker build -t alukyanets/market-web --build-arg module=market-web .``
+* or ``docker build -t alukyanets/market-rest --build-arg module=market-rest .``
+
+== Legacy branches
+
+``jsp-2021`` - status with web views based on JSP and Apache Tiles, before moving to Thymeleaf
+
+``good-old-2014`` - status for 2014
+
+== Links
+
+For project description in Russian from good old 2014 refer to ``README_RU.md``
diff --git a/sut-market/README_RU.md b/sut-market/README_RU.md
new file mode 100644
index 0000000..78f1a70
--- /dev/null
+++ b/sut-market/README_RU.md
@@ -0,0 +1,316 @@
+[![Codacy Badge](https://api.codacy.com/project/badge/Grade/8c0bd51bdba44e04bd2cbbfd7f643e9f)](https://www.codacy.com/manual/aleksey-lukyanets/market?utm_source=github.com&utm_medium=referral&utm_content=aleksey-lukyanets/market&utm_campaign=Badge_Grade)
+[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/8c0bd51bdba44e04bd2cbbfd7f643e9f)](https://www.codacy.com/manual/aleksey-lukyanets/market?utm_source=github.com&utm_medium=referral&utm_content=aleksey-lukyanets/market&utm_campaign=Badge_Coverage)
+![build](https://github.com/aleksey-lukyanets/market/workflows/build/badge.svg)
+
+Технологии
+
+
+ - сервлет: Spring MVC, JavaServer Pages, Arache Tiles
+ - авторизация пользователей: Spring Security
+ - доступ к данным: Hibernate, Spring Data JPA
+ - веб-служба в стиле REST
+ - тесты: Spring Test, JUnit, Hamcrest, JSONPath;
+ - веб-интерфейс: jQuery, jQuery Validate Plugin, Bootstrap
+ - база данных: PostgreSQL
+ - контейнер сервлетов: Apache Tomcat
+
+
+Функционал магазина
+
+
+ - Наглядное представление ассортимента товаров
+ - Корзина покупателя
+
+ - выбор товаров: добавление, удаление, изменение количества
+ - просмотр содержимого корзины
+ - оформление заказа
+ - хранение корзины зарегистрированного покупателя в базе данных
+
+
+ - Панель управления магазином
+
+ - товары и категории: добавление, редактирование, удаление
+ - просмотр информации о размещённых заказах
+ - управление наличием товаров на складе
+ - перевод заказов из состояния "в исполнении" в состояние "исполнен"
+
+
+ - Безопасный доступ к приложению
+
+ - регистрация и авторизация пользователей
+ - ограничение доступа к панели управления
+
+
+ - Двойная проверка содержимого форм: на стороне клиента и на стороне сервера
+
+
+Оформление заказа
+
+Ниже приведена диаграмма процесса оформления заказа, на которую нанесены
+ элементы данных и доступные покупателю действия.
+
+
+
+
+
+
+
+Веб-служба REST
+
+Помимо гипертекстового интерфейса магазин представляет веб-службу, через которую
+ доступен функционал магазина. Описание веб-службы находится на странице
+ REST API.
+
+Сортировка, фильтрация, разбивка на страницы
+
+Приложение предоставляет возможность просматривать ресурсы удобным пользователю способом.
+Не нарушая принципов стиля REST, критерии передаются в параметрах URI. Например,
+ в панели управления магазином так выглядит запрос отображения таблицы всех имеющихся
+ в наличии товаров, с сортировкой по возрастанию цены и 5 товаров на странице:
+ /admin/storage ? available=true & direct=asc & size=5
+ (без пробелов).
+Операции сортировки, фильтрации и постраничного отображения выделены в отдельную
+ иерархию классов в пакете market.sorting
, которая объединяет
+ методы изменения значений опций, формирования запроса к БД (PageRequest
+ Spring Data) и добавления всех необходимых данных к модели Model
Spring MVC.
+
+Валидация форм
+
+Проверка данных всех форм пользовательского и административного интерфейса выполняется
+ дважды: на стороне пользователя и на стороне сервера.
+
+ - Проверка на стороне пользователя осуществляется с использованием jQuery Validate Plugin,
+ который проверяет данные в момент ввода средствами JavaSript. Для посимвольной проверки строки применены
+ регулярные выражения (regex). Визуализация дополнена классами Bootstrap.
+ - Проверка на стороне сервера выполняется с использованием пакетов
javax.validation
и
+ org.springframework.validation
.
+
+Такой подход к валидации форм делает процесс проверки данных комфортным
+ для пользователя и вместе с тем гарантирует выполнение проверки при отключённом
+ JavaScript в браузере пользователя.
+
+Обработка исключений
+
+В приложении реализована централизованная обработка исключений классом
+ market.controller.SpringExceptionHandler
с аннотацией
+ @ControllerAdvice
, предоставленной Spring.
+Помимо объединения обработчиков в единый класс, такой подход позволяет
+ вынести проверку авторизации клиентов веб-сервиса из реализации контроллеров:
+ права пользователя проверяются перехватчиком
+ market.interceptor.RestUserCheckInterceptor
и в случае неудачной
+ аутентификации исключение RestNotAuthenticatedException
передаётся
+ обработчику в обход контроллеров, с последующим возвратом клиенту HTTP-состояния
+ 401 Unauthorized
(вместо перенаправления на страницу входа, как в случае
+ гипертекстового интерфейса магазина). При удачной аутентификации запрос
+ передаётся соответствующему контроллеру.
+
+Модель базы данных
+
+База данных приложения состоит из 13 связанных таблиц, отображаемых средствами Hibernate в 14 классов.
+
+
+
+
+
+
+
+Слой доступа к данным на первоначальном этапе разработки был представлен классами DAO,
+ а с введением функций разбивки на страницы и сортировки реализован
+ с помощью репозитория Spring Data JPA.
+
+Пользовательские классы Spring
+
+Функционал фреймворков Spring MVC и Spring Security расширен следующими классами:
+
+ UserDetailsServiceImpl
реализует интерфейс UserDetailsService
+ и обеспечивает извлечение профиля пользователя из базы данных;
+ CustomAuthenticationSuccessHandler
реализует интерфейс
+ AuthenticationSuccessHandler
и обрабатывает событие успешной аутентификации пользователя;
+ SpringExceptionHandler
осуществляет централизованную обработку исключений;
+ SessionCartInterceptor
реализует интерфейс HandlerInterceptorAdapter
+ и проверяет до обработки запроса контроллерами, существует ли в модели атрибут корзины покупателя;
+ при отсутствии создатся новая корзина; такое решение позволяет централизовать создание корзины,
+ в том числе для случая, когда браузер пользователя не принимает cookies и поэтому
+ не поддерживает хранение параметров сессии;
+ RestUserCheckInterceptor
реализует интерфейс HandlerInterceptorAdapter
+ и используется для проверки прав пользователя при доступе к веб-службе.
+
+
+Веб-служба REST
+
+REST-интерфейс приложения предоставляет доступ к ресурсам магазина: позволяет
+ регистрировать покупателей, изменять их контактные данные, запрашивать
+ сведения о товарах, добавлять товары в корзину и оформлять заказы.
+Обмен данными между клиентом и веб-службой магазина осуществляется в формате JSON,
+ аутентификация выполняется средствами Basic Authentication.
+В соответствии со стилем REST, взаимодействие между клиентом и сервером лишено
+ сеансового состояния, поэтому веб-служба магазина не предоставляет гостевую
+ корзину: информация о товарах доступна всем клиентам, но заказ может быть
+ собран и оформлен только зарегистрированным пользователем.
+Ниже приводится интерфейс веб-службы и примеры взаимодействия.
+
+Взаимодействие с веб-службой
+
+Доступ к ресурсам магазина можно получить с использованием любого HTTP-клиента,
+ поддерживающего Basic-аутентификацию, например RESTClient для Mozilla Firefox.
+Так обращение к ресурсу http://46.101.111.55:8080/rest/products
+ возвращает список всех товаров:
+[
{
"productId": 1,
"distillery": "Ardbeg",
"name": "Ten",
"price": 4030,
"links": [{
"rel": "self",
"href": "http://46.101.111.55:8080/rest/products/1"}]
},
{
"productId": 2,
"distillery": "Balvenie",
"name": "12 y.o. Doublewood",
"price": 4100,
"links": [{
"rel": "self",
"href": "http://46.101.111.55:8080/rest/products/2" }]
}
...
]
+Сведения об отдельном товаре доступны по его идентификатору.
+ Ресурс /products/2
:
+{
"id": 2,
"distillery": "Balvenie",
"name": "12 y.o. Doublewood",
"price": 4100,
"age": 12,
"volume": 700,
"alcohol": 40,
"description": "Помимо стандартных бочек выдерживается в бочках
из-под хереса. Гармоничный аромат свежего мёда, ванили,
воска, дуба и цветочной пыльцы с сухой горчинкой.",
"inStock": true
}
+
+
+Регистрация покупателя
+
+Регистрация нового покупателя осуществляется отправкой запроса POST /signup
:
+заголовок: Content-Type: application/json; charset=UTF-8
тело: {
"email": "ivan.petrov@yandex.ru",
"name": "Иван Петров",
"password": "Иван Петров",
"phone": "+7 123 456 67 89",
"address": "ул. Итальянская, д. 7"
}
+Ответ приложения при успешном создании аккаунта: 200 Ok
.
+Если покупатель с указанным адресом электронной почты уже существует в магазине,
+ либо если возникли нарушения при валидации переданных данных, клиенту будет возвращено
+ HTTP-состояние 406 Not Acceptable
с соответствующими пояснениями.
+Например, об ошибке в имени "name": "name@#$%^"
+ сервер уведомит ответом:
+{
"fieldErrors":
[{
"field": "name",
"message": "В имени допустимы только буквы, пробел, дефис и апостроф."
}]
}
+Получить или изменить контактные данные можно обращением к ресурсу соответственно
+ /customer/contacts
запросами GET или PUT.
+
+
+Формирование заказа
+
+Зарегистрированный покупатель может добавить товар в корзину запросом PUT /cart
:
+заголовок: Content-Type: application/json; charset=UTF-8
Authorization: Basic aXZhbi5wZXRyb3ZAeWFuZGV4LnJ10nBldHJvdg==
тело: {"productId": 2, "quantity": 1}
+В ответе приложение вернёт изменённую корзину:
+заголовок: Status Code: 200 Ok
Content-Type: application/json; charset=UTF-8
тело: {
"user": "ivan.petrov@yandex.ru",
"items":
[
{
"productId": 2,
"quantity": 1,
"links": [{
"rel": "self",
"href": "http://46.101.111.55:8080/rest/products/2" }]
}
],
"totalItems": 1,
"productsCost": 4100,
"deliveryCost": 400,
"deliveryIncluded": true,
"totalCost": 4500,
"links":
[
{
"rel": "Customer contacts",
"href": "http://46.101.111.55:8080/rest/customer/contacts"
},
{
"rel": "Payment",
"href": "http://46.101.111.55:8080/rest/cart/payment"
}
]
}
+Опция доставки может быть изменена запросом PUT /cart/delivery/{boolean}
.
+
+Для оформления заказа следует отправить номер банковской карты, с которой
+ будет оплачен заказ, запросом POST /cart/payment
:
+заголовок: Content-Type: application/json; charset=UTF-8
Authorization: Basic aXZhbi5wZXRyb3ZAeWFuZGV4LnJ10nBldHJvdg==
тело: {"number": "4444 3333 2222 1111"}
+Приложение вернёт подтверждение об оплате и приёме заказа:
+заголовок: Status Code: 201 Created
Location: http://46.101.111.55:8080/rest/customer/orders/13
Content-Type: application/json; charset=UTF-8
тело: {
"id": 13,
"user": "ivan.petrov@yandex.ru",
"billNumber": 525553712,
"dateCreated": 1397559589798,
"productsCost": 4100,
"deliveryCost": 400,
"deliveryIncluded": true,
"totalCost": 4500,
"payed": true,
"executed": false
}
+Получить перечень всех оставленных покупателем заказов можно обратившись к ресурсу
+ /customer/orders
.
+
+
+Операции с ресурсами
+
+Сведения о товарах
+
+
+
+ ресурс |
+ описание |
+ статусы ответа |
+
+
+
+
+ GET /products |
+ Возвращает перечень всех товаров магазина |
+ 200 |
+
+
+ GET /products/:id |
+ Возвращает товар с указанным id |
+ 200 — товар возвращён в теле ответа,
+ 404 — товара с таким id не существует |
+
+
+
+
+Корзина покупателя (требует авторизации)
+
+
+
+ ресурс |
+ описание |
+ статусы ответа |
+
+
+
+
+ GET /cart |
+ Возвращает корзину покупателя |
+ 200 |
+
+
+ PUT /cart |
+ Добавляет товар в корзину |
+ 200 — товар добавлен, обновлённая корзина находится в теле ответа,
+ 406 — товара с запрошенным id не существует; пояснения в теле ответа |
+
+
+ DELETE /cart |
+ Удаляет из корзины все товары |
+ 200 — корзина очищена, обновлённая корзина находится в теле ответа |
+
+
+ PUT /cart/delivery/:boolean |
+ Включает в заказ доставку |
+ 200 — опция доставки изменена, обновлённая корзина находится в теле ответа |
+
+
+ POST /cart/payment |
+ Подтверждает заказ и оплачивает его картой с указанным номером |
+ 201 — заказ оплачен и принят, ссылка на заказ находится в заголовке, детали заказа — в теле ответа,
+ 406 — некорректный формат номера карты, либо корзина пуста; пояснения в теле ответа |
+
+
+
+
+Регистрация нового покупателя
+
+
+
+ ресурс |
+ описание |
+ статусы ответа |
+
+
+
+
+ POST /signup |
+ Регистрирует нового покупателя |
+ 200 — покупатель зарегистрирован и возвращён в теле ответа,
+ 406 — некорректный формат полученных данных; пояснения в теле ответа |
+
+
+
+
+Профиль покупателя (требует авторизации)
+
+
+
+ ресурс |
+ описание |
+ статусы ответа |
+
+
+
+
+ GET /customer/contacts |
+ Возвращает контактные данные покупателя |
+ 200 — контактные данные возвращены в теле ответа |
+
+
+ PUT /customer/contacts |
+ Изменяет контактные данные покупателя |
+ 200 — данные изменены, обновлённые возвращены в теле ответа,
+ 406 — некорректный формат полученных данных; пояснения в теле ответа |
+
+
+ GET /customer/orders |
+ Возвращает перечень заказов покупателя |
+ 200 — перечень заказов возвращён в теле ответа |
+
+
+ GET /customer/orders/:id |
+ Возвращает заказ с указанным id |
+ 200 — заказ возвращён в теле ответа,
+ 404 — заказ с таким id у авторизованного пользователя не существует |
+
+
+
diff --git a/sut-market/database-model.mwb b/sut-market/database-model.mwb
new file mode 100644
index 0000000..0c06621
Binary files /dev/null and b/sut-market/database-model.mwb differ
diff --git a/sut-market/docker-compose-jenkins-workspace.sh b/sut-market/docker-compose-jenkins-workspace.sh
new file mode 100644
index 0000000..40cfdec
--- /dev/null
+++ b/sut-market/docker-compose-jenkins-workspace.sh
@@ -0,0 +1,19 @@
+# La forma natural de hacer el bind de los ficheros sql de setup de la bd
+# seria usar en el docker compose la variable de entorno WORKSPACE que pone jenkins.
+# Esto asigna el path corecto a los bindings (si se examina el container con inspect)
+# pero luego en tiempo de ejecucion no se localizan.
+# Puede ser debido a que la localizacion del workspace ya es un bind en el slave
+# y que esto causa un doble bind?
+
+# La solucion sera este shell que modifica el docker compose:
+# - Obtiene WORKSPACE_FOLDER_VAR como el ultimo componente del workspace
+# - En el docker compose se hara el bind con la ruta absoluta al workspace general del slave,
+# anyadiendo WORKSPACE_FOLDER_VAR y luego la ruta de los ficheros.
+
+# WORKSPACE="/ab/cd/efxxxxxxxxxx"
+
+echo "Replacing workspace location in docker compose file docker-compose-jenkins.yaml"
+WORKSPACE_FOLDER_VAR=`echo $WORKSPACE | awk -F/ '{print $NF}'`
+echo "Workspace folder is $WORKSPACE_FOLDER_VAR"
+
+sed -i "s/WORKSPACE_FOLDER/${WORKSPACE_FOLDER_VAR}/" docker-compose-jenkins.yaml
diff --git a/sut-market/docker-compose-jenkins.yaml b/sut-market/docker-compose-jenkins.yaml
new file mode 100644
index 0000000..86ec92a
--- /dev/null
+++ b/sut-market/docker-compose-jenkins.yaml
@@ -0,0 +1,40 @@
+version: '3'
+services:
+ db:
+ container_name: swagger-market-db
+ image: postgres:9.6
+ #ports:
+ # - 8384:5432
+ environment:
+ - POSTGRES_DB=market
+ - POSTGRES_USER=market
+ - POSTGRES_PASSWORD=market
+ - WORKSPACE
+ volumes:
+ # No se puede usar la variable de entorno del workspace, ver docker-compose-jenkins-workspace.sh
+ #- ${WORKSPACE}/swagger-market-fork/market-core/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/1_schema.sql
+ #- ${WORKSPACE}/swagger-market-fork/market-core/src/main/resources/data.sql:/docker-entrypoint-initdb.d/2_data.sql
+ - /t2/cdat/jenkins-slave-xa/workspace/WORKSPACE_FOLDER/swagger-market-fork/market-core/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/1_schema.sql
+ - /t2/cdat/jenkins-slave-xa/workspace/WORKSPACE_FOLDER/swagger-market-fork/market-core/src/main/resources/data.sql:/docker-entrypoint-initdb.d/2_data.sql
+ - postgres-data:/var/lib/postgresql/data
+ networks:
+ - net-swagger-test
+ market-rest:
+ container_name: swagger-market-rest
+ image: swagger-market-rest
+ depends_on:
+ - db
+ ports:
+ - 8383:8081
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - DB_HOST=db
+ networks:
+ - net-swagger-test
+volumes:
+ postgres-data:
+ driver: local
+networks:
+ # all containers are in this network, same as the dedicated jenkins agent
+ net-swagger-test:
+ external: true
diff --git a/sut-market/docker-compose-rest.yaml b/sut-market/docker-compose-rest.yaml
new file mode 100644
index 0000000..0403192
--- /dev/null
+++ b/sut-market/docker-compose-rest.yaml
@@ -0,0 +1,28 @@
+version: '3'
+services:
+ db:
+ container_name: db
+ image: postgres:9.6
+ ports:
+ - 8084:5432
+ environment:
+ - POSTGRES_DB=market
+ - POSTGRES_USER=market
+ - POSTGRES_PASSWORD=market
+ volumes:
+ - ./market-core/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/1_schema.sql
+ - ./market-core/src/main/resources/data.sql:/docker-entrypoint-initdb.d/2_data.sql
+ - postgres-data:/var/lib/postgresql/data
+ market-rest:
+ container_name: market-rest
+ image: alukyanets/market-rest
+ depends_on:
+ - db
+ ports:
+ - 8083:8081
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - DB_HOST=db
+volumes:
+ postgres-data:
+ driver: local
\ No newline at end of file
diff --git a/sut-market/docker-compose-web.yaml b/sut-market/docker-compose-web.yaml
new file mode 100644
index 0000000..181e594
--- /dev/null
+++ b/sut-market/docker-compose-web.yaml
@@ -0,0 +1,28 @@
+version: '3'
+services:
+ db:
+ container_name: db
+ image: postgres:9.6
+ ports:
+ - 8084:5432
+ environment:
+ - POSTGRES_DB=market
+ - POSTGRES_USER=market
+ - POSTGRES_PASSWORD=market
+ volumes:
+ - ./market-core/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/1_schema.sql
+ - ./market-core/src/main/resources/data.sql:/docker-entrypoint-initdb.d/2_data.sql
+ - postgres-data:/var/lib/postgresql/data
+ market-web:
+ container_name: market-web
+ image: alukyanets/market-web
+ depends_on:
+ - db
+ ports:
+ - 8082:8080
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - DB_HOST=db
+volumes:
+ postgres-data:
+ driver: local
\ No newline at end of file
diff --git a/sut-market/docker-compose.yaml b/sut-market/docker-compose.yaml
new file mode 100644
index 0000000..4e9a491
--- /dev/null
+++ b/sut-market/docker-compose.yaml
@@ -0,0 +1,38 @@
+version: '3'
+services:
+ db:
+ container_name: db
+ image: postgres:9.6
+ ports:
+ - 8084:5432
+ environment:
+ - POSTGRES_DB=market
+ - POSTGRES_USER=market
+ - POSTGRES_PASSWORD=market
+ volumes:
+ - ./market-core/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/1_schema.sql
+ - ./market-core/src/main/resources/data.sql:/docker-entrypoint-initdb.d/2_data.sql
+ - postgres-data:/var/lib/postgresql/data
+ market-web:
+ container_name: market-web
+ image: market-web
+ depends_on:
+ - db
+ ports:
+ - 8082:8080
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - DB_HOST=db
+ market-rest:
+ container_name: market-rest
+ image: market-rest
+ depends_on:
+ - db
+ ports:
+ - 8083:8081
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - DB_HOST=db
+volumes:
+ postgres-data:
+ driver: local
\ No newline at end of file
diff --git a/sut-market/market-core/.gitignore b/sut-market/market-core/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/sut-market/market-core/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/sut-market/market-core/pom.xml b/sut-market/market-core/pom.xml
new file mode 100644
index 0000000..2c7b218
--- /dev/null
+++ b/sut-market/market-core/pom.xml
@@ -0,0 +1,111 @@
+
+
+
+ market
+ net.lukyanets
+ 0.1.2
+ ..
+
+ 4.0.0
+
+ market-core
+ 0.1.2
+
+ market-core
+
+
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
+ true
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+
+ prepare-agent
+
+
+
+ report
+ prepare-package
+
+ report
+
+
+
+
+
+ **/*market/dto/**
+ **/*market/exception/**
+
+
+
+
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.16.1
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.postgresql
+ postgresql
+ 42.3.2
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+
+
+
+ org.springframework.hateoas
+ spring-hateoas
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
\ No newline at end of file
diff --git a/sut-market/market-core/src/main/java/market/DataConfig.java b/sut-market/market-core/src/main/java/market/DataConfig.java
new file mode 100644
index 0000000..41cac1b
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/DataConfig.java
@@ -0,0 +1,65 @@
+package market;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+@Configuration
+@EntityScan(basePackages="market.domain")
+@EnableTransactionManagement
+@ComponentScan(basePackages = {
+ "market.properties",
+ "market.service"
+})
+public class DataConfig {
+
+ @Bean
+ @Primary
+ @ConfigurationProperties("spring.datasource")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ @Bean // with Spring Boot this is actually not necessary but leaving it to note things explicitly
+ @Primary
+ @ConfigurationProperties("spring.datasource.configuration")
+ public HikariDataSource dataSource() {
+ DataSourceProperties properties = dataSourceProperties();
+ return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
+ LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); // todo: EntityManagerFactoryBuilder ?
+ em.setDataSource(dataSource);
+ em.setPackagesToScan("market.domain");
+ em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+ return em;
+ }
+
+ @Bean
+ public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
+ JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(emf);
+ return transactionManager;
+ }
+
+ @Bean
+ public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
+ return new PersistenceExceptionTranslationPostProcessor();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/FixturesFactory.java b/sut-market/market-core/src/main/java/market/FixturesFactory.java
new file mode 100644
index 0000000..d63f0f4
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/FixturesFactory.java
@@ -0,0 +1,116 @@
+package market;
+
+import market.domain.*;
+
+import java.util.Date;
+
+public class FixturesFactory {
+ private static final String DISTILLERY_TITLE = "distillery_title";
+ private static final String DISTILLERY_DESCRIPTION = "distillery_description";
+
+ private static final String REGION_NAME = "region name";
+ private static final String REGION_SUBTITLE = "region subtitle";
+ private static final String REGION_DESCRIPTION = "region description";
+ private static final String REGION_COLOR = "#ffffff";
+
+ private static final double PRODUCT_PRICE = 100.0;
+ private static final String PRODUCT_NAME = "product_name";
+ private static final int PRODUCT_AGE = 10;
+ private static final float PRODUCT_ALCOHOL = 40;
+ private static final int PRODUCT_VOLUME = 700;
+ private static final String PRODUCT_DESCRIPTION = "product_description";
+ private static final boolean PRODUCT_AVAILABLE = true;
+
+ private static final String ACCOUNT_EMAIL = "email@domain.com";
+ private static final String ACCOUNT_PASSWORD = "password";
+ private static final String ACCOUNT_NAME = "Name";
+ private static final boolean ACCOUNT_ACTIVE = true;
+
+ private static final String CONTACTS_PHONE = "+97211234567";
+ private static final String CONTACTS_ADDRESS = "some_address";
+
+ private static final int ORDERED_PRODUCT_QUANTITY = 3;
+
+ private static final String BILL_CARD_NUMBER = "1111222233334444";
+
+ private static long regionId = 123L;
+ private static long distilleryId = 234L;
+ private static long productId = 10L;
+ private static long accountId = 50L;
+ private static long orderId = 3000L;
+ private static int billId = 400;
+
+ public static Region.Builder region() {
+ return new Region.Builder()
+ .setId(++regionId)
+ .setName(REGION_NAME + regionId)
+ .setSubtitle(REGION_SUBTITLE)
+ .setDescription(REGION_DESCRIPTION)
+ .setColor(REGION_COLOR);
+ }
+
+ public static Distillery.Builder distillery(Region region) {
+ return new Distillery.Builder()
+ .setId(++distilleryId)
+ .setRegion(region)
+ .setTitle(DISTILLERY_TITLE + distilleryId)
+ .setDescription(DISTILLERY_DESCRIPTION);
+ }
+
+ public static Product.Builder product(Distillery distillery) {
+ return new Product.Builder()
+ .setId(++productId)
+ .setDistillery(distillery)
+ .setName(PRODUCT_NAME + productId)
+ .setAge(PRODUCT_AGE)
+ .setAlcohol(PRODUCT_ALCOHOL)
+ .setPrice(PRODUCT_PRICE)
+ .setVolume(PRODUCT_VOLUME)
+ .setDescription(PRODUCT_DESCRIPTION)
+ .setAvailable(PRODUCT_AVAILABLE);
+ }
+
+ public static UserAccount.Builder account(Cart cart) {
+ return account()
+ .setCart(cart);
+ }
+
+ public static UserAccount.Builder account() {
+ return new UserAccount.Builder()
+ .setId(++accountId)
+ .setEmail(ACCOUNT_EMAIL)
+ .setPassword(ACCOUNT_PASSWORD)
+ .setName(ACCOUNT_NAME)
+ .setActive(ACCOUNT_ACTIVE);
+ }
+
+ public static Contacts.Builder contacts() {
+ return new Contacts.Builder()
+ .setPhone(CONTACTS_PHONE)
+ .setAddress(CONTACTS_ADDRESS);
+ }
+
+ public static Order.Builder order(UserAccount userAccount) {
+ return new Order.Builder()
+ .setId(++orderId)
+ .setUserAccount(userAccount)
+ .setDateCreated(new Date());
+ }
+
+ public static OrderedProduct.Builder orderedProduct(Order order, Product product) {
+ return new OrderedProduct.Builder()
+ .setProduct(product)
+ .setOrder(order)
+ .setQuantity(ORDERED_PRODUCT_QUANTITY);
+ }
+
+ public static Bill.Builder bill(Order order) {
+ return new Bill.Builder()
+ .setOrder(order)
+ .setNumber(++billId)
+ .setTotalCost(order.getProductsCost() + order.getDeliveryCost())
+ .setPayed(true)
+ .setDateCreated(new Date())
+ .setCcNumber(BILL_CARD_NUMBER);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/SecurityConfigBase.java b/sut-market/market-core/src/main/java/market/SecurityConfigBase.java
new file mode 100644
index 0000000..33f2488
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/SecurityConfigBase.java
@@ -0,0 +1,63 @@
+package market;
+
+import market.security.AuthenticationService;
+import market.security.UserDetailsServiceImpl;
+import market.service.UserAccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@EnableWebSecurity
+@ComponentScan(basePackages = {"market.security"})
+@EnableGlobalMethodSecurity(securedEnabled = true)
+public class SecurityConfigBase extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private DaoAuthenticationProvider daoAuthenticationProvider;
+
+ @Bean
+ public AuthenticationService authenticationService(AuthenticationManager authenticationManager) {
+ return new AuthenticationService(authenticationManager);
+ }
+
+ @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+ protected void configure(AuthenticationManagerBuilder auth) {
+ auth.authenticationProvider(daoAuthenticationProvider);
+ }
+
+ @Bean
+ public DaoAuthenticationProvider daoAuthenticationProvider(
+ UserDetailsServiceImpl customUserDetailsService, PasswordEncoder passwordEncoder)
+ {
+ DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+ provider.setUserDetailsService(customUserDetailsService);
+ provider.setPasswordEncoder(passwordEncoder);
+ return provider;
+ }
+
+ @Bean
+ public UserDetailsServiceImpl customUserDetailsService(UserAccountService userAccountService) {
+ return new UserDetailsServiceImpl(userAccountService);
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/CartDAO.java b/sut-market/market-core/src/main/java/market/dao/CartDAO.java
new file mode 100644
index 0000000..dd8e9e9
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/CartDAO.java
@@ -0,0 +1,9 @@
+package market.dao;
+
+import market.domain.Cart;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+public interface CartDAO extends CrudRepository, JpaRepository {
+
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/ContactsDAO.java b/sut-market/market-core/src/main/java/market/dao/ContactsDAO.java
new file mode 100644
index 0000000..7397097
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/ContactsDAO.java
@@ -0,0 +1,11 @@
+package market.dao;
+
+import market.domain.Contacts;
+import market.domain.UserAccount;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+public interface ContactsDAO extends CrudRepository, JpaRepository {
+
+ Contacts findByUserAccount(UserAccount userAccount);
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/DistilleryDAO.java b/sut-market/market-core/src/main/java/market/dao/DistilleryDAO.java
new file mode 100644
index 0000000..b430497
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/DistilleryDAO.java
@@ -0,0 +1,15 @@
+package market.dao;
+
+import market.domain.Distillery;
+import market.domain.Region;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.List;
+
+public interface DistilleryDAO extends CrudRepository, JpaRepository {
+
+ List findByRegionOrderByTitleAsc(Region region);
+
+ Distillery findByTitle(String title);
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/OrderDAO.java b/sut-market/market-core/src/main/java/market/dao/OrderDAO.java
new file mode 100644
index 0000000..95f059e
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/OrderDAO.java
@@ -0,0 +1,22 @@
+package market.dao;
+
+import market.domain.Order;
+import market.domain.UserAccount;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Date;
+import java.util.List;
+
+public interface OrderDAO extends CrudRepository, JpaRepository {
+
+ List findByUserAccountOrderByDateCreatedDesc(UserAccount userAccount);
+
+ Page findByExecuted(boolean stored, Pageable pageable);
+
+ Page findByDateCreatedGreaterThan(Date created, Pageable pageable);
+
+ Page findByExecutedAndDateCreatedGreaterThan(boolean executed, Date created, Pageable pageable);
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/OrderedProductDAO.java b/sut-market/market-core/src/main/java/market/dao/OrderedProductDAO.java
new file mode 100644
index 0000000..b484e49
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/OrderedProductDAO.java
@@ -0,0 +1,9 @@
+package market.dao;
+
+import market.domain.OrderedProduct;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+public interface OrderedProductDAO extends CrudRepository, JpaRepository {
+
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/ProductDAO.java b/sut-market/market-core/src/main/java/market/dao/ProductDAO.java
new file mode 100644
index 0000000..e4276d2
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/ProductDAO.java
@@ -0,0 +1,21 @@
+package market.dao;
+
+import market.domain.Distillery;
+import market.domain.Product;
+import market.domain.Region;
+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.CrudRepository;
+import org.springframework.data.repository.query.Param;
+
+public interface ProductDAO extends CrudRepository, JpaRepository {
+
+ Page findByDistilleryOrderByName(Distillery distillery, Pageable request);
+
+ @Query(value = "SELECT p FROM Product p WHERE p.distillery IN (SELECT d FROM Distillery d WHERE d.region = :region) order by p.name")
+ Page findByRegionOrderByName(@Param("region") Region region, Pageable request);
+
+ Page findByAvailableOrderByName(boolean available, Pageable request);
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/RegionDAO.java b/sut-market/market-core/src/main/java/market/dao/RegionDAO.java
new file mode 100644
index 0000000..5b89859
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/RegionDAO.java
@@ -0,0 +1,13 @@
+package market.dao;
+
+import market.domain.Region;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface RegionDAO extends CrudRepository, JpaRepository {
+
+ Optional findByName(String regionName);
+
+}
diff --git a/sut-market/market-core/src/main/java/market/dao/UserAccountDAO.java b/sut-market/market-core/src/main/java/market/dao/UserAccountDAO.java
new file mode 100644
index 0000000..9ca336b
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dao/UserAccountDAO.java
@@ -0,0 +1,10 @@
+package market.dao;
+
+import market.domain.UserAccount;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
+
+public interface UserAccountDAO extends CrudRepository, JpaRepository {
+
+ UserAccount findByEmail(String email);
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Bill.java b/sut-market/market-core/src/main/java/market/domain/Bill.java
new file mode 100644
index 0000000..a65ff6c
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Bill.java
@@ -0,0 +1,171 @@
+package market.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.OneToOne;
+import javax.persistence.PrimaryKeyJoinColumn;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import static javax.persistence.TemporalType.TIMESTAMP;
+
+/**
+ * Bill of the {@link Order}.
+ */
+@Entity
+@Table(name = "bill")
+public class Bill implements Serializable {
+ private static final long serialVersionUID = 3689283961628876802L;
+
+ @Id
+ @Column(name = "id", unique = true, nullable = false)
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ // // Test: anyadido para evitar recursividad en json con order
+ @JsonBackReference
+ @OneToOne
+ @PrimaryKeyJoinColumn
+ private Order order;
+
+ @Column(name = "number", nullable = false)
+ private int number;
+
+ @Column(name = "total_cost", nullable = false)
+ private double totalCost;
+
+ @Column(name = "payed", nullable = false)
+ private boolean payed = false;
+
+ @Column(name = "cc_number", nullable = false)
+ @NotEmpty
+ @Pattern(regexp = "\\b(?:\\d[ -]*?){13,16}\\b")
+ private String ccNumber;
+
+ @Column(name = "date_created", nullable = false)
+ @Temporal(TIMESTAMP)
+ private Date dateCreated;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getCcNumber() {
+ return ccNumber;
+ }
+
+ public void setCcNumber(String ccNumber) {
+ this.ccNumber = ccNumber;
+ }
+
+ public Order getOrder() {
+ return order;
+ }
+
+ public void setOrder(Order order) {
+ this.order = order;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public void setNumber(int number) {
+ this.number = number;
+ }
+
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public void setTotalCost(double totalCost) {
+ this.totalCost = totalCost;
+ }
+
+ public boolean isPayed() {
+ return payed;
+ }
+
+ public void setPayed(boolean payed) {
+ this.payed = payed;
+ }
+
+ public static class Builder {
+ private Long id;
+ private Order order;
+ private int number;
+ private double totalCost;
+ private boolean payed = false;
+ private String ccNumber;
+ private Date dateCreated;
+
+ public Bill build() {
+ Bill bill = new Bill();
+ bill.id = id;
+ bill.order = order;
+ bill.number = number;
+ bill.totalCost = totalCost;
+ bill.payed = payed;
+ bill.ccNumber = ccNumber;
+ bill.dateCreated = dateCreated;
+ return bill;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setOrder(Order order) {
+ this.order = order;
+ return this;
+ }
+
+ public Builder setNumber(int number) {
+ this.number = number;
+ return this;
+ }
+
+ public Builder setTotalCost(double totalCost) {
+ this.totalCost = totalCost;
+ return this;
+ }
+
+ public Builder setPayed(boolean payed) {
+ this.payed = payed;
+ return this;
+ }
+
+ public Builder setCcNumber(String ccNumber) {
+ this.ccNumber = ccNumber;
+ return this;
+ }
+
+ public Builder setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Cart.java b/sut-market/market-core/src/main/java/market/domain/Cart.java
new file mode 100644
index 0000000..e81c9da
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Cart.java
@@ -0,0 +1,194 @@
+package market.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Cart of the {@link UserAccount}.
+ */
+
+/*
+ * Tests: incluido orden de columnas para serializar en Json
+ */
+@Entity
+@Table(name = "cart")
+@JsonPropertyOrder({"id", "userAccount", "cartItems", "deliveryIncluded", "itemsCost", "itemsCount", "empty"})
+public class Cart implements Serializable {
+ private static final long serialVersionUID = -6884843696895527904L;
+
+ @Id
+ @Column(name = "id", unique = true, nullable = false)
+ @GeneratedValue(generator = "gen")
+ @GenericGenerator(name = "gen", strategy = "foreign", parameters = @Parameter(name = "property", value = "userAccount"))
+ private long id;
+
+ // Test: anyadido para evitar recursividad en json con UserAccount
+ @JsonManagedReference
+ @OneToOne
+ @PrimaryKeyJoinColumn
+ private UserAccount userAccount;
+
+ @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true,
+ targetEntity = CartItem.class, mappedBy = "cart")
+ private List cartItems = new ArrayList<>(0);
+
+ @Column(name = "delivery_included", nullable = false)
+ private boolean deliveryIncluded = true;
+
+ public Cart() {
+ this(null);
+ }
+
+ public Cart(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ }
+
+ public boolean isEmpty() {
+ return cartItems.isEmpty();
+ }
+
+ public CartItem update(Product product, int newQuantity) {
+ if (product == null)
+ return null;
+
+ CartItem updatedItem = null;
+ if (newQuantity > 0) {
+ CartItem existingItem = findItem(product.getId());
+ if (existingItem == null) {
+ CartItem newItem = new CartItem(this, product, newQuantity);
+ cartItems.add(newItem);
+ updatedItem = newItem;
+ } else {
+ existingItem.setQuantity(newQuantity);
+ updatedItem = existingItem;
+ }
+ } else {
+ removeItem(product.getId());
+ }
+ return updatedItem;
+ }
+
+ private void removeItem(long productId) {
+ cartItems.removeIf(item -> item.getProduct().getId() == productId);
+ }
+
+ private CartItem findItem(long productId) {
+ for (CartItem existingItem : cartItems) {
+ if (existingItem.getProduct().getId() == productId)
+ return existingItem;
+ }
+ return null;
+ }
+
+ private double calculateItemsCost() {
+ return cartItems.stream()
+ .mapToDouble(CartItem::calculateCost)
+ .sum();
+ }
+
+ public void clear() {
+ cartItems.clear();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public UserAccount getUserAccount() {
+ return userAccount;
+ }
+
+ public void setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ }
+
+ public List getCartItems() {
+ return Collections.unmodifiableList(cartItems);
+ }
+
+ public boolean isDeliveryIncluded() {
+ return deliveryIncluded;
+ }
+
+ public void setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ }
+
+ public int getItemsCount() {
+ return cartItems.size();
+ }
+
+ public double getItemsCost() {
+ return calculateItemsCost();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Cart cart = (Cart) o;
+ return deliveryIncluded == cart.deliveryIncluded &&
+ Objects.equals(id, cart.id) &&
+ Objects.equals(cartItems, cart.cartItems);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, cartItems, deliveryIncluded);
+ }
+
+ public static class Builder {
+ private long id;
+ private UserAccount userAccount;
+ private List cartItems = new ArrayList<>(0);
+ private boolean deliveryIncluded = true;
+
+ public Builder() {
+ }
+
+ public Builder(Cart cart) {
+ id = cart.id;
+ userAccount = cart.userAccount;
+ cartItems = cart.cartItems;
+ deliveryIncluded = cart.deliveryIncluded;
+ }
+
+ public Cart build() {
+ Cart cart = new Cart();
+ cart.id = id;
+ cart.userAccount = userAccount;
+ cart.cartItems = cartItems;
+ cart.deliveryIncluded = deliveryIncluded;
+ return cart;
+ }
+
+ public Builder setId(long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ return this;
+ }
+
+ public Builder setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/CartItem.java b/sut-market/market-core/src/main/java/market/domain/CartItem.java
new file mode 100644
index 0000000..2c4b2cd
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/CartItem.java
@@ -0,0 +1,82 @@
+package market.domain;
+
+import javax.persistence.Column;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.MapsId;
+import javax.persistence.Table;
+import java.io.Serializable;
+import java.util.Objects;
+
+@Entity
+@Table(name = "cart_item")
+public class CartItem implements Serializable {
+ private static final long serialVersionUID = -3995571478236070123L;
+
+ @EmbeddedId
+ private CartItemId pk = new CartItemId();
+
+ @MapsId("cartId")
+ @JoinColumn(name = "cart_id", referencedColumnName = "id")
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ private Cart cart;
+
+ @MapsId("productId")
+ @JoinColumn(name = "product_id", referencedColumnName = "id")
+ @ManyToOne(optional = false, fetch = FetchType.EAGER)
+ private Product product;
+
+ @Column(name = "quantity")
+ private int quantity;
+
+ public CartItem() {
+ }
+
+ public CartItem(Cart cart, Product product, int quantity) {
+ this.pk = new CartItemId(cart.getId(), product.getId());
+ this.cart = cart;
+ this.product = product;
+ this.quantity = quantity;
+ }
+
+ public double calculateCost() {
+ return quantity * product.getPrice();
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public Product getProduct() {
+ return product;
+ }
+
+ public void setProduct(Product product) {
+ this.product = product;
+ pk.setProduct(product.getId());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CartItem that = (CartItem) o;
+ return Objects.equals(pk, that.pk);
+ }
+
+ @Override
+ public int hashCode() {
+ return (pk != null ? pk.hashCode() : 0);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/CartItemId.java b/sut-market/market-core/src/main/java/market/domain/CartItemId.java
new file mode 100644
index 0000000..daae66f
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/CartItemId.java
@@ -0,0 +1,54 @@
+package market.domain;
+
+import javax.persistence.Embeddable;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Primary key of the {@link CartItem}.
+ */
+@Embeddable
+public class CartItemId implements Serializable {
+ private static final long serialVersionUID = -1255025293895062037L;
+
+ private long cartId;
+ private long productId;
+
+ public CartItemId() {
+ }
+
+ public CartItemId(long cartId, long productId) {
+ this.cartId = cartId;
+ this.productId = productId;
+ }
+
+ public long getCart() {
+ return cartId;
+ }
+
+ public void setCart(long orderId) {
+ this.cartId = orderId;
+ }
+
+ public long getProduct() {
+ return productId;
+ }
+
+ public void setProduct(long productId) {
+ this.productId = productId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CartItemId that = (CartItemId) o;
+ return cartId == that.cartId &&
+ productId == that.productId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cartId, productId);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Contacts.java b/sut-market/market-core/src/main/java/market/domain/Contacts.java
new file mode 100644
index 0000000..e6a8977
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Contacts.java
@@ -0,0 +1,160 @@
+package market.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Contacts of the {@link UserAccount}.
+ */
+@Entity
+@Table(name = "contacts")
+public class Contacts implements Serializable {
+ private static final long serialVersionUID = 582080671801480110L;
+
+ @Id
+ @Column(name = "id", unique = true, nullable = false)
+ @GeneratedValue(generator = "gen")
+ @GenericGenerator(name = "gen", strategy = "foreign", parameters = @Parameter(name = "property", value = "userAccount"))
+ private Long id;
+
+ // Test: anyadido para evitar recursividad en json con UserAccount
+ @JsonManagedReference
+ @OneToOne
+ @PrimaryKeyJoinColumn
+ private UserAccount userAccount; // todo: change to 'String userLogin'
+
+ @Column(name = "phone", nullable = false)
+ private String phone;
+
+ @Column(name = "address", nullable = false)
+ private String address;
+
+ @Column(name = "city_region", nullable = false)
+ private String cityAndRegion;
+
+ public Contacts() {
+ }
+
+ public Contacts(UserAccount userAccount, String phone, String address/*, String cityAndRegion*/) {
+ this.userAccount = userAccount;
+ this.phone = phone;
+ this.address = address;
+ this.cityAndRegion = "13";
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public UserAccount getUserAccount() {
+ return userAccount;
+ }
+
+ public void setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getCityAndRegion() {
+ return cityAndRegion;
+ }
+
+ public void setCityAndRegion(String cityAndRegion) {
+ this.cityAndRegion = cityAndRegion;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Contacts contacts = (Contacts) o;
+ return Objects.equals(id, contacts.id) &&
+ Objects.equals(phone, contacts.phone) &&
+ Objects.equals(address, contacts.address) &&
+ Objects.equals(cityAndRegion, contacts.cityAndRegion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, phone, address, cityAndRegion);
+ }
+
+ public static class Builder {
+ private Long id;
+ private UserAccount userAccount;
+ private String phone;
+ private String address;
+ private String cityAndRegion;
+
+ public Builder() {
+ }
+
+ public Builder(Contacts contacts) {
+ id = contacts.id;
+ userAccount = contacts.userAccount;
+ phone = contacts.phone;
+ address = contacts.address;
+ cityAndRegion = contacts.cityAndRegion;
+ }
+
+ public Contacts build() {
+ Contacts contacts = new Contacts();
+ contacts.id = id;
+ contacts.userAccount = userAccount;
+ contacts.phone = phone;
+ contacts.address = address;
+ contacts.cityAndRegion = cityAndRegion;
+ return contacts;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ return this;
+ }
+
+ public Builder setPhone(String phone) {
+ this.phone = phone;
+ return this;
+ }
+
+ public Builder setAddress(String address) {
+ this.address = address;
+ return this;
+ }
+
+ public Builder setCityAndRegion(String cityAndRegion) {
+ this.cityAndRegion = cityAndRegion;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Distillery.java b/sut-market/market-core/src/main/java/market/domain/Distillery.java
new file mode 100644
index 0000000..eeba07d
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Distillery.java
@@ -0,0 +1,132 @@
+package market.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+import java.util.Objects;
+
+@Entity
+@Table(name = "distillery")
+public class Distillery implements Serializable {
+ private static final long serialVersionUID = -1491932412037172392L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ private Long id;
+
+ @Column(name = "title", nullable = false)
+ @NotEmpty
+ @Pattern(regexp = "^[^#$%^*()']*$")
+ private String title;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "region_id", nullable = false)
+ private Region region;
+
+ @Column(name = "description")
+ private String description;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Region getRegion() {
+ return region;
+ }
+
+ public void setRegion(Region region) {
+ this.region = region;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Distillery that = (Distillery) o;
+ return Objects.equals(id, that.id) &&
+ Objects.equals(title, that.title) &&
+ Objects.equals(region, that.region) &&
+ Objects.equals(description, that.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, title, region, description);
+ }
+
+ public static class Builder {
+ private Long id;
+ private String title;
+ private Region region;
+ private String description;
+
+ public Builder() {
+ }
+
+ public Builder(Distillery source) {
+ id = source.id;
+ title = source.title;
+ region = source.region;
+ description = source.description;
+ }
+
+ public Distillery build() {
+ Distillery distillery = new Distillery();
+ distillery.id = id;
+ distillery.title = title;
+ distillery.region = region;
+ distillery.description = description;
+ return distillery;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Builder setRegion(Region region) {
+ this.region = region;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Order.java b/sut-market/market-core/src/main/java/market/domain/Order.java
new file mode 100644
index 0000000..c6750c6
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Order.java
@@ -0,0 +1,229 @@
+package market.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import static javax.persistence.TemporalType.TIMESTAMP;
+
+/**
+ * Order of the {@link UserAccount}.
+ */
+
+/*
+ * Tests: incluido orden de columnas para serializar en Json
+ */
+@Entity
+@Table(name = "customer_order")
+@JsonPropertyOrder({"id", "userAccount", "orderedProducts", "bill", "productsCost", "dateCreated", "deliveryCost", "deliveryIncluded", "executed"})
+public class Order implements Serializable {
+ private static final long serialVersionUID = -8328584058042877489L;
+
+ @Id
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ @GeneratedValue(generator = "gen")
+ @GenericGenerator(name = "gen", strategy = "foreign", parameters = @Parameter(name = "property", value = "bill"))
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "user_account_id")
+ private UserAccount userAccount;
+
+ @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
+ targetEntity = OrderedProduct.class, mappedBy = "order")
+// @OneToMany(fetch = FetchType.LAZY, mappedBy ="pk.order", cascade =
+// {CascadeType.PERSIST, CascadeType.MERGE})
+// @Cascade({CascadeType.SAVE_UPDATE, CascadeType.DELETE_ORPHAN})
+ private Set orderedProducts = new HashSet<>(0);
+
+ @OneToOne(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
+ private Bill bill;
+
+ @Column(name = "products_cost", nullable = false)
+ private double productsCost;
+
+ @Column(name = "date_created", nullable = false)
+ @Temporal(TIMESTAMP)
+ private Date dateCreated;
+
+ @Column(name = "delivery_cost")
+ private int deliveryCost;
+
+ @Column(name = "delivery_included", nullable = false)
+ private boolean deliveryIncluded;
+
+ @Column(name = "executed", nullable = false)
+ private boolean executed;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public double getProductsCost() {
+ return productsCost;
+ }
+
+ public void setProductsCost(double amount) {
+ this.productsCost = amount;
+ }
+
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(Date name) {
+ this.dateCreated = name;
+ }
+
+ public UserAccount getUserAccount() {
+ return userAccount;
+ }
+
+ public void setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ }
+
+ public Set getOrderedProducts() {
+ return orderedProducts;
+ }
+
+ public void setOrderedProducts(Set products) {
+ this.orderedProducts = products;
+ }
+
+ public int getDeliveryCost() {
+ return deliveryCost;
+ }
+
+ public void setDeliveryCost(int deliveryСost) {
+ this.deliveryCost = deliveryСost;
+ }
+
+ public boolean isDeliveryIncluded() {
+ return deliveryIncluded;
+ }
+
+ public void setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ }
+
+ public Bill getBill() {
+ return bill;
+ }
+
+ public void setBill(Bill bill) {
+ this.bill = bill;
+ }
+
+ public boolean isExecuted() {
+ return executed;
+ }
+
+ public void setExecuted(boolean executed) {
+ this.executed = executed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Order order = (Order) o;
+ return Double.compare(order.productsCost, productsCost) == 0 &&
+ deliveryCost == order.deliveryCost &&
+ deliveryIncluded == order.deliveryIncluded &&
+ executed == order.executed &&
+ Objects.equals(id, order.id) &&
+ Objects.equals(userAccount, order.userAccount) &&
+ Objects.equals(orderedProducts, order.orderedProducts) &&
+ Objects.equals(bill, order.bill) &&
+ Objects.equals(dateCreated, order.dateCreated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, userAccount, orderedProducts, bill, productsCost, dateCreated, deliveryCost, deliveryIncluded, executed);
+ }
+
+ public static class Builder {
+ private Long id;
+ private UserAccount userAccount;
+ private Set orderedProducts = new HashSet<>();
+ private Bill bill;
+ private double productsCost;
+ private Date dateCreated;
+ private int deliveryCost;
+ private boolean deliveryIncluded;
+ private boolean executed;
+
+ public Order build() {
+ Order order = new Order();
+ order.id = id;
+ order.userAccount = userAccount;
+ order.orderedProducts = orderedProducts;
+ order.bill = bill;
+ order.productsCost = productsCost;
+ order.dateCreated = dateCreated;
+ order.deliveryCost = deliveryCost;
+ order.deliveryIncluded = deliveryIncluded;
+ order.executed = executed;
+ return order;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setUserAccount(UserAccount userAccount) {
+ this.userAccount = userAccount;
+ return this;
+ }
+
+ public Builder setOrderedProducts(Set orderedProducts) {
+ this.orderedProducts = orderedProducts;
+ return this;
+ }
+
+ public Builder setBill(Bill bill) {
+ this.bill = bill;
+ return this;
+ }
+
+ public Builder setProductsCost(double productsCost) {
+ this.productsCost = productsCost;
+ return this;
+ }
+
+ public Builder setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ return this;
+ }
+
+ public Builder setDeliveryCost(int deliveryCost) {
+ this.deliveryCost = deliveryCost;
+ return this;
+ }
+
+ public Builder setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ return this;
+ }
+
+ public Builder setExecuted(boolean executed) {
+ this.executed = executed;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/OrderedProduct.java b/sut-market/market-core/src/main/java/market/domain/OrderedProduct.java
new file mode 100644
index 0000000..447165d
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/OrderedProduct.java
@@ -0,0 +1,119 @@
+package market.domain;
+
+import javax.persistence.*;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * {@link Product} of the {@link Order}.
+ */
+
+/*
+ * Tests: incluido orden de columnas para serializar en Json
+ */
+@Entity
+@Table(name = "ordered_product")
+@JsonPropertyOrder({"pk", "order", "product", "quantity"})
+public class OrderedProduct implements Serializable {
+ private static final long serialVersionUID = -2055528467252485472L;
+
+ @EmbeddedId
+ private OrderedProductId pk = new OrderedProductId();
+
+ // Test: anyadido para evitar recursividad en json con Order
+ @JsonBackReference
+ @MapsId("orderId")
+ @JoinColumn(name = "customer_order_id", referencedColumnName = "id")
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ private Order order;
+
+ @MapsId("productId")
+ @JoinColumn(name = "product_id", referencedColumnName = "id")
+ @ManyToOne(optional = false, fetch = FetchType.EAGER)
+ private Product product;
+
+ @Column(name = "quantity")
+ private int quantity;
+
+ public OrderedProductId getPk() {
+ return pk;
+ }
+
+ public void setPk(OrderedProductId pk) {
+ this.pk = pk;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public Order getOrder() {
+ return order;
+ }
+
+ public void setOrder(Order order) {
+ this.order = order;
+ getPk().setCustomerOrder(order.getId());
+ }
+
+ public Product getProduct() {
+ return product;
+ }
+
+ public void setProduct(Product product) {
+ this.product = product;
+ getPk().setProduct(product.getId());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ OrderedProduct that = (OrderedProduct) o;
+ return quantity == that.quantity &&
+ Objects.equals(order.getId(), that.order.getId()) &&
+ Objects.equals(product.getId(), that.product.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(order.getId(), product.getId(), quantity);
+ }
+
+ public static class Builder {
+ private Order order;
+ private Product product;
+ private int quantity;
+
+ public OrderedProduct build() {
+ OrderedProduct orderedProduct = new OrderedProduct();
+ orderedProduct.setOrder(order);
+ orderedProduct.setProduct(product);
+ orderedProduct.setQuantity(quantity);
+ return orderedProduct;
+ }
+
+ public Builder setOrder(Order order) {
+ this.order = order;
+ return this;
+ }
+
+ public Builder setProduct(Product product) {
+ this.product = product;
+ return this;
+ }
+
+ public Builder setQuantity(int quantity) {
+ this.quantity = quantity;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/OrderedProductId.java b/sut-market/market-core/src/main/java/market/domain/OrderedProductId.java
new file mode 100644
index 0000000..beac55d
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/OrderedProductId.java
@@ -0,0 +1,60 @@
+package market.domain;
+
+import javax.persistence.*;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Primary key of a {@link Product} inside the {@link Order}.
+ */
+
+/*
+ * Tests: incluido orden de columnas para serializar en Json
+ */
+@JsonPropertyOrder({"product","customerOrder"})
+
+@Embeddable
+public class OrderedProductId implements Serializable {
+ private static final long serialVersionUID = 5368453186150127449L;
+
+ private Long orderId;
+ private Long productId;
+
+ public Long getCustomerOrder() {
+ return orderId;
+ }
+
+ public void setCustomerOrder(Long orderId) {
+ this.orderId = orderId;
+ }
+
+ public Long getProduct() {
+ return productId;
+ }
+
+ public void setProduct(Long productId) {
+ this.productId = productId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ OrderedProductId that = (OrderedProductId) o;
+
+ if (!Objects.equals(orderId, that.orderId)) return false;
+ return Objects.equals(productId, that.productId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ result = (orderId != null ? orderId.hashCode() : 0);
+ result = 31 * result + (productId != null ? productId.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Product.java b/sut-market/market-core/src/main/java/market/domain/Product.java
new file mode 100644
index 0000000..2a9618a
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Product.java
@@ -0,0 +1,241 @@
+package market.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+import java.util.Objects;
+
+@Entity
+@Table(name = "product")
+public class Product implements Serializable {
+ private static final long serialVersionUID = -5637368176838137416L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "distillery_id", nullable = false)
+ private Distillery distillery;
+
+ @Column(name = "name", nullable = false)
+ @NotEmpty
+ @Pattern(regexp = "^[^#$%^&*()']*$")
+ private String name;
+
+ @Column(name = "price", nullable = false)
+ @NotNull
+ private Double price;
+
+ @Column(name = "description")
+ private String description;
+
+ @Column(name = "volume")
+ @NotNull
+ private Integer volume;
+
+ @Column(name = "alcohol")
+ @NotNull
+ @Min(value = 1)
+ @Max(value = 96)
+ private Float alcohol;
+
+ @Column(name = "age")
+ @Max(value = 2000)
+ private Integer age;
+
+ @Column(name = "available", nullable = false)
+ private boolean available = true;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public @NotNull Double getPrice() {
+ return price;
+ }
+
+ public void setPrice(@NotNull Double price) {
+ this.price = price;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Integer getVolume() {
+ return volume;
+ }
+
+ public void setVolume(Integer volume) {
+ this.volume = volume;
+ }
+
+ public Float getAlcohol() {
+ return alcohol;
+ }
+
+ public void setAlcohol(Float alcohol) {
+ this.alcohol = alcohol;
+ }
+
+ public Integer getAge() {
+ return age;
+ }
+
+ public void setAge(Integer age) {
+ this.age = age;
+ }
+
+ public Distillery getDistillery() {
+ return distillery;
+ }
+
+ public void setDistillery(Distillery distillery) {
+ this.distillery = distillery;
+ }
+
+ public boolean isAvailable() {
+ return available;
+ }
+
+ public void setAvailable(boolean available) {
+ this.available = available;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Product product = (Product) o;
+ return id == product.id &&
+ available == product.available &&
+ Objects.equals(distillery, product.distillery) &&
+ Objects.equals(name, product.name) &&
+ Objects.equals(price, product.price) &&
+ Objects.equals(description, product.description) &&
+ Objects.equals(volume, product.volume) &&
+ Objects.equals(alcohol, product.alcohol) &&
+ Objects.equals(age, product.age);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, distillery, name, price, description, volume, alcohol, age, available);
+ }
+
+ public static class Builder {
+ private Long id;
+ private Distillery distillery;
+ private String name;
+ private Double price;
+ private String description;
+ private Integer volume;
+ private Float alcohol;
+ private Integer age;
+ private boolean available = true;
+
+ public Builder() {
+ }
+
+ public Builder(Product product) {
+ id = product.id;
+ distillery = product.distillery;
+ name = product.name;
+ price = product.price;
+ description = product.description;
+ volume = product.volume;
+ alcohol = product.alcohol;
+ age = product.age;
+ available = product.available;
+ }
+
+ public Product build() {
+ Product product = new Product();
+ product.id = id;
+ product.distillery = distillery;
+ product.name = name;
+ product.price = price;
+ product.description = description;
+ product.volume = volume;
+ product.alcohol = alcohol;
+ product.age = age;
+ product.available = available;
+ return product;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setDistillery(Distillery distillery) {
+ this.distillery = distillery;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setPrice(@NotNull Double price) {
+ this.price = price;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder setVolume(Integer volume) {
+ this.volume = volume;
+ return this;
+ }
+
+ public Builder setAlcohol(Float alcohol) {
+ this.alcohol = alcohol;
+ return this;
+ }
+
+ public Builder setAge(Integer age) {
+ this.age = age;
+ return this;
+ }
+
+ public Builder setAvailable(boolean available) {
+ this.available = available;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Region.java b/sut-market/market-core/src/main/java/market/domain/Region.java
new file mode 100644
index 0000000..01cfe88
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Region.java
@@ -0,0 +1,158 @@
+package market.domain;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+import java.util.Objects;
+
+@Entity
+@Table(name = "region")
+public class Region implements Serializable {
+ public static final Region NULL;
+ private static final long serialVersionUID = 5413261502059862627L;
+
+ static {
+ NULL = new Region.Builder()
+ .setId(0L)
+ .setName("null region")
+ .build();
+ }
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ private Long id;
+
+ @Column(name = "name", nullable = false)
+ @NotEmpty
+ @Pattern(regexp = "^[^#$%^&*()']*$")
+ private String name;
+
+ @Column(name = "subtitle")
+ @Pattern(regexp = "^[^#$%^*()']*$")
+ private String subtitle;
+
+ @Column(name = "description")
+ private String description;
+
+ @Column(name = "color")
+ @Pattern(regexp = "^(a-z|A-Z|0-9-)*[^#$%^&*()']*$")
+ private String color;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getSubtitle() {
+ return subtitle;
+ }
+
+ public void setSubtitle(String subtitle) {
+ this.subtitle = subtitle;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getColor() {
+ return color;
+ }
+
+ public void setColor(String color) {
+ this.color = color;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Region region = (Region) o;
+ return Objects.equals(id, region.id) &&
+ Objects.equals(name, region.name) &&
+ Objects.equals(subtitle, region.subtitle) &&
+ Objects.equals(description, region.description) &&
+ Objects.equals(color, region.color);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, subtitle, description, color);
+ }
+
+ public static class Builder {
+ private Long id;
+ private String name;
+ private String subtitle;
+ private String description;
+ private String color;
+
+ public Builder() {
+ }
+
+ public Builder(Region region) {
+ id = region.id;
+ name = region.name;
+ subtitle = region.subtitle;
+ description = region.description;
+ color = region.color;
+ }
+
+ public Region build() {
+ Region region = new Region();
+ region.id = id;
+ region.name = name;
+ region.subtitle = subtitle;
+ region.description = description;
+ region.color = color;
+ return region;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setSubtitle(String subtitle) {
+ this.subtitle = subtitle;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder setColor(String color) {
+ this.color = color;
+ return this;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/Role.java b/sut-market/market-core/src/main/java/market/domain/Role.java
new file mode 100644
index 0000000..e540008
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/Role.java
@@ -0,0 +1,60 @@
+package market.domain;
+
+import javax.persistence.*;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Role of a {@link UserAccount}.
+ */
+@Entity
+@Table(name = "role")
+public class Role implements Serializable {
+ private static final long serialVersionUID = -1387354647838847103L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ private Long id;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ // Test: anyadido para evitar recursividad en json con UserAccount
+ @JsonBackReference
+ @ManyToMany(mappedBy = "roles")
+ private Set users = new HashSet<>();
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public Set getUsers() {
+ return users;
+ }
+
+ public void setUsers(Set users) {
+ this.users = users;
+ }
+
+ enum Roles {
+ ROLE_ADMIN,
+ ROLE_STAFF,
+ ROLE_USER
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/domain/UserAccount.java b/sut-market/market-core/src/main/java/market/domain/UserAccount.java
new file mode 100644
index 0000000..d45486e
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/domain/UserAccount.java
@@ -0,0 +1,218 @@
+package market.domain;
+
+import javax.persistence.*;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+@Entity
+@Table(name = "user_account")
+public class UserAccount implements Serializable {
+ private static final long serialVersionUID = -8278943418573848966L;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", insertable = false, updatable = false, nullable = false)
+ private Long id;
+
+ @Column(name = "email", nullable = false)
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ private String password;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "active", nullable = false)
+ private boolean active;
+
+ // Test: anyadido para evitar recursividad en json con Contacts
+ @JsonBackReference
+ @OneToOne(mappedBy = "userAccount", cascade = CascadeType.ALL)
+ private Contacts contacts;
+
+
+ // Test: anyadido para evitar recursividad en json con Cart
+ @JsonBackReference
+ @OneToOne(mappedBy = "userAccount", cascade = CascadeType.ALL)
+ private Cart cart;
+
+ @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
+ @JoinTable(name = "user_role",
+ joinColumns = {
+ @JoinColumn(name = "user_id")},
+ inverseJoinColumns = {
+ @JoinColumn(name = "role_id")})
+ private Set roles = new HashSet<>();
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public Contacts getContacts() {
+ return contacts;
+ }
+
+ public void setContacts(Contacts contacts) {
+ this.contacts = contacts;
+ }
+
+ public Cart getCart() {
+ return cart;
+ }
+
+ public void setCart(Cart cart) {
+ this.cart = cart;
+ }
+
+ public Set getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set roles) {
+ this.roles = roles;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserAccount account = (UserAccount) o;
+ return active == account.active &&
+ Objects.equals(id, account.id) &&
+ Objects.equals(email, account.email) &&
+ Objects.equals(password, account.password) &&
+ Objects.equals(name, account.name) &&
+ Objects.equals(contacts, account.contacts) &&
+ Objects.equals(cart, account.cart) &&
+ Objects.equals(roles, account.roles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, email, password, name, active, contacts, cart, roles);
+ }
+
+ public static class Builder {
+ private Long id;
+ private String email;
+ private String password;
+ private String name;
+ private boolean active;
+ private Contacts contacts;
+ private Cart cart;
+ private Set roles = new HashSet<>();
+
+ public Builder() {
+ }
+
+ public Builder(UserAccount account) {
+ this.id = account.id;
+ this.email = account.email;
+ this.password = account.password;
+ this.name = account.name;
+ this.active = account.active;
+ this.contacts = account.contacts;
+ this.cart = account.cart;
+ this.roles = account.roles;
+ }
+
+ public UserAccount build() {
+ UserAccount account = new UserAccount();
+ account.id = id;
+ account.email = email;
+ account.password = password;
+ account.name = name;
+ account.active = active;
+ account.contacts = contacts;
+ account.cart = cart;
+ account.roles = roles;
+ return account;
+ }
+
+ public Builder setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public Builder setEmail(String email) {
+ this.email = email;
+ return this;
+ }
+
+ public Builder setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setActive(boolean active) {
+ this.active = active;
+ return this;
+ }
+
+ public Builder setContacts(Contacts contacts) {
+ this.contacts = contacts;
+ return this;
+ }
+
+ public Builder setCart(Cart cart) {
+ this.cart = cart;
+ return this;
+ }
+
+ public Builder setRoles(Set roles) {
+ this.roles = roles;
+ return this;
+ }
+
+ public Long getId() {
+ return id;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/BillDTO.java b/sut-market/market-core/src/main/java/market/dto/BillDTO.java
new file mode 100644
index 0000000..98503c1
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/BillDTO.java
@@ -0,0 +1,80 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Objects;
+
+public class BillDTO extends RepresentationModel {
+ private Long id;
+ private int number;
+ private double totalCost;
+ private boolean payed;
+ private String ccNumber;
+ private String dateCreated;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public void setNumber(int number) {
+ this.number = number;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public void setTotalCost(double totalCost) {
+ this.totalCost = totalCost;
+ }
+
+ public boolean isPayed() {
+ return payed;
+ }
+
+ public void setPayed(boolean payed) {
+ this.payed = payed;
+ }
+
+ public String getCcNumber() {
+ return ccNumber;
+ }
+
+ public void setCcNumber(String ccNumber) {
+ this.ccNumber = ccNumber;
+ }
+
+ public String getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(String dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BillDTO billDTO = (BillDTO) o;
+ return number == billDTO.number &&
+ Double.compare(billDTO.totalCost, totalCost) == 0 &&
+ payed == billDTO.payed &&
+ Objects.equals(id, billDTO.id) &&
+ Objects.equals(ccNumber, billDTO.ccNumber) &&
+ Objects.equals(dateCreated, billDTO.dateCreated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, number, totalCost, payed, ccNumber, dateCreated);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/CartDTO.java b/sut-market/market-core/src/main/java/market/dto/CartDTO.java
new file mode 100644
index 0000000..605aded
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/CartDTO.java
@@ -0,0 +1,119 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Адаптер корзины.
+ */
+public class CartDTO extends RepresentationModel {
+
+ private String user;
+ private int totalItems;
+ private double productsCost;
+ private int deliveryCost;
+ private boolean deliveryIncluded;
+ private double totalCost;
+ private List cartItems = Collections.emptyList();
+
+ public boolean containsProductId(long targetProductId) {
+ return cartItems.stream()
+ .map(CartItemDTO::getProductId)
+ .anyMatch(id -> id == targetProductId);
+ }
+
+ public boolean isEmpty() {
+ return cartItems.isEmpty();
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public List getCartItems() {
+ return cartItems;
+ }
+
+ public void setCartItems(List cartItems) {
+ this.cartItems = cartItems;
+ }
+
+ public boolean isDeliveryIncluded() {
+ return deliveryIncluded;
+ }
+
+ public void setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ }
+
+ public double getProductsCost() {
+ return productsCost;
+ }
+
+ public void setProductsCost(double productsCost) {
+ this.productsCost = productsCost;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public void setTotalCost(double totalCost) {
+ this.totalCost = totalCost;
+ }
+
+ public int getDeliveryCost() {
+ return deliveryCost;
+ }
+
+ public void setDeliveryCost(int deliveryCost) {
+ this.deliveryCost = deliveryCost;
+ }
+
+ public int getTotalItems() {
+ return totalItems;
+ }
+
+ public void setTotalItems(int totalItems) {
+ this.totalItems = totalItems;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CartDTO cartDTO = (CartDTO) o;
+ return totalItems == cartDTO.totalItems &&
+ Double.compare(cartDTO.productsCost, productsCost) == 0 &&
+ deliveryCost == cartDTO.deliveryCost &&
+ deliveryIncluded == cartDTO.deliveryIncluded &&
+ Double.compare(cartDTO.totalCost, totalCost) == 0 &&
+ Objects.equals(user, cartDTO.user) &&
+ Objects.equals(cartItems, cartDTO.cartItems);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(user, cartItems, totalItems, productsCost, deliveryCost, deliveryIncluded, totalCost);
+ }
+
+ @Override
+ public String toString() {
+ return "CartDTO{" +
+ "user='" + user + '\'' +
+ ", totalItems=" + totalItems +
+ ", productsCost=" + productsCost +
+ ", deliveryCost=" + deliveryCost +
+ ", deliveryIncluded=" + deliveryIncluded +
+ ", totalCost=" + totalCost +
+ ", cartItems=" + cartItems +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/CartItemDTO.java b/sut-market/market-core/src/main/java/market/dto/CartItemDTO.java
new file mode 100644
index 0000000..b740df1
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/CartItemDTO.java
@@ -0,0 +1,59 @@
+package market.dto;
+
+import org.springframework.format.annotation.NumberFormat;
+import org.springframework.hateoas.RepresentationModel;
+
+import javax.validation.constraints.Positive;
+import java.util.Objects;
+
+/**
+ * Адаптер элемента корзины.
+ */
+public class CartItemDTO extends RepresentationModel {
+
+ @Positive
+ @NumberFormat
+ private long productId;
+
+ @Positive
+ @NumberFormat
+ private int quantity;
+
+ public long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CartItemDTO that = (CartItemDTO) o;
+ return productId == that.productId &&
+ quantity == that.quantity;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productId, quantity);
+ }
+
+ @Override
+ public String toString() {
+ return "CartItemDTO{" +
+ "productId=" + productId +
+ ", quantity=" + quantity +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/ContactsDTO.java b/sut-market/market-core/src/main/java/market/dto/ContactsDTO.java
new file mode 100644
index 0000000..1bee8fd
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/ContactsDTO.java
@@ -0,0 +1,62 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.util.Objects;
+
+/**
+ * Контактные данные пользователя.
+ */
+public class ContactsDTO extends RepresentationModel {
+
+ @NotEmpty
+ @Size(max = 20)
+ @Pattern(regexp = UserDTO.USER_PHONE_REGEX)
+ private String phone;
+
+ @NotEmpty
+ @Size(max = 100)
+ @Pattern(regexp = UserDTO.USER_ADDRESS_REGEX)
+ private String address;
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ContactsDTO that = (ContactsDTO) o;
+ return Objects.equals(phone, that.phone) &&
+ Objects.equals(address, that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(phone, address);
+ }
+
+ @Override
+ public String toString() {
+ return "ContactsDTO{" +
+ "phone='" + phone + '\'' +
+ ", address='" + address + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/CreditCardDTO.java b/sut-market/market-core/src/main/java/market/dto/CreditCardDTO.java
new file mode 100644
index 0000000..eaa57e3
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/CreditCardDTO.java
@@ -0,0 +1,36 @@
+package market.dto;
+
+import org.hibernate.validator.constraints.CreditCardNumber;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+public class CreditCardDTO {
+
+ @NotEmpty
+ @CreditCardNumber
+ @Pattern(regexp = "\\b(?:\\d[ -]*?){13,16}\\b")
+ private String ccNumber;
+
+ public CreditCardDTO() {
+ }
+
+ public CreditCardDTO(String number) {
+ this.ccNumber = number;
+ }
+
+ public String getCcNumber() {
+ return ccNumber;
+ }
+
+ public void setCcNumber(String ccNumber) {
+ this.ccNumber = ccNumber;
+ }
+
+ @Override
+ public String toString() {
+ return "CreditCardDTO{" +
+ "ccNumber='" + ccNumber + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/DistilleryDTO.java b/sut-market/market-core/src/main/java/market/dto/DistilleryDTO.java
new file mode 100644
index 0000000..9a0faea
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/DistilleryDTO.java
@@ -0,0 +1,71 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Objects;
+
+public class DistilleryDTO extends RepresentationModel {
+
+ private Long id;
+ private String title;
+ private String region;
+ private String description;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DistilleryDTO that = (DistilleryDTO) o;
+ return Objects.equals(id, that.id) &&
+ Objects.equals(title, that.title) &&
+ Objects.equals(region, that.region) &&
+ Objects.equals(description, that.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, title, region, description);
+ }
+
+ @Override
+ public String toString() {
+ return "DistilleryDTO{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", region='" + region + '\'' +
+ ", description='" + description + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/OrderDTO.java b/sut-market/market-core/src/main/java/market/dto/OrderDTO.java
new file mode 100644
index 0000000..e3b5e0b
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/OrderDTO.java
@@ -0,0 +1,130 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class OrderDTO extends RepresentationModel {
+
+ private String userAccount;
+ private long id;
+ private int billNumber;
+ private Date dateCreated;
+ private double productsCost;
+ private int deliveryCost;
+ private boolean deliveryIncluded;
+ private double totalCost;
+ private boolean payed;
+ private boolean executed;
+
+ public String getUserAccount() {
+ return userAccount;
+ }
+
+ public void setUserAccount(String userAccount) {
+ this.userAccount = userAccount;
+ }
+
+ public int getBillNumber() {
+ return billNumber;
+ }
+
+ public void setBillNumber(int billNumber) {
+ this.billNumber = billNumber;
+ }
+
+ public double getProductsCost() {
+ return productsCost;
+ }
+
+ public void setProductsCost(double productsCost) {
+ this.productsCost = productsCost;
+ }
+
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public int getDeliveryCost() {
+ return deliveryCost;
+ }
+
+ public void setDeliveryCost(int deliveryCost) {
+ this.deliveryCost = deliveryCost;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public void setTotalCost(double totalCost) {
+ this.totalCost = totalCost;
+ }
+
+ public boolean isDeliveryIncluded() {
+ return deliveryIncluded;
+ }
+
+ public void setDeliveryIncluded(boolean deliveryIncluded) {
+ this.deliveryIncluded = deliveryIncluded;
+ }
+
+ public boolean isExecuted() {
+ return executed;
+ }
+
+ public void setExecuted(boolean executed) {
+ this.executed = executed;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public boolean isPayed() {
+ return payed;
+ }
+
+ public void setPayed(boolean payed) {
+ this.payed = payed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ OrderDTO orderDTO = (OrderDTO) o;
+ return id == orderDTO.id &&
+ billNumber == orderDTO.billNumber &&
+ Double.compare(orderDTO.productsCost, productsCost) == 0 &&
+ deliveryCost == orderDTO.deliveryCost &&
+ deliveryIncluded == orderDTO.deliveryIncluded &&
+ Double.compare(orderDTO.totalCost, totalCost) == 0 &&
+ payed == orderDTO.payed &&
+ executed == orderDTO.executed &&
+ Objects.equals(userAccount, orderDTO.userAccount) &&
+ Objects.equals(dateCreated, orderDTO.dateCreated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userAccount, id, billNumber, dateCreated, productsCost, deliveryCost, deliveryIncluded, totalCost, payed, executed);
+ }
+
+ @Override
+ public String toString() {
+ return "OrderDTO{" +
+ "userAccount='" + userAccount + '\'' +
+ ", id=" + id +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/OrderedProductDTO.java b/sut-market/market-core/src/main/java/market/dto/OrderedProductDTO.java
new file mode 100644
index 0000000..7a87aff
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/OrderedProductDTO.java
@@ -0,0 +1,59 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Objects;
+
+public class OrderedProductDTO extends RepresentationModel {
+ private long orderId;
+ private int quantity;
+ private long productId;
+
+ public long getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(long orderId) {
+ this.orderId = orderId;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(long productId) {
+ this.productId = productId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ OrderedProductDTO that = (OrderedProductDTO) o;
+ return orderId == that.orderId &&
+ quantity == that.quantity &&
+ productId == that.productId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(orderId, quantity, productId);
+ }
+
+ @Override
+ public String toString() {
+ return "OrderedProductDTO{" +
+ "orderId=" + orderId +
+ ", quantity=" + quantity +
+ ", productId=" + productId +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/ProductDTO.java b/sut-market/market-core/src/main/java/market/dto/ProductDTO.java
new file mode 100644
index 0000000..c4a4d88
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/ProductDTO.java
@@ -0,0 +1,146 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.util.Objects;
+
+public class ProductDTO extends RepresentationModel {
+
+ private Long productId;
+
+ private String distillery;
+
+ @NotEmpty
+ @Pattern(regexp = "^[^#$%^&*()']*$")
+ private String name;
+
+ @NotNull
+ private Double price;
+
+ @Max(value = 2000)
+ private Integer age;
+
+ @NotNull
+ private Integer volume;
+
+ @NotNull
+ @Min(value = 1)
+ @Max(value = 96)
+ private Float alcohol;
+
+ private String description;
+ private boolean available;
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public String getDistillery() {
+ return distillery;
+ }
+
+ public void setDistillery(String distillery) {
+ this.distillery = distillery;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Double getPrice() {
+ return price;
+ }
+
+ public void setPrice(@NotNull Double price) {
+ this.price = price;
+ }
+
+ public Integer getAge() {
+ return age;
+ }
+
+ public void setAge(Integer age) {
+ this.age = age;
+ }
+
+ public Integer getVolume() {
+ return volume;
+ }
+
+ public void setVolume(Integer volume) {
+ this.volume = volume;
+ }
+
+ public Float getAlcohol() {
+ return alcohol;
+ }
+
+ public void setAlcohol(Float alcohol) {
+ this.alcohol = alcohol;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public boolean isAvailable() {
+ return available;
+ }
+
+ public void setAvailable(boolean available) {
+ this.available = available;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ProductDTO that = (ProductDTO) o;
+ return available == that.available &&
+ Objects.equals(productId, that.productId) &&
+ Objects.equals(distillery, that.distillery) &&
+ Objects.equals(name, that.name) &&
+ Objects.equals(price, that.price) &&
+ Objects.equals(age, that.age) &&
+ Objects.equals(volume, that.volume) &&
+ Objects.equals(alcohol, that.alcohol) &&
+ Objects.equals(description, that.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productId, distillery, name, price, age, volume, alcohol, description, available);
+ }
+
+ @Override
+ public String toString() {
+ return "ProductDTO{" +
+ "productId=" + productId +
+ ", distillery='" + distillery + '\'' +
+ ", name='" + name + '\'' +
+ ", price=" + price +
+ ", age=" + age +
+ ", volume=" + volume +
+ ", alcohol=" + alcohol +
+ ", description='" + description + '\'' +
+ ", available=" + available +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/ProductPreviewDTO.java b/sut-market/market-core/src/main/java/market/dto/ProductPreviewDTO.java
new file mode 100644
index 0000000..62435f3
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/ProductPreviewDTO.java
@@ -0,0 +1,66 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+/**
+ * Адаптер товара.
+ */
+public class ProductPreviewDTO extends RepresentationModel {
+
+ private long productId;
+ private String region;
+ private String distillery;
+ private String name;
+ private Double price;
+
+ public long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(long productId) {
+ this.productId = productId;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getDistillery() {
+ return distillery;
+ }
+
+ public void setDistillery(String distillery) {
+ this.distillery = distillery;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Double getPrice() {
+ return price;
+ }
+
+ public void setPrice(Double price) {
+ this.price = price;
+ }
+
+ @Override
+ public String toString() {
+ return "ProductPreviewDTO{" +
+ "productId=" + productId +
+ ", region='" + region + '\'' +
+ ", distillery='" + distillery + '\'' +
+ ", name='" + name + '\'' +
+ ", price=" + price +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/RegionDTO.java b/sut-market/market-core/src/main/java/market/dto/RegionDTO.java
new file mode 100644
index 0000000..8d08ab8
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/RegionDTO.java
@@ -0,0 +1,82 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import java.util.Objects;
+
+public class RegionDTO extends RepresentationModel {
+
+ private Long id;
+ private String name;
+ private String subtitle;
+ private String description;
+ private String color;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getSubtitle() {
+ return subtitle;
+ }
+
+ public void setSubtitle(String subtitle) {
+ this.subtitle = subtitle;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getColor() {
+ return color;
+ }
+
+ public void setColor(String color) {
+ this.color = color;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RegionDTO regionDTO = (RegionDTO) o;
+ return Objects.equals(id, regionDTO.id) &&
+ Objects.equals(name, regionDTO.name) &&
+ Objects.equals(subtitle, regionDTO.subtitle) &&
+ Objects.equals(description, regionDTO.description) &&
+ Objects.equals(color, regionDTO.color);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, subtitle, description, color);
+ }
+
+ @Override
+ public String toString() {
+ return "RegionDTO{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", subtitle='" + subtitle + '\'' +
+ ", description='" + description + '\'' +
+ ", color='" + color + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/UserDTO.java b/sut-market/market-core/src/main/java/market/dto/UserDTO.java
new file mode 100644
index 0000000..bfcee6a
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/UserDTO.java
@@ -0,0 +1,109 @@
+package market.dto;
+
+import org.springframework.hateoas.RepresentationModel;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.util.Objects;
+
+public class UserDTO extends RepresentationModel {
+ public static final String ALPHANUMERIC = "^[a-zA-Z0-9]+$";
+ public static final String EMAIL_REGEX = "^[\\w-]+(\\.[\\w-]+)*@([\\w-]+\\.)+[a-zA-Z]+$";
+ public static final String USER_PHONE_REGEX = "^\\+[1-9][0-9]?[\\s]*\\(?\\d{3}\\)?[-\\s]?\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{2}$";
+ public static final String USER_ADDRESS_REGEX = "^[^#$%^*()']*$";
+
+ public static final String HIDDEN_PASSWORD = "hidden";
+
+ @NotEmpty
+ @Size(max = 50)
+ @Pattern(regexp = EMAIL_REGEX)
+ private String email;
+
+ @Size(min = 6, max = 50)
+ @Pattern(regexp = ALPHANUMERIC)
+ private String password;
+
+ @NotEmpty
+ @Size(max = 50)
+ @Pattern(regexp = "^[\\pL '-]+$")
+ private String name;
+
+ @NotEmpty
+ @Size(max = 20)
+ @Pattern(regexp = USER_PHONE_REGEX)
+ private String phone;
+
+ @NotEmpty
+ @Size(max = 100)
+ @Pattern(regexp = USER_ADDRESS_REGEX)
+ private String address;
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserDTO userDTO = (UserDTO) o;
+ return Objects.equals(email, userDTO.email) &&
+ Objects.equals(password, userDTO.password) &&
+ Objects.equals(name, userDTO.name) &&
+ Objects.equals(phone, userDTO.phone) &&
+ Objects.equals(address, userDTO.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(email, password, name, phone, address);
+ }
+
+ @Override
+ public String toString() {
+ return "UserDTO{" +
+ "email='" + email + '\'' +
+ ", password='" + HIDDEN_PASSWORD + '\'' +
+ ", name='" + name + '\'' +
+ ", phone='" + phone + '\'' +
+ ", address='" + address + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/BillDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/BillDtoAssembler.java
new file mode 100644
index 0000000..6ad857f
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/BillDtoAssembler.java
@@ -0,0 +1,20 @@
+package market.dto.assembler;
+
+import market.domain.Bill;
+import market.dto.BillDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+public class BillDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public BillDTO toModel(Bill bill) {
+ BillDTO dto = new BillDTO();
+ dto.setId(bill.getId());
+ dto.setNumber(bill.getNumber());
+ dto.setDateCreated(bill.getDateCreated().toString());
+ dto.setTotalCost(bill.getTotalCost());
+ dto.setPayed(bill.isPayed());
+ dto.setCcNumber(bill.getCcNumber());
+ return dto;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/CartDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/CartDtoAssembler.java
new file mode 100644
index 0000000..1cec2cd
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/CartDtoAssembler.java
@@ -0,0 +1,74 @@
+package market.dto.assembler;
+
+import market.domain.Cart;
+import market.domain.CartItem;
+import market.domain.Product;
+import market.dto.CartDTO;
+import market.dto.CartItemDTO;
+import market.properties.MarketProperties;
+import market.service.ProductService;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class CartDtoAssembler implements RepresentationModelAssembler {
+
+ private final MarketProperties marketProperties;
+
+ public CartDtoAssembler(MarketProperties marketProperties) {
+ this.marketProperties = marketProperties;
+ }
+
+ @Override
+ public CartDTO toModel(Cart cart) {
+ CartDTO dto = toAnonymousResource(cart);
+ dto.setUser(cart.getUserAccount().getEmail());
+ return dto;
+ }
+
+ public CartDTO toAnonymousResource(Cart cart) {
+ int deliveryCost = marketProperties.getDeliveryCost();
+
+ CartDTO dto = new CartDTO();
+ dto.setDeliveryIncluded(cart.isDeliveryIncluded());
+ dto.setProductsCost(cart.getItemsCost());
+ dto.setDeliveryCost(deliveryCost);
+ dto.setTotalCost(cart.getItemsCost() + deliveryCost);
+ dto.setTotalItems(cart.getItemsCount());
+
+ List cartItemsDto = cart.getCartItems().stream()
+ .map(this::toCartItemDto)
+ .collect(Collectors.toList());
+ dto.setCartItems(cartItemsDto);
+
+ return dto;
+ }
+
+ public CartItemDTO toCartItemDto(CartItem cartItem) {
+ Long productId = cartItem.getProduct().getId();
+
+ CartItemDTO dto = new CartItemDTO();
+ dto.setProductId(productId);
+ dto.setQuantity(cartItem.getQuantity());
+ return dto;
+ }
+
+ /**
+ * @return domain cart created from DTO
+ */
+ public Cart toDomain(CartDTO cartDTO, ProductService productService) { // todo: avoid passing service here, pass a map
+ Cart cart = new Cart();
+ cart.setDeliveryIncluded(cartDTO.isDeliveryIncluded());
+ for (CartItemDTO cartItemDto : cartDTO.getCartItems()) {
+ Optional productOptional = productService.findById(cartItemDto.getProductId());
+ if (productOptional.isPresent()) {
+ Product product = productOptional.get();
+ if (product.isAvailable())
+ cart.update(product, cartItemDto.getQuantity());
+ }
+ }
+ return cart;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/ContactsDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/ContactsDtoAssembler.java
new file mode 100644
index 0000000..ea00a42
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/ContactsDtoAssembler.java
@@ -0,0 +1,23 @@
+package market.dto.assembler;
+
+import market.domain.Contacts;
+import market.dto.ContactsDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+public class ContactsDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public ContactsDTO toModel(Contacts contacts) {
+ ContactsDTO dto = new ContactsDTO();
+ dto.setPhone(contacts.getPhone());
+ dto.setAddress(contacts.getAddress());
+ return dto;
+ }
+
+ public Contacts toDomain(ContactsDTO dto) {
+ Contacts contacts = new Contacts();
+ contacts.setAddress(dto.getAddress());
+ contacts.setPhone(dto.getPhone());
+ return contacts;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/DistilleryDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/DistilleryDtoAssembler.java
new file mode 100644
index 0000000..6f5a607
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/DistilleryDtoAssembler.java
@@ -0,0 +1,31 @@
+package market.dto.assembler;
+
+import market.domain.Distillery;
+import market.dto.DistilleryDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+import java.util.List;
+
+public class DistilleryDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public DistilleryDTO toModel(Distillery distillery) {
+ DistilleryDTO dto = new DistilleryDTO();
+ dto.setId(distillery.getId());
+ dto.setTitle(distillery.getTitle());
+ dto.setDescription(distillery.getDescription());
+ dto.setRegion(distillery.getRegion().getName());
+ return dto;
+ }
+
+ public DistilleryDTO[] toDtoArray(List items) {
+ return toCollectionModel(items).getContent().toArray(new DistilleryDTO[items.size()]);
+ }
+
+ public Distillery toDomain(DistilleryDTO dto) {
+ return new Distillery.Builder()
+ .setTitle(dto.getTitle())
+ .setDescription(dto.getDescription())
+ .build();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/OrderDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/OrderDtoAssembler.java
new file mode 100644
index 0000000..07e1649
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/OrderDtoAssembler.java
@@ -0,0 +1,37 @@
+package market.dto.assembler;
+
+import market.domain.Order;
+import market.dto.OrderDTO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+import java.util.List;
+
+public class OrderDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public OrderDTO toModel(Order order) {
+ OrderDTO dto = new OrderDTO();
+ dto.setId(order.getId());
+ dto.setUserAccount(order.getUserAccount().getEmail());
+ dto.setBillNumber(order.getBill().getNumber());
+ dto.setProductsCost(order.getProductsCost());
+ dto.setDateCreated(order.getDateCreated());
+ dto.setDeliveryCost(order.getDeliveryCost());
+ dto.setTotalCost(order.isDeliveryIncluded() ? (order.getProductsCost() + order.getDeliveryCost()) : order.getProductsCost());
+ dto.setDeliveryIncluded(order.isDeliveryIncluded());
+ dto.setPayed(order.getBill().isPayed());
+ dto.setExecuted(order.isExecuted());
+ return dto;
+ }
+
+ public PageImpl toModel(Page page) {
+ List dtoList = page.map(this::toModel).toList();
+ return new PageImpl<>(dtoList, page.getPageable(), page.getTotalElements());
+ }
+
+ public OrderDTO[] toDtoArray(List items) {
+ return toCollectionModel(items).getContent().toArray(new OrderDTO[items.size()]);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/OrderedProductDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/OrderedProductDtoAssembler.java
new file mode 100644
index 0000000..a0095ee
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/OrderedProductDtoAssembler.java
@@ -0,0 +1,17 @@
+package market.dto.assembler;
+
+import market.domain.OrderedProduct;
+import market.dto.OrderedProductDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+public class OrderedProductDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public OrderedProductDTO toModel(OrderedProduct orderedProduct) {
+ OrderedProductDTO dto = new OrderedProductDTO();
+ dto.setOrderId(orderedProduct.getOrder().getId());
+ dto.setQuantity(orderedProduct.getQuantity());
+ dto.setProductId(orderedProduct.getProduct().getId());
+ return dto;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/ProductDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/ProductDtoAssembler.java
new file mode 100644
index 0000000..c4df732
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/ProductDtoAssembler.java
@@ -0,0 +1,44 @@
+package market.dto.assembler;
+
+import market.domain.Product;
+import market.dto.ProductDTO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+import java.util.List;
+
+public class ProductDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public ProductDTO toModel(Product product) {
+ ProductDTO dto = new ProductDTO();
+ dto.setProductId(product.getId());
+ dto.setDistillery(product.getDistillery() == null ? null : product.getDistillery().getTitle());
+ dto.setName(product.getName());
+ dto.setAge(product.getAge());
+ dto.setAlcohol(product.getAlcohol());
+ dto.setPrice(product.getPrice());
+ dto.setVolume(product.getVolume());
+ dto.setDescription(product.getDescription());
+ dto.setAvailable(product.isAvailable());
+ return dto;
+ }
+
+ public PageImpl toModel(Page page) {
+ List dtoList = page.map(this::toModel).toList();
+ return new PageImpl<>(dtoList, page.getPageable(), page.getTotalElements());
+ }
+
+ public Product dtoDomain(ProductDTO dto) {
+ return new Product.Builder()
+ .setName(dto.getName())
+ .setAge(dto.getAge())
+ .setAlcohol(dto.getAlcohol())
+ .setPrice(dto.getPrice())
+ .setVolume(dto.getVolume())
+ .setDescription(dto.getDescription())
+ .setAvailable(dto.isAvailable())
+ .build();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/ProductPreviewAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/ProductPreviewAssembler.java
new file mode 100644
index 0000000..f90bb39
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/ProductPreviewAssembler.java
@@ -0,0 +1,19 @@
+package market.dto.assembler;
+
+import market.domain.Product;
+import market.dto.ProductPreviewDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+public class ProductPreviewAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public ProductPreviewDTO toModel(Product product) {
+ ProductPreviewDTO dto = new ProductPreviewDTO();
+ dto.setProductId(product.getId());
+ dto.setRegion(product.getDistillery().getRegion().getName());
+ dto.setDistillery(product.getDistillery().getTitle());
+ dto.setName(product.getName());
+ dto.setPrice(product.getPrice());
+ return dto;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/RegionDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/RegionDtoAssembler.java
new file mode 100644
index 0000000..60360e6
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/RegionDtoAssembler.java
@@ -0,0 +1,35 @@
+package market.dto.assembler;
+
+import market.domain.Region;
+import market.dto.RegionDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+import java.util.List;
+
+public class RegionDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public RegionDTO toModel(Region region) {
+ RegionDTO dto = new RegionDTO();
+ dto.setId(region.getId());
+ dto.setName(region.getName());
+ dto.setSubtitle(region.getSubtitle());
+ dto.setColor(region.getColor());
+ dto.setDescription(region.getDescription());
+ return dto;
+ }
+
+ public RegionDTO[] toDtoArray(List items) {
+ return toCollectionModel(items).getContent().toArray(new RegionDTO[items.size()]);
+ }
+
+ public Region toDomain(RegionDTO dto) {
+ return new Region.Builder()
+ .setId(dto.getId())
+ .setName(dto.getName())
+ .setSubtitle(dto.getSubtitle())
+ .setColor(dto.getColor())
+ .setDescription(dto.getDescription())
+ .build();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/assembler/UserAccountDtoAssembler.java b/sut-market/market-core/src/main/java/market/dto/assembler/UserAccountDtoAssembler.java
new file mode 100644
index 0000000..2ddb073
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/assembler/UserAccountDtoAssembler.java
@@ -0,0 +1,36 @@
+package market.dto.assembler;
+
+import market.domain.Contacts;
+import market.domain.UserAccount;
+import market.dto.UserDTO;
+import org.springframework.hateoas.server.RepresentationModelAssembler;
+
+public class UserAccountDtoAssembler implements RepresentationModelAssembler {
+
+ @Override
+ public UserDTO toModel(UserAccount userAccount) {
+ UserDTO dto = new UserDTO();
+ dto.setEmail(userAccount.getEmail());
+ dto.setPassword(UserDTO.HIDDEN_PASSWORD);
+ dto.setName(userAccount.getName());
+ dto.setPhone(userAccount.getContacts().getPhone());
+ dto.setAddress(userAccount.getContacts().getAddress());
+ return dto;
+ }
+
+ public UserAccount toDomain(UserDTO user) {
+ UserAccount userAccount = new UserAccount.Builder()
+ .setEmail(user.getEmail())
+ .setPassword(user.getPassword())
+ .setName(user.getName())
+ .setActive(true)
+ .build();
+ Contacts contacts = new Contacts.Builder()
+ .setUserAccount(userAccount)
+ .setPhone(user.getPhone())
+ .setAddress(user.getAddress())
+ .build();
+ userAccount.setContacts(contacts);
+ return userAccount;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/exception/FieldErrorDTO.java b/sut-market/market-core/src/main/java/market/dto/exception/FieldErrorDTO.java
new file mode 100644
index 0000000..f4e254a
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/exception/FieldErrorDTO.java
@@ -0,0 +1,31 @@
+package market.dto.exception;
+
+/**
+ * Нарушение ограничения поля при валидации.
+ */
+public class FieldErrorDTO {
+
+ private final String field;
+ private final String message;
+
+ public FieldErrorDTO(String field, String message) {
+ this.field = field;
+ this.message = message;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public String toString() {
+ return "{" +
+ "field='" + field + '\'' +
+ ", message='" + message + '\'' +
+ '}';
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/dto/exception/ValidationErrorDTO.java b/sut-market/market-core/src/main/java/market/dto/exception/ValidationErrorDTO.java
new file mode 100644
index 0000000..2e36d28
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/dto/exception/ValidationErrorDTO.java
@@ -0,0 +1,21 @@
+package market.dto.exception;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Список нашенных при валидации ограничений.
+ */
+public class ValidationErrorDTO {
+
+ private final List fieldErrors = new ArrayList<>();
+
+ public void addFieldError(String path, String message) {
+ FieldErrorDTO error = new FieldErrorDTO(path, message);
+ fieldErrors.add(error);
+ }
+
+ public List getFieldErrors() {
+ return fieldErrors;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/exception/CustomNotValidException.java b/sut-market/market-core/src/main/java/market/exception/CustomNotValidException.java
new file mode 100644
index 0000000..492bf3f
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/exception/CustomNotValidException.java
@@ -0,0 +1,38 @@
+package market.exception;
+
+import org.springframework.validation.FieldError;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ *
+ */
+public class CustomNotValidException extends RuntimeException {
+ private static final long serialVersionUID = 4461435547254498698L;
+
+ private final String eventType;
+ private final String entityType;
+ private final String field;
+
+ public CustomNotValidException(String eventType, String entityType, String field) {
+ super();
+ this.eventType = eventType;
+ this.entityType = entityType;
+ this.field = field;
+ }
+
+ public FieldError getFieldError() {
+ String[] codes = {eventType + "." + field, eventType + "." + entityType + "." + field};
+ Object[] arguments = {field};
+ return new FieldError(entityType, field, "", false, codes, arguments, "");
+ }
+
+ public List getFieldErrors() {
+ return Collections.singletonList(getFieldError());
+ }
+
+ public String getEntityType() {
+ return entityType;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/exception/EmailExistsException.java b/sut-market/market-core/src/main/java/market/exception/EmailExistsException.java
new file mode 100644
index 0000000..9aacd2f
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/exception/EmailExistsException.java
@@ -0,0 +1,11 @@
+package market.exception;
+
+/**
+ * Пользователь с указанным адресом уже существует.
+ */
+public class EmailExistsException extends CustomNotValidException {
+
+ public EmailExistsException(Class> clazz) {
+ super("Exists", clazz.getSimpleName(), "email");
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/exception/EmptyCartException.java b/sut-market/market-core/src/main/java/market/exception/EmptyCartException.java
new file mode 100644
index 0000000..95df36e
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/exception/EmptyCartException.java
@@ -0,0 +1,11 @@
+package market.exception;
+
+/**
+ * Заказ не может быть оформлен: корзина пуста.
+ */
+public class EmptyCartException extends CustomNotValidException {
+
+ public EmptyCartException() {
+ super("NotEmpty", "cart", "items");
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/exception/UnknownEntityException.java b/sut-market/market-core/src/main/java/market/exception/UnknownEntityException.java
new file mode 100644
index 0000000..558d19d
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/exception/UnknownEntityException.java
@@ -0,0 +1,29 @@
+package market.exception;
+
+/**
+ * Попытка сослаться на неизвестный товар.
+ */
+public class UnknownEntityException extends CustomNotValidException {
+ private static final long serialVersionUID = 4827971686664741607L;
+
+ private final String idField;
+ private final long idValue;
+
+ public UnknownEntityException(Class> clazz, long idValue) {
+ this(clazz, "id", idValue);
+ }
+
+ public UnknownEntityException(Class> clazz, String idField, long idValue) {
+ super("NotExist", clazz.getSimpleName(), idField);
+ this.idField = idField;
+ this.idValue = idValue;
+ }
+
+ public String getIdField() {
+ return idField;
+ }
+
+ public long getIdValue() {
+ return idValue;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/properties/MarketProperties.java b/sut-market/market-core/src/main/java/market/properties/MarketProperties.java
new file mode 100644
index 0000000..35d6b0f
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/properties/MarketProperties.java
@@ -0,0 +1,17 @@
+package market.properties;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MarketProperties {
+ private final int deliveryCost;
+
+ public MarketProperties(@Value("${deliveryCost}") int deliveryCost) {
+ this.deliveryCost = deliveryCost;
+ }
+
+ public int getDeliveryCost() {
+ return deliveryCost;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/security/AuthenticationService.java b/sut-market/market-core/src/main/java/market/security/AuthenticationService.java
new file mode 100644
index 0000000..ff2e676
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/security/AuthenticationService.java
@@ -0,0 +1,27 @@
+package market.security;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class AuthenticationService {
+
+ private final AuthenticationManager authenticationManager;
+
+ public AuthenticationService(AuthenticationManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ }
+
+ public boolean authenticate(String login, String password) {
+ try {
+ Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login, password));
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ return auth.isAuthenticated();
+ } catch (BadCredentialsException ex) {
+ // todo
+ return false;
+ }
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/security/UserDetailsServiceImpl.java b/sut-market/market-core/src/main/java/market/security/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..1e73197
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/security/UserDetailsServiceImpl.java
@@ -0,0 +1,58 @@
+package market.security;
+
+import market.domain.Role;
+import market.domain.UserAccount;
+import market.service.UserAccountService;
+import org.springframework.dao.DataAccessException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Реализация сервиса извлечения аккаунта пользователя из БД.
+ */
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ private final UserAccountService userAccountService;
+
+ public UserDetailsServiceImpl(UserAccountService userAccountService) {
+ this.userAccountService = userAccountService;
+ }
+
+ @Override
+ @Transactional
+ public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException, DataAccessException
+ {
+ UserAccount userEntity = userAccountService.findByEmail(login);
+ if (userEntity == null) {
+ throw new UsernameNotFoundException("user not found");
+ }
+ return buildUser(userEntity);
+ }
+
+ private User buildUser(UserAccount account) {
+ String login = account.getEmail();
+ String password = account.getPassword();
+ boolean enabled = account.isActive();
+ boolean accountNonExpired = account.isActive();
+ boolean credentialsNonExpired = account.isActive();
+ boolean accountNonLocked = account.isActive();
+
+ Collection authorities = new ArrayList<>();
+ for (Role role : account.getRoles()) {
+ authorities.add(new SimpleGrantedAuthority(role.getTitle()));
+ }
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+
+ User user = new User(login, password, enabled,
+ accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
+ return user;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/service/CartService.java b/sut-market/market-core/src/main/java/market/service/CartService.java
new file mode 100644
index 0000000..fb21407
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/CartService.java
@@ -0,0 +1,38 @@
+package market.service;
+
+import market.domain.Cart;
+import market.domain.CartItem;
+
+import java.util.List;
+
+public interface CartService {
+
+ /**
+ * Returns existing or creates new cart of the specified user.
+ */
+ Cart getCartOrCreate(String userEmail);
+
+ /**
+ * Adds new item into the specified user cart and saves cart.
+ * @return updated cart
+ */
+ Cart addToCart(String userEmail, long productId, int quantity);
+
+ /**
+ * Adds all the listed items into the specified user cart and saves cart.
+ * @return updated cart
+ */
+ Cart addAllToCart(String userEmail, List itemsToCopy);
+
+ /**
+ * Changes delivery option of the specified user cart.
+ * @return updated cart
+ */
+ Cart setDelivery(String userEmail, boolean deliveryIncluded);
+
+ /**
+ * Clears the specified user cart.
+ * @return updated cart
+ */
+ Cart clearCart(String userEmail);
+}
diff --git a/sut-market/market-core/src/main/java/market/service/ContactsService.java b/sut-market/market-core/src/main/java/market/service/ContactsService.java
new file mode 100644
index 0000000..7f1e757
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/ContactsService.java
@@ -0,0 +1,17 @@
+package market.service;
+
+import market.domain.Contacts;
+
+public interface ContactsService {
+
+ /**
+ * @return contacts of the specified user
+ */
+ Contacts getContacts(String userLogin);
+
+ /**
+ * Updates contacts of the specified user.
+ */
+ Contacts updateUserContacts(Contacts changedContacts, String userLogin);
+
+}
diff --git a/sut-market/market-core/src/main/java/market/service/DistilleryService.java b/sut-market/market-core/src/main/java/market/service/DistilleryService.java
new file mode 100644
index 0000000..4bf4c73
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/DistilleryService.java
@@ -0,0 +1,44 @@
+package market.service;
+
+import market.domain.Distillery;
+import market.domain.Region;
+
+import java.util.List;
+
+public interface DistilleryService {
+
+ /**
+ * @return all the distilleries sorted by title
+ */
+ List findAll();
+
+ /**
+ * @return all the distilleries of the specified region sorted by title
+ */
+ List findByRegion(Region region);
+
+ /**
+ * @return distillery with the specified id
+ */
+ Distillery findById(long distilleryId);
+
+ /**
+ * @return distillery with the specified title
+ */
+ Distillery findByTitle(String title);
+
+ /**
+ * Creates new distillery.
+ */
+ void create(Distillery newDistillery, String regionName);
+
+ /**
+ * Updates existing distillery.
+ */
+ void update(long distilleryId, Distillery changedDistillery, String regionTitle);
+
+ /**
+ * Removes distillery.
+ */
+ void delete(long distilleryId);
+}
diff --git a/sut-market/market-core/src/main/java/market/service/OrderService.java b/sut-market/market-core/src/main/java/market/service/OrderService.java
new file mode 100644
index 0000000..1250c3a
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/OrderService.java
@@ -0,0 +1,42 @@
+package market.service;
+
+import market.domain.Order;
+import market.exception.EmptyCartException;
+import market.exception.UnknownEntityException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OrderService {
+
+ /**
+ * @return all the orders of the specified user
+ */
+ List getUserOrders(String userLogin);
+
+ /**
+ * @return order of the specified user and id
+ * @throws UnknownEntityException if the requested order does not exist
+ */
+ Optional getUserOrder(String userLogin, long orderId);
+
+ /**
+ * @return orders filtered according to the passed parameters
+ */
+ Page fetchFiltered(String executed, String created, PageRequest request);
+
+ /**
+ * Creates new order for the specified user.
+ *
+ * @return newly created order
+ * @throws EmptyCartException if the specified user cart is empty
+ */
+ Order createUserOrder(String userLogin, int deliveryCost, String cardNumber);
+
+ /**
+ * Updates a state of the order with the specified id
+ */
+ void updateStatus(long orderId, boolean executed);
+}
diff --git a/sut-market/market-core/src/main/java/market/service/ProductService.java b/sut-market/market-core/src/main/java/market/service/ProductService.java
new file mode 100644
index 0000000..44a6a3b
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/ProductService.java
@@ -0,0 +1,73 @@
+package market.service;
+
+import market.domain.Distillery;
+import market.domain.Product;
+import market.domain.Region;
+import market.exception.UnknownEntityException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public interface ProductService {
+
+ /**
+ * @return all the products, sorted by title
+ */
+ List findAll();
+
+ /**
+ * @return all the products, sorted by title and paged
+ */
+ Page findAll(PageRequest request);
+
+ /**
+ * @return all the products of the specified distillery, sorted by title
+ */
+ Page findByDistillery(Distillery distillery, PageRequest request);
+
+ /**
+ * @return all the products of the specified region, sorted by title
+ */
+ Page findByRegion(Region region, PageRequest request);
+
+ /**
+ * @return all the available products, sorted by title
+ */
+ Page findByAvailability(String available, PageRequest request);
+
+ /**
+ * @return product with the specified id
+ * @throws UnknownEntityException if product does not exist
+ */
+ Product getProduct(long productId);
+
+ /**
+ * @return product with the specified id
+ */
+ Optional findById(long productId);
+
+ /**
+ * Creates new product.
+ */
+ void create(Product product, String distilleryTitle);
+
+ /**
+ * Updates existing product.
+ *
+ * @throws UnknownEntityException if product does not exist
+ */
+ void update(long productId, Product product, String distilleryTitle);
+
+ /**
+ * Updates availability of the specified product.
+ */
+ void updateAvailability(Map> productIdsByAvailability);
+
+ /**
+ * Removes distillery.
+ */
+ void delete(long product);
+}
diff --git a/sut-market/market-core/src/main/java/market/service/RegionService.java b/sut-market/market-core/src/main/java/market/service/RegionService.java
new file mode 100644
index 0000000..136cb0c
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/RegionService.java
@@ -0,0 +1,39 @@
+package market.service;
+
+import market.domain.Region;
+
+import java.util.List;
+
+public interface RegionService {
+
+ /**
+ * @return all the existing regions sorted by region name
+ */
+ List findAll();
+
+ /**
+ * @return region with the specified id
+ */
+ Region findOne(long regionId);
+
+ /**
+ * @return region with the specified name
+ */
+ Region findByName(String regionName);
+
+ /**
+ * Creates new region.
+ */
+ void create(Region newRegion);
+
+ /**
+ * Updates existing region.
+ */
+ void update(long regionId, Region changedRegion);
+
+ /**
+ * Removes region.
+ */
+ void delete(long regionId);
+
+}
diff --git a/sut-market/market-core/src/main/java/market/service/TestService.java b/sut-market/market-core/src/main/java/market/service/TestService.java
new file mode 100644
index 0000000..86c47cc
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/TestService.java
@@ -0,0 +1,10 @@
+package market.service;
+
+import java.sql.SQLException;
+
+// Add a service for test: clean the database (deleteAll) and get all data (getAll)
+
+public interface TestService {
+ public void deleteAll() throws SQLException;
+ public Object getAll();
+}
diff --git a/sut-market/market-core/src/main/java/market/service/UserAccountService.java b/sut-market/market-core/src/main/java/market/service/UserAccountService.java
new file mode 100644
index 0000000..ae0394a
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/UserAccountService.java
@@ -0,0 +1,20 @@
+package market.service;
+
+import market.domain.UserAccount;
+import market.exception.EmailExistsException;
+
+public interface UserAccountService {
+
+ /**
+ * @return user account associated with the specified email
+ */
+ UserAccount findByEmail(String email);
+
+ /**
+ * Creates new account.
+ * @return newly created account
+ * @throws EmailExistsException if some account is already associated with the specified email
+ */
+ UserAccount create(UserAccount userAccount);
+
+}
diff --git a/sut-market/market-core/src/main/java/market/service/impl/CartServiceImpl.java b/sut-market/market-core/src/main/java/market/service/impl/CartServiceImpl.java
new file mode 100644
index 0000000..e9f57c5
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/CartServiceImpl.java
@@ -0,0 +1,91 @@
+package market.service.impl;
+
+import market.dao.CartDAO;
+import market.domain.Cart;
+import market.domain.CartItem;
+import market.domain.Product;
+import market.domain.UserAccount;
+import market.service.CartService;
+import market.service.ProductService;
+import market.service.UserAccountService;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class CartServiceImpl implements CartService {
+ private static final Logger log = LogManager.getLogger(CartServiceImpl.class);
+
+ private final CartDAO cartDAO;
+ private final UserAccountService userAccountService;
+ private final ProductService productService;
+
+ public CartServiceImpl(CartDAO cartDAO, UserAccountService userAccountService, ProductService productService) {
+ this.cartDAO = cartDAO;
+ this.userAccountService = userAccountService;
+ this.productService = productService;
+ }
+
+ @Transactional(propagation = Propagation.SUPPORTS)
+ @Override
+ public Cart getCartOrCreate(String userEmail) {
+ UserAccount account = userAccountService.findByEmail(userEmail); // todo: check if this user exists
+ Optional cartOptional = cartDAO.findById(account.getId());
+ return cartOptional.orElseGet(() -> createCart(account));
+ }
+
+ private Cart createCart(UserAccount account) {
+ if (log.isDebugEnabled())
+ log.debug("Creating new cart for account #" + account.getId());
+ return cartDAO.save(new Cart(account));
+ }
+
+ @Transactional
+ @Override
+ public Cart addToCart(String userEmail, long productId, int quantity) {
+ Cart cart = getCartOrCreate(userEmail);
+ Product product = productService.getProduct(productId);
+ if (product.isAvailable()) {
+ cart.update(product, quantity);
+ return cartDAO.save(cart);
+ } else {
+ return cart;
+ }
+ }
+
+ @Transactional
+ @Override
+ public Cart addAllToCart(String userEmail, List itemsToAdd) {
+ Cart cart = getCartOrCreate(userEmail);
+ boolean updated = false;
+ for (CartItem item : itemsToAdd) {
+ Optional product = productService.findById(item.getProduct().getId());
+ if (product.isPresent() && product.get().isAvailable()) {
+ cart.update(product.get(), item.getQuantity());
+ updated = true;
+ }
+ }
+ return updated ? cartDAO.save(cart) : cart;
+ }
+
+ @Transactional
+ @Override
+ public Cart setDelivery(String userEmail, boolean deliveryIncluded) {
+ Cart cart = getCartOrCreate(userEmail);
+ cart.setDeliveryIncluded(deliveryIncluded);
+ return cartDAO.save(cart);
+ }
+
+ @Transactional
+ @Override
+ public Cart clearCart(String userEmail) {
+ Cart cart = getCartOrCreate(userEmail);
+ cart.clear();
+ return cartDAO.save(cart);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/service/impl/ContactsServiceImpl.java b/sut-market/market-core/src/main/java/market/service/impl/ContactsServiceImpl.java
new file mode 100644
index 0000000..4b74127
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/ContactsServiceImpl.java
@@ -0,0 +1,41 @@
+package market.service.impl;
+
+import market.dao.ContactsDAO;
+import market.domain.Contacts;
+import market.domain.UserAccount;
+import market.exception.CustomNotValidException;
+import market.service.ContactsService;
+import market.service.UserAccountService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class ContactsServiceImpl implements ContactsService {
+ private final ContactsDAO contactsDAO;
+ private final UserAccountService userAccountService;
+
+ public ContactsServiceImpl(ContactsDAO contactsDAO, UserAccountService userAccountService) {
+ this.contactsDAO = contactsDAO;
+ this.userAccountService = userAccountService;
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Contacts getContacts(String userLogin) {
+ UserAccount account = userAccountService.findByEmail(userLogin);
+ return contactsDAO.findByUserAccount(account);
+ }
+
+ @Transactional
+ @Override
+ public Contacts updateUserContacts(Contacts changedContacts, String userLogin) {
+ Contacts originalContacts = getContacts(userLogin);
+ if (originalContacts == null)
+ throw new CustomNotValidException("NotExist", "userLogin", "userLogin"); // todo: some custom exception
+
+ originalContacts.setPhone(changedContacts.getPhone());
+ originalContacts.setAddress(changedContacts.getAddress());
+ contactsDAO.save(originalContacts);
+ return originalContacts;
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/service/impl/DbTestUtil.java b/sut-market/market-core/src/main/java/market/service/impl/DbTestUtil.java
new file mode 100644
index 0000000..0aa786e
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/DbTestUtil.java
@@ -0,0 +1,29 @@
+package market.service.impl;
+
+import org.springframework.context.ApplicationContext;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/*
+ * New class for test: reset the autoincrement sequences in tables of database
+ */
+public final class DbTestUtil {
+
+ private DbTestUtil() {}
+
+ public static void resetAutoIncrementColumns(ApplicationContext applicationContext,
+ String... tableNames) throws SQLException {
+ DataSource dataSource = applicationContext.getBean(DataSource.class);
+ Connection dbConnection = dataSource.getConnection();
+
+ //Create and invoke SQL statements that reset the auto increment columns
+ for (String resetSqlArgument: tableNames) {
+ Statement statement = dbConnection.createStatement();
+ String resetSql = "TRUNCATE TABLE "+resetSqlArgument+" RESTART IDENTITY";
+ statement.execute(resetSql);
+ }
+ dbConnection.close();
+ }
+ }
\ No newline at end of file
diff --git a/sut-market/market-core/src/main/java/market/service/impl/DistilleryServiceImpl.java b/sut-market/market-core/src/main/java/market/service/impl/DistilleryServiceImpl.java
new file mode 100644
index 0000000..db0d4c7
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/DistilleryServiceImpl.java
@@ -0,0 +1,80 @@
+package market.service.impl;
+
+import market.dao.DistilleryDAO;
+import market.domain.Distillery;
+import market.domain.Region;
+import market.service.DistilleryService;
+import market.service.RegionService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class DistilleryServiceImpl implements DistilleryService {
+ private final RegionService regionService;
+ private final DistilleryDAO distilleryDAO;
+
+ public DistilleryServiceImpl(RegionService regionService, DistilleryDAO distilleryDAO) {
+ this.regionService = regionService;
+ this.distilleryDAO = distilleryDAO;
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public List findAll() {
+ return distilleryDAO.findAll().stream()
+ .sorted(Comparator.comparing(Distillery::getTitle))
+ .collect(Collectors.toList());
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public List findByRegion(Region region) {
+ return distilleryDAO.findByRegionOrderByTitleAsc(region);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Distillery findById(long distilleryId) {
+ return distilleryDAO.findById(distilleryId).orElse(null);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Distillery findByTitle(String title) {
+ return distilleryDAO.findByTitle(title);
+ }
+
+ @Transactional
+ @Override
+ public void create(Distillery newDistillery, String regionName) {
+ saveInternal(newDistillery, regionName);
+ }
+
+ @Override
+ public void update(long distilleryId, Distillery changedDistillery, String regionName) {
+ Optional originalDistillery = distilleryDAO.findById(distilleryId);
+ if (originalDistillery.isPresent()) {
+ changedDistillery.setId(originalDistillery.get().getId());
+ saveInternal(changedDistillery, regionName);
+ }
+ }
+
+ private void saveInternal(Distillery distillery, String regionName) {
+ Region region = regionService.findByName(regionName);
+ if (region != null) {
+ distillery.setRegion(region);
+ distilleryDAO.save(distillery);
+ }
+ }
+
+ @Transactional
+ @Override
+ public void delete(long distilleryId) {
+ distilleryDAO.deleteById(distilleryId);
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/service/impl/OrderServiceImpl.java b/sut-market/market-core/src/main/java/market/service/impl/OrderServiceImpl.java
new file mode 100644
index 0000000..a48199b
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/OrderServiceImpl.java
@@ -0,0 +1,143 @@
+package market.service.impl;
+
+import market.dao.OrderDAO;
+import market.domain.Bill;
+import market.domain.Cart;
+import market.domain.CartItem;
+import market.domain.Order;
+import market.domain.OrderedProduct;
+import market.domain.UserAccount;
+import market.exception.EmptyCartException;
+import market.service.CartService;
+import market.service.OrderService;
+import market.service.UserAccountService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toSet;
+
+@Service
+public class OrderServiceImpl implements OrderService {
+
+ private final OrderDAO orderDAO;
+ private final UserAccountService userAccountService;
+ private final CartService cartService;
+
+ public OrderServiceImpl(OrderDAO orderDAO, UserAccountService userAccountService, CartService cartService) {
+ this.orderDAO = orderDAO;
+ this.userAccountService = userAccountService;
+ this.cartService = cartService;
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public List getUserOrders(String userLogin) {
+ UserAccount account = userAccountService.findByEmail(userLogin);
+ return orderDAO.findByUserAccountOrderByDateCreatedDesc(account);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Optional getUserOrder(String userLogin, long orderId) {
+ // todo: add user check
+ return orderDAO.findById(orderId);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Page fetchFiltered(String executed, String orderAgeInDays, PageRequest request) {
+ Date startTime = new Date();
+ if (!"all".equals(orderAgeInDays)) {
+ int days = Integer.parseInt(orderAgeInDays);
+ Calendar c = Calendar.getInstance();
+ c.setTime(new Date());
+ c.add(Calendar.HOUR_OF_DAY, -(days * 24));
+ startTime = c.getTime();
+ }
+ if (!"all".equals(executed) && !"all".equals(orderAgeInDays)) {
+ boolean executedState = Boolean.parseBoolean(executed);
+ return orderDAO.findByExecutedAndDateCreatedGreaterThan(executedState, startTime, request);
+ } else if (!"all".equals(executed)) {
+ boolean executedState = Boolean.parseBoolean(executed);
+ return orderDAO.findByExecuted(executedState, request);
+ } else if (!"all".equals(orderAgeInDays)) {
+ return orderDAO.findByDateCreatedGreaterThan(startTime, request);
+ } else {
+ return orderDAO.findAll(request);
+ }
+ }
+
+ @Transactional
+ @Override
+ public Order createUserOrder(String userLogin, int deliveryCost, String cardNumber) {
+ Cart cart = cartService.getCartOrCreate(userLogin);
+ if (cart.isEmpty())
+ throw new EmptyCartException();
+
+ Order order = createNewOrder(userLogin, cart, deliveryCost);
+ Bill bill = createBill(order, cardNumber);
+ order.setBill(bill);
+ orderDAO.saveAndFlush(order);
+
+ fillOrderItems(cart, order);
+ orderDAO.save(order);
+ cartService.clearCart(userLogin);
+
+ return order;
+ }
+
+ @Override
+ public void updateStatus(long orderId, boolean executed) {
+ Order order = orderDAO.findById(orderId).orElse(null);
+ if (order != null) {
+ order.setExecuted(executed);
+ orderDAO.save(order);
+ }
+ }
+
+ private Order createNewOrder(String userLogin, Cart cart, int deliveryCost) {
+ return new Order.Builder()
+ .setDeliveryIncluded(cart.isDeliveryIncluded())
+ .setDeliveryCost(cart.isDeliveryIncluded() ? deliveryCost : 0)
+ .setUserAccount(userAccountService.findByEmail(userLogin))
+ .setProductsCost(cart.getItemsCost())
+ .setDateCreated(new Date())
+ .setExecuted(false)
+ .build();
+ }
+
+ private Bill createBill(Order order, String cardNumber) {
+ return new Bill.Builder()
+ .setOrder(order)
+ .setNumber(new Random().nextInt(999999999))
+ .setTotalCost(order.getProductsCost() + order.getDeliveryCost())
+ .setPayed(true)
+ .setDateCreated(new Date())
+ .setCcNumber(cardNumber)
+ .build();
+ }
+
+ private void fillOrderItems(Cart cart, Order order) {
+ Set ordered = cart.getCartItems().stream()
+ .map(item -> createOrderedProduct(order, item))
+ .collect(toSet());
+ order.setOrderedProducts(ordered);
+ }
+
+ private OrderedProduct createOrderedProduct(Order order, CartItem item) {
+ return new OrderedProduct.Builder()
+ .setProduct(item.getProduct())
+ .setOrder(order)
+ .setQuantity(item.getQuantity())
+ .build();
+ }
+}
diff --git a/sut-market/market-core/src/main/java/market/service/impl/ProductServiceImpl.java b/sut-market/market-core/src/main/java/market/service/impl/ProductServiceImpl.java
new file mode 100644
index 0000000..2cab3f2
--- /dev/null
+++ b/sut-market/market-core/src/main/java/market/service/impl/ProductServiceImpl.java
@@ -0,0 +1,130 @@
+package market.service.impl;
+
+import market.dao.ProductDAO;
+import market.domain.Distillery;
+import market.domain.Product;
+import market.domain.Region;
+import market.exception.UnknownEntityException;
+import market.service.DistilleryService;
+import market.service.ProductService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class ProductServiceImpl implements ProductService {
+
+ private final ProductDAO productDAO;
+ private final DistilleryService distilleryService;
+
+ public ProductServiceImpl(ProductDAO productDAO, DistilleryService distilleryService) {
+ this.productDAO = productDAO;
+ this.distilleryService = distilleryService;
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public List findAll() {
+ return productDAO.findAll().stream()
+ .sorted(Comparator.comparing(Product::getName))
+ .collect(Collectors.toList());
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Page findAll(PageRequest request) {
+ return productDAO.findAll(request);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Page findByDistillery(Distillery distillery, PageRequest request) {
+ return productDAO.findByDistilleryOrderByName(distillery, request);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Page findByRegion(Region region, PageRequest request) {
+ return productDAO.findByRegionOrderByName(region, request);
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Page findByAvailability(String available, PageRequest request) {
+ Page pagedList;
+ if ("all".equals(available)) {
+ pagedList = productDAO.findAll(request);
+ } else {
+ boolean availability = Boolean.parseBoolean(available);
+ pagedList = productDAO.findByAvailableOrderByName(availability, request);
+ }
+ return pagedList;
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Product getProduct(long productId) {
+ return productDAO.findById(productId)
+ .orElseThrow(() -> new UnknownEntityException(Product.class, productId));
+ }
+
+ @Transactional(readOnly = true)
+ @Override
+ public Optional