diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index 20fe015..5690186 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -23,7 +23,7 @@ jobs: GITEA_HEAD_REF: ${{ gitea.event.pull_request.head.ref }} - name: Checkout tests - run: python3 /opt/scripts/copy-tests.py --repo-url "Olympic/NTO-2025-Android-TeamTask-tests" --branch "main" --task-type "spring" + run: python3 /opt/scripts/copy-tests.py --repo-url "Olympic/NTO-2025-Backend-TeamTask-tests" --branch "main" --task-type "spring" - name: Run tests run: mvn test diff --git a/src/main/java/com/example/nto/App.java b/src/main/java/com/example/nto/App.java index e453f89..d4add94 100644 --- a/src/main/java/com/example/nto/App.java +++ b/src/main/java/com/example/nto/App.java @@ -1,12 +1,11 @@ package com.example.nto; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +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 index 9885f84..80f885e 100644 --- a/src/main/java/com/example/nto/controller/BookingController.java +++ b/src/main/java/com/example/nto/controller/BookingController.java @@ -1,10 +1,32 @@ package com.example.nto.controller; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +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.*; +@Validated +@RestController +@RequestMapping("api") +@RequiredArgsConstructor public class BookingController { + + private static BookingService bookingService; + + @GetMapping("/{code}/booking") + @ResponseStatus(code = HttpStatus.OK) + public Map> getByDate(@PathVariable String code) { + return bookingService.getFreePLace(code); + } + + @GetMapping("/{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 index 47658f9..ada1246 100644 --- a/src/main/java/com/example/nto/controller/EmployeeController.java +++ b/src/main/java/com/example/nto/controller/EmployeeController.java @@ -1,10 +1,27 @@ package com.example.nto.controller; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +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..6bc3e66 --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/BookingCreateDto.java @@ -0,0 +1,18 @@ +package com.example.nto.controller.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.*; + +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..645dd3c --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/EmployeeDto.java @@ -0,0 +1,32 @@ +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); + } +} \ No newline at end of file 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..ac67aba --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/PlaceDto.java @@ -0,0 +1,18 @@ +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 index 21c1981..d0e04ba 100644 --- a/src/main/java/com/example/nto/entity/Booking.java +++ b/src/main/java/com/example/nto/entity/Booking.java @@ -1,8 +1,6 @@ package com.example.nto.entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -10,26 +8,28 @@ import lombok.NoArgsConstructor; import java.time.LocalDate; - -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ @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 index a52102b..6bf2fa1 100644 --- a/src/main/java/com/example/nto/entity/Employee.java +++ b/src/main/java/com/example/nto/entity/Employee.java @@ -8,27 +8,28 @@ import lombok.NoArgsConstructor; import java.util.List; - -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ @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 index 00c253b..c266212 100644 --- a/src/main/java/com/example/nto/entity/Place.java +++ b/src/main/java/com/example/nto/entity/Place.java @@ -1,29 +1,23 @@ package com.example.nto.entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; - -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ @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/BookingAlreadyExistException.java b/src/main/java/com/example/nto/exception/BookingAlreadyExistException.java new file mode 100644 index 0000000..2748341 --- /dev/null +++ b/src/main/java/com/example/nto/exception/BookingAlreadyExistException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class BookingAlreadyExistException extends RuntimeException { + public BookingAlreadyExistException(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..c2c496f --- /dev/null +++ b/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,35 @@ +package com.example.nto.exception.handler; + +import com.example.nto.exception.BookingAlreadyExistException; +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(BookingAlreadyExistException.class) + public ResponseEntity handleBookingAlreadyExistException(BookingAlreadyExistException 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 handlerGenericException(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 index 303bb54..7d4093b 100644 --- a/src/main/java/com/example/nto/repository/BookingRepository.java +++ b/src/main/java/com/example/nto/repository/BookingRepository.java @@ -1,10 +1,18 @@ package com.example.nto.repository; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ -public interface BookingRepository { +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 index 210d29c..d845a04 100644 --- a/src/main/java/com/example/nto/repository/EmployeeRepository.java +++ b/src/main/java/com/example/nto/repository/EmployeeRepository.java @@ -1,10 +1,12 @@ package com.example.nto.repository; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ -public interface EmployeeRepository { +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 index d3bea1d..e7b84c9 100644 --- a/src/main/java/com/example/nto/repository/PlaceRepository.java +++ b/src/main/java/com/example/nto/repository/PlaceRepository.java @@ -1,10 +1,7 @@ package com.example.nto.repository; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ -public interface PlaceRepository { +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 index 31ec148..a382837 100644 --- a/src/main/java/com/example/nto/service/BookingService.java +++ b/src/main/java/com/example/nto/service/BookingService.java @@ -1,10 +1,15 @@ package com.example.nto.service; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +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 index cccd209..e5b41a4 100644 --- a/src/main/java/com/example/nto/service/EmployeeService.java +++ b/src/main/java/com/example/nto/service/EmployeeService.java @@ -1,10 +1,8 @@ package com.example.nto.service; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +import com.example.nto.controller.dto.EmployeeDto; + public interface EmployeeService { -} + EmployeeDto getByCode(String code); + void auth(String code); +} \ No newline at end of file diff --git a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java index d24b244..583a7ba 100644 --- a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java +++ b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java @@ -1,12 +1,102 @@ 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.BookingAlreadyExistException; +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; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ +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 BookingAlreadyExistException("Booking already exists"); + } + + if (bookingRepository.findByDateAndEmployee(date, employee).isPresent()) { + throw new BookingAlreadyExistException("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 index f8125e5..07cd2af 100644 --- a/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java +++ b/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java @@ -1,12 +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; -/** - * TODO: ДОРАБОТАТЬ в рамках задания - * ================================= - * МОЖНО: Добавлять методы, аннотации, зависимости - * НЕЛЬЗЯ: Изменять название класса и пакета - */ + +@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!"); + } + } +} \ No newline at end of file