commit 05522dd0c3d98f86c24dddf3b8e6693447cf4a7f Author: Anastasia Tarazevich Date: Mon Feb 23 17:50:23 2026 +0300 feat: Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c57e64 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# НТО 2025. II отборочный этап. Командные задания — Backend Решение +## 📖 Предыстория + +В компании S есть возможность бронирования мест в пространствах, предназначенных под общее использование (open-space). На данный момент для бронирования места используются различные способы бронирования, разработанные в каждом офисе индивидуально. +Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра забронированных мест. + +## 📑 Технологический стек + +- Java 17 +- Spring Boot +- H2 +- Liquibase + + +## 🛠️ Техническое задание + +Требуется разработать серверное приложение на Java с использованием Spring Boot, которое работает на основе протокола HTTP и взаимодействует с клиентами благодаря RESTful API. + +Для хранения данных о сотрудниках и их посещениях должна использоваться реляционная база данных (H2). Схема БД должна создаваться liquibase-скриптами. ID-поля всех сущностей, сохраняемых в базе, должны выдаваться на уровне БД. Стратегия генерации ID - автоинкремент (1, 2, 3, 4…) + +Сотрудникам не нужно регистрироваться, все данные в базе должны быть предзаполнены liquibase-скриптами при запуске серверного приложения. Данные для предзаполнения таблиц представлены ниже. + +Картинки для аватаров пользователей не должны храниться в БД. Должны быть сохранены лишь URL-адреса на ресурсы, откуда в последующем мобильное приложение загрузит соответствующее изображение. + +Сервер разрабатывается на основе предоставляемой заготовки проекта. Версии зависимостей и сами зависимости изменяться не должны. + + +## 📂 Правила работы с проектом-заготовкой + +В предоставленном проекте необходимо изучить, но никак не модифицировать, не перемещать и не удалять следующие файлы: +- `pom.xml` +- `application.yml` +- все файлы из `db.changelog` + +Кроме описанных выше файлов, в проекте уже созданы основные классы-сущности и добавлены пустые классы всех слоев. В этих классах необходимо будет написать программный код и добавить аннотации для реализации описанного задания. Наименования классов и прочий код уже написанный в предоставляемом проекте **изменять/удалять не нужно, необходимо их доработать**. Добавлять свои дополнительные классы в проект можно. + +Создание таблиц в БД и предзаполнение их требуемыми данными уже реализовано в заготовке при помощи liquibase. В одной из сущностей добавлены аннотации для реализации связи “один ко многим”, обратите внимание, что в проекте потребуется еще связь данного типа. + + +## 🌐 Где необходимо разместить сервер + +Серверное приложение должно быть разработано и протестировано локально (**не требуется** размещать сервер удаленно и осуществлять его функционирование 24/7). + + +## 📋 Технические требования к серверу и его ответам клиенту + +Для конфигурирования Вашего сервера (его хоста/IP адреса) используйте константы из файла `Constants.kt`. + +| **Тип запроса** | **Путь** | **Описание** | **Параметры/Тело** | **Ответы** | +|-----------------|-----------------------|---------------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **GET** | `api//auth` | Проверка авторизации | `` - код для входа | `400` - что-то пошло не так
`401` - кода не существует
`200` - данный код существует - можно пользоваться приложением | +| **GET** | `api//info` | Получение информации о пользователе | `` - код для входа | `400` - что-то пошло не так
`401` - кода не существует
`200` - ОК
{
"name":"Иванов Петр Федорович",
"photoUrl":"",
"booking":{
"2025-01-05": {"id":1,"place":"102"},
"2025-01-06": {"id":2,"place":"209.13"},
"2025-01-09": {"id":3,"place":"Зона 51. 50"}
}
}
| +| **GET** | `api//booking` | Получение доступных для бронирования мест | `` - код для входа | `400` - что-то пошло не так
`401` - кода не существует
`200` - ОК
{
"2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-06": [{"id": 3, "place": "Зона 51. 50"}],
"2025-01-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-08": [{"id": 2, "place": "209.13"}]
}
**Список дат ограничен текущим + 3 днями** (ответ от сервера содержит 4 дня со свободными местами для каждого) +| **POST** | `api//book` | Создание нового бронирования | `` - код для входа
Тело:
{
“date”: “2025-01-05”,
“placeId”: 1
}
|`400` - что-то пошло не так
`401` - кода не существует
`409` - уже забронировано
`201` - бронирование успешно создано + + +## 📊 Пример данных + +Таблица сотрудников: + +| **id** | **name** | **code** | **photo_url** | +|--------|-----------|---------------------------------|---------------------------------------------------------------------------------------------| +| 1 | Ivanov Ivan | 1111 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg | +| 2 | Petrov Petr | 2222 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg | +| 3 | Kozlov Oleg | 3333 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg | +| 4 | Smirnova Anna | 4444 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg | + + +Таблица мест для бронирования: + +| **id** | **place_name** | +|--------|-----------------| +| 1 | K-19 | +| 2 | M-16 | +| 3 | T-1 | + + +Таблица бронирований: + +| **id** | **date** | **place_id** | **employee_id** | +|--------|-----------|--------------|------------------| +| 1 | 2025-11-08 | 1 | 1 | +| 2 | 2025-11-10 | 2 | 2 | + + +# 📝 Решение + +Работу необходимо осуществлять в предоставленном проекте-заготовке (шаблоне). +Когда завершите разработку, создайте пулреквест и запустите workflow в учебной системе. + + +## ✅ Особенности оценивания + +При тестировании сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. +Сервер и приложение тестируются независимо. + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..93eee07 --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + org.example + NTO-2025-Backend-Team-Task + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-web + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.liquibase + liquibase-core + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.8 + + + + \ No newline at end of file diff --git a/src/main/java/com/example/nto/App.java b/src/main/java/com/example/nto/App.java new file mode 100644 index 0000000..d4add94 --- /dev/null +++ b/src/main/java/com/example/nto/App.java @@ -0,0 +1,11 @@ +package com.example.nto; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/src/main/java/com/example/nto/controller/BookingController.java b/src/main/java/com/example/nto/controller/BookingController.java new file mode 100644 index 0000000..a4ed8b1 --- /dev/null +++ b/src/main/java/com/example/nto/controller/BookingController.java @@ -0,0 +1,35 @@ +package com.example.nto.controller; + +import com.example.nto.controller.dto.BookingCreateDto; +import com.example.nto.controller.dto.PlaceDto; +import com.example.nto.service.BookingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Validated +@RestController +@RequestMapping("api") +@RequiredArgsConstructor +public class BookingController { + + private final BookingService bookingService; + + @GetMapping("/{code}/booking") + @ResponseStatus(code = HttpStatus.OK) + public Map> getByDate(@PathVariable String code) { + return bookingService.getFreePlace(code); + } + + @PostMapping("/{code}/book") + @ResponseStatus(code = HttpStatus.CREATED) + public void create(@PathVariable String code, @RequestBody BookingCreateDto bookingCreateDto) { + bookingService.create(code, bookingCreateDto); + } + +} diff --git a/src/main/java/com/example/nto/controller/EmployeeController.java b/src/main/java/com/example/nto/controller/EmployeeController.java new file mode 100644 index 0000000..7f73702 --- /dev/null +++ b/src/main/java/com/example/nto/controller/EmployeeController.java @@ -0,0 +1,29 @@ +package com.example.nto.controller; + + +import com.example.nto.controller.dto.EmployeeDto; +import com.example.nto.service.EmployeeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("api") +@RequiredArgsConstructor +public class EmployeeController { + + private final EmployeeService employeeService; + + @GetMapping("/{code}/auth") + @ResponseStatus(code = HttpStatus.OK) + public void login(@PathVariable String code) { + employeeService.auth(code); + } + + @GetMapping("/{code}/info") + @ResponseStatus(code = HttpStatus.OK) + public EmployeeDto getByCode(@PathVariable String code) { + return employeeService.getByCode(code); + } + +} diff --git a/src/main/java/com/example/nto/controller/dto/BookingCreateDto.java b/src/main/java/com/example/nto/controller/dto/BookingCreateDto.java new file mode 100644 index 0000000..e2b7ddd --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/BookingCreateDto.java @@ -0,0 +1,21 @@ +package com.example.nto.controller.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BookingCreateDto { + @NotNull + private LocalDate date; + @Positive + private long placeId; +} diff --git a/src/main/java/com/example/nto/controller/dto/EmployeeDto.java b/src/main/java/com/example/nto/controller/dto/EmployeeDto.java new file mode 100644 index 0000000..3c87566 --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/EmployeeDto.java @@ -0,0 +1,31 @@ +package com.example.nto.controller.dto; + +import com.example.nto.entity.Booking; +import com.example.nto.entity.Employee; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.Map; +import java.util.TreeMap; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EmployeeDto { + private String name; + private String photoUrl; + private Map booking; + + public static EmployeeDto toDto(Employee employee) { + Map dtoTreeMap = new TreeMap<>(); + for (Booking booking : employee.getBookingList()) { + dtoTreeMap.put(booking.getDate(), PlaceDto.toDto(booking.getPlace())); + } + + return new EmployeeDto(employee.getName(), employee.getPhotoUrl(), dtoTreeMap); + } +} diff --git a/src/main/java/com/example/nto/controller/dto/PlaceDto.java b/src/main/java/com/example/nto/controller/dto/PlaceDto.java new file mode 100644 index 0000000..40dfda1 --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/PlaceDto.java @@ -0,0 +1,20 @@ +package com.example.nto.controller.dto; + +import com.example.nto.entity.Place; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PlaceDto { + private long id; + private String place; + + public static PlaceDto toDto(Place place) { + return new PlaceDto(place.getId(), place.getPlace()); + } +} diff --git a/src/main/java/com/example/nto/entity/Booking.java b/src/main/java/com/example/nto/entity/Booking.java new file mode 100644 index 0000000..a208eb9 --- /dev/null +++ b/src/main/java/com/example/nto/entity/Booking.java @@ -0,0 +1,33 @@ +package com.example.nto.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "booking") +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "date") + private LocalDate date; + + @ManyToOne(targetEntity = Place.class, fetch = FetchType.LAZY) + @JoinColumn(name = "place_id") + private Place place; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "employee_id") + private Employee employee; +} diff --git a/src/main/java/com/example/nto/entity/Employee.java b/src/main/java/com/example/nto/entity/Employee.java new file mode 100644 index 0000000..e854a92 --- /dev/null +++ b/src/main/java/com/example/nto/entity/Employee.java @@ -0,0 +1,34 @@ +package com.example.nto.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "name") + private String name; + + @Column(name = "code") + private String code; + + @Column(name = "photo_url") + private String photoUrl; + + @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List bookingList; +} diff --git a/src/main/java/com/example/nto/entity/Place.java b/src/main/java/com/example/nto/entity/Place.java new file mode 100644 index 0000000..c266212 --- /dev/null +++ b/src/main/java/com/example/nto/entity/Place.java @@ -0,0 +1,23 @@ +package com.example.nto.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "place") +public class Place { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(name = "place_name") + private String place; +} diff --git a/src/main/java/com/example/nto/exception/BookingAlreadyExistsException.java b/src/main/java/com/example/nto/exception/BookingAlreadyExistsException.java new file mode 100644 index 0000000..019136d --- /dev/null +++ b/src/main/java/com/example/nto/exception/BookingAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class BookingAlreadyExistsException extends RuntimeException { + public BookingAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/EmployeeNotFoundException.java b/src/main/java/com/example/nto/exception/EmployeeNotFoundException.java new file mode 100644 index 0000000..d427077 --- /dev/null +++ b/src/main/java/com/example/nto/exception/EmployeeNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class EmployeeNotFoundException extends RuntimeException { + public EmployeeNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/PlaceNotFoundException.java b/src/main/java/com/example/nto/exception/PlaceNotFoundException.java new file mode 100644 index 0000000..2560027 --- /dev/null +++ b/src/main/java/com/example/nto/exception/PlaceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class PlaceNotFoundException extends RuntimeException { + public PlaceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..234a158 --- /dev/null +++ b/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +package com.example.nto.exception.handler; + +import com.example.nto.exception.BookingAlreadyExistsException; +import com.example.nto.exception.EmployeeNotFoundException; +import com.example.nto.exception.PlaceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(EmployeeNotFoundException.class) + public ResponseEntity handleEmployeeNotFoundException(EmployeeNotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(BookingAlreadyExistsException.class) + public ResponseEntity handleBookingAlreadyExistsException(BookingAlreadyExistsException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT); + } + + @ExceptionHandler(PlaceNotFoundException.class) + public ResponseEntity handlePlaceNotFoundException(PlaceNotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/example/nto/repository/BookingRepository.java b/src/main/java/com/example/nto/repository/BookingRepository.java new file mode 100644 index 0000000..7d4093b --- /dev/null +++ b/src/main/java/com/example/nto/repository/BookingRepository.java @@ -0,0 +1,18 @@ +package com.example.nto.repository; + +import com.example.nto.entity.Booking; +import com.example.nto.entity.Employee; +import com.example.nto.entity.Place; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface BookingRepository extends JpaRepository { + List findByDateBetween(LocalDate start, LocalDate end); + + Optional findByDateAndPlace(LocalDate date, Place place); + + Optional findByDateAndEmployee(LocalDate date, Employee employee); +} diff --git a/src/main/java/com/example/nto/repository/EmployeeRepository.java b/src/main/java/com/example/nto/repository/EmployeeRepository.java new file mode 100644 index 0000000..d845a04 --- /dev/null +++ b/src/main/java/com/example/nto/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.nto.repository; + +import com.example.nto.entity.Employee; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EmployeeRepository extends JpaRepository { + @EntityGraph(attributePaths = {"bookingList", "bookingList.place"}) + Optional findByCode(String code); +} diff --git a/src/main/java/com/example/nto/repository/PlaceRepository.java b/src/main/java/com/example/nto/repository/PlaceRepository.java new file mode 100644 index 0000000..e7b84c9 --- /dev/null +++ b/src/main/java/com/example/nto/repository/PlaceRepository.java @@ -0,0 +1,7 @@ +package com.example.nto.repository; + +import com.example.nto.entity.Place; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlaceRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/nto/service/BookingService.java b/src/main/java/com/example/nto/service/BookingService.java new file mode 100644 index 0000000..64e6ac6 --- /dev/null +++ b/src/main/java/com/example/nto/service/BookingService.java @@ -0,0 +1,15 @@ +package com.example.nto.service; + +import com.example.nto.controller.dto.BookingCreateDto; +import com.example.nto.controller.dto.PlaceDto; +import com.example.nto.entity.Booking; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +public interface BookingService { + Map> getFreePlace(String code); + + Booking create(String code, BookingCreateDto bookingCreateDto); +} diff --git a/src/main/java/com/example/nto/service/EmployeeService.java b/src/main/java/com/example/nto/service/EmployeeService.java new file mode 100644 index 0000000..83144af --- /dev/null +++ b/src/main/java/com/example/nto/service/EmployeeService.java @@ -0,0 +1,9 @@ +package com.example.nto.service; + +import com.example.nto.controller.dto.EmployeeDto; + +public interface EmployeeService { + EmployeeDto getByCode(String code); + + void auth(String code); +} diff --git a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java new file mode 100644 index 0000000..ffc4f86 --- /dev/null +++ b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java @@ -0,0 +1,105 @@ +package com.example.nto.service.impl; + +import com.example.nto.controller.dto.BookingCreateDto; +import com.example.nto.controller.dto.PlaceDto; +import com.example.nto.entity.Booking; +import com.example.nto.entity.Employee; +import com.example.nto.entity.Place; +import com.example.nto.exception.BookingAlreadyExistsException; +import com.example.nto.exception.EmployeeNotFoundException; +import com.example.nto.exception.PlaceNotFoundException; +import com.example.nto.repository.BookingRepository; +import com.example.nto.repository.EmployeeRepository; +import com.example.nto.repository.PlaceRepository; +import com.example.nto.service.BookingService; +import com.example.nto.service.EmployeeService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BookingServiceImpl implements BookingService { + + private final BookingRepository bookingRepository; + private final EmployeeRepository employeeRepository; + private final PlaceRepository placeRepository; + private final EmployeeService employeeService; + + @Value("${booking.days-ahead}") + private int daysAhead; + + @Override + @Transactional(readOnly = true) + public Map> getFreePlace(String code) { + employeeService.auth(code); + + List allPlaces = placeRepository.findAll(); + + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + LocalDate end = today.plusDays(daysAhead); + + List bookings = bookingRepository.findByDateBetween(today, end); + + Map> busyByDate = bookings.stream() + .collect(Collectors.groupingBy( + Booking::getDate, + Collectors.mapping(b -> b.getPlace().getId(), Collectors.toSet()) + )); + + Map> result = new LinkedHashMap<>(); + + for (int i = 0; i <= daysAhead; i++) { + LocalDate currentDate = today.plusDays(i); + Set busyPlaces = busyByDate.getOrDefault(currentDate, Collections.emptySet()); + + List freePlaces = allPlaces.stream() + .filter(place -> !busyPlaces.contains(place.getId())) + .map(place -> new PlaceDto(place.getId(), place.getPlace())) + .toList(); + + result.put(currentDate, freePlaces); + } + + return result; + } + + @Override + @Transactional + public Booking create(String code, BookingCreateDto bookingCreateDto) { + LocalDate date = bookingCreateDto.getDate(); + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + if (date.isBefore(today) || date.isAfter(today.plusDays(daysAhead))) { + throw new IllegalArgumentException("Date is out of booking window"); + } + + Employee employee = employeeRepository.findByCode(code) + .orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!")); + + long placeId = bookingCreateDto.getPlaceId(); + Place place = placeRepository.findById(placeId) + .orElseThrow(() -> new PlaceNotFoundException("Place with " + placeId + " id not found!")); + + if (bookingRepository.findByDateAndPlace(date, place).isPresent()) { + throw new BookingAlreadyExistsException("Booking already exists"); + } + + if (bookingRepository.findByDateAndEmployee(date, employee).isPresent()) { + throw new BookingAlreadyExistsException("This employee already has another booking on " + date); + } + + Booking booking = Booking.builder() + .date(date) + .employee(employee) + .place(place) + .build(); + + return bookingRepository.save(booking); + } +} diff --git a/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java b/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java new file mode 100644 index 0000000..3085dc2 --- /dev/null +++ b/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java @@ -0,0 +1,31 @@ +package com.example.nto.service.impl; + +import com.example.nto.controller.dto.EmployeeDto; +import com.example.nto.exception.EmployeeNotFoundException; +import com.example.nto.repository.EmployeeRepository; +import com.example.nto.service.EmployeeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class EmployeeServiceImpl implements EmployeeService { + + private final EmployeeRepository employeeRepository; + + @Override + @Transactional(readOnly = true) + public EmployeeDto getByCode(String code) { + return employeeRepository.findByCode(code).map(EmployeeDto::toDto) + .orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!")); + } + + @Override + @Transactional(readOnly = true) + public void auth(String code) { + if (employeeRepository.findByCode(code).isEmpty()) { + throw new EmployeeNotFoundException("Employee with " + code + " code not found!"); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..8b68191 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,22 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + + h2: + console: + enabled: true + + jpa: + generate-ddl: false + + hibernate: + ddl-auto: none + + show-sql: true + + liquibase: + enabled: true + change-log: classpath:db.changelog/db.changelog-master.xml + +booking: + days-ahead: 3 \ No newline at end of file diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml new file mode 100644 index 0000000..d1f92f8 --- /dev/null +++ b/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml new file mode 100644 index 0000000..db4a2b2 --- /dev/null +++ b/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml new file mode 100644 index 0000000..fa62dce --- /dev/null +++ b/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml new file mode 100644 index 0000000..40f8ddb --- /dev/null +++ b/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml new file mode 100644 index 0000000..e5351dd --- /dev/null +++ b/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml new file mode 100644 index 0000000..eea0c6b --- /dev/null +++ b/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv new file mode 100644 index 0000000..87ddc6b --- /dev/null +++ b/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv @@ -0,0 +1,5 @@ +name;code;photo_url +Ivanov Ivan;1111;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg +Petrov Petr;2222;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg +Kozlov Oleg;3333;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg +Smirnova Anna;4444;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv new file mode 100644 index 0000000..3354529 --- /dev/null +++ b/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv @@ -0,0 +1,4 @@ +place_name +K-19 +M-16 +T-1 \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv new file mode 100644 index 0000000..11f0364 --- /dev/null +++ b/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv @@ -0,0 +1,3 @@ +date;place_id;employee_id +2025-11-08;1;1 +2025-11-10;2;2 \ No newline at end of file diff --git a/src/main/resources/db.changelog/db.changelog-master.xml b/src/main/resources/db.changelog/db.changelog-master.xml new file mode 100644 index 0000000..90031f0 --- /dev/null +++ b/src/main/resources/db.changelog/db.changelog-master.xml @@ -0,0 +1,14 @@ + + + + + + + + + + +