diff --git a/src/main/java/com/example/nto/controller/BookingController.java b/src/main/java/com/example/nto/controller/BookingController.java index 9885f84..66b6ead 100644 --- a/src/main/java/com/example/nto/controller/BookingController.java +++ b/src/main/java/com/example/nto/controller/BookingController.java @@ -1,10 +1,38 @@ package com.example.nto.controller; +import com.example.nto.dto.BookRequestDto; +import com.example.nto.dto.BookingRecordDto; +import com.example.nto.dto.PlaceRecordDto; +import com.example.nto.service.BookingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + /** * TODO: ДОРАБОТАТЬ в рамках задания * ================================= * МОЖНО: Добавлять методы, аннотации, зависимости * НЕЛЬЗЯ: Изменять название класса и пакета */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") public class BookingController { + private final BookingService bookingService; + + @GetMapping("/{code}/booking") + Map> booking(@PathVariable("code") String code) { + return bookingService.booking(code); + } + + @PostMapping("/{code}/book") + ResponseEntity book(@PathVariable("code") String code, @RequestBody BookRequestDto bookRequest) { + bookingService.book(code, bookRequest); + return new ResponseEntity<>(null, HttpStatus.CREATED); + } } diff --git a/src/main/java/com/example/nto/dto/BookRequestDto.java b/src/main/java/com/example/nto/dto/BookRequestDto.java new file mode 100644 index 0000000..e91290c --- /dev/null +++ b/src/main/java/com/example/nto/dto/BookRequestDto.java @@ -0,0 +1,17 @@ +package com.example.nto.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BookRequestDto { + private LocalDate date; + private long placeID; +} diff --git a/src/main/java/com/example/nto/dto/BookedPlacesDto.java b/src/main/java/com/example/nto/dto/BookedPlacesDto.java new file mode 100644 index 0000000..746ac0e --- /dev/null +++ b/src/main/java/com/example/nto/dto/BookedPlacesDto.java @@ -0,0 +1,13 @@ +package com.example.nto.dto; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +@Builder +public class BookedPlacesDto { + private LocalDate date; + private String place; +} diff --git a/src/main/java/com/example/nto/dto/EmployeeInfoWithBookingDto.java b/src/main/java/com/example/nto/dto/EmployeeInfoWithBookingDto.java index a56557e..2f8e8a5 100644 --- a/src/main/java/com/example/nto/dto/EmployeeInfoWithBookingDto.java +++ b/src/main/java/com/example/nto/dto/EmployeeInfoWithBookingDto.java @@ -11,5 +11,5 @@ import java.util.Map; public class EmployeeInfoWithBookingDto { private String name; private String photoUrl; - Map booking; + private Map booking; } diff --git a/src/main/java/com/example/nto/dto/PlaceRecordDto.java b/src/main/java/com/example/nto/dto/PlaceRecordDto.java new file mode 100644 index 0000000..8a7472e --- /dev/null +++ b/src/main/java/com/example/nto/dto/PlaceRecordDto.java @@ -0,0 +1,11 @@ +package com.example.nto.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class PlaceRecordDto { + private long id; + private String place; +} diff --git a/src/main/java/com/example/nto/dto/PlaceWithDateDto.java b/src/main/java/com/example/nto/dto/PlaceWithDateDto.java new file mode 100644 index 0000000..7eac3b5 --- /dev/null +++ b/src/main/java/com/example/nto/dto/PlaceWithDateDto.java @@ -0,0 +1,19 @@ +package com.example.nto.dto; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class PlaceWithDateDto { + private LocalDate date; + private long id; + private String place; + + public PlaceWithDateDto(java.sql.Date date, long id, String place) { + this.date = date.toLocalDate(); + this.id = id; + this.place = place; + } +} diff --git a/src/main/java/com/example/nto/dto/mapper/ListPlaceWithDateDtoMapper.java b/src/main/java/com/example/nto/dto/mapper/ListPlaceWithDateDtoMapper.java new file mode 100644 index 0000000..314dcfd --- /dev/null +++ b/src/main/java/com/example/nto/dto/mapper/ListPlaceWithDateDtoMapper.java @@ -0,0 +1,32 @@ +package com.example.nto.dto.mapper; + +import com.example.nto.dto.PlaceRecordDto; +import com.example.nto.dto.PlaceWithDateDto; +import lombok.experimental.UtilityClass; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@UtilityClass +public class ListPlaceWithDateDtoMapper { + public static Map> toMapLocalDateListPlaceRecordDto(List d) { + if (d == null) { + return new HashMap<>(); + } + + return d.stream() + .collect(Collectors.groupingBy( + PlaceWithDateDto::getDate, + Collectors.mapping( + placeWithDate -> PlaceRecordDto.builder() + .id(placeWithDate.getId()) + .place(placeWithDate.getPlace()) + .build(), + Collectors.toList() + ) + )); + } +} diff --git a/src/main/java/com/example/nto/entity/Booking.java b/src/main/java/com/example/nto/entity/Booking.java index cf27b9d..9371e6f 100644 --- a/src/main/java/com/example/nto/entity/Booking.java +++ b/src/main/java/com/example/nto/entity/Booking.java @@ -34,7 +34,7 @@ public class Booking { @JoinColumn(name = "place_id") private Place place; - @OneToOne(fetch = FetchType.LAZY) + @ManyToOne(targetEntity = Employee.class, fetch = FetchType.LAZY) @JoinColumn( name = "employee_id", referencedColumnName = "id" diff --git a/src/main/java/com/example/nto/exception/AlreadyBookingForThisDateException.java b/src/main/java/com/example/nto/exception/AlreadyBookingForThisDateException.java new file mode 100644 index 0000000..e72aa56 --- /dev/null +++ b/src/main/java/com/example/nto/exception/AlreadyBookingForThisDateException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class AlreadyBookingForThisDateException extends RuntimeException { + public AlreadyBookingForThisDateException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/GlobalExceptionHandler.java b/src/main/java/com/example/nto/exception/GlobalExceptionHandler.java index ec69958..3a5ccbd 100644 --- a/src/main/java/com/example/nto/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/nto/exception/GlobalExceptionHandler.java @@ -8,31 +8,23 @@ import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(EmployeeNotFoundByCodeException.class) - ResponseEntity codeNotFoundExceptionHandler(EmployeeNotFoundByCodeException e) { + ResponseEntity employeeNotFoundByCodeExceptionHandler(EmployeeNotFoundByCodeException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); } - /*@ExceptionHandler(CodeNotFoundException.class) - ResponseEntity codeNotFoundExceptionHandler(CodeNotFoundException e) { + + @ExceptionHandler(PlaceAlreadyBookedException.class) + ResponseEntity placeAlreadyBookedExceptionHandler(PlaceAlreadyBookedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT); + } + + @ExceptionHandler(PlaceNotFoundException.class) + ResponseEntity placeNotFoundExceptionHandler(PlaceNotFoundException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } - @ExceptionHandler(EmployeeNotFoundException.class) - ResponseEntity employeeNotFoundExceptionHandler(EmployeeNotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED); + @ExceptionHandler(AlreadyBookingForThisDateException.class) + ResponseEntity alreadyBookingForThisDateExceptionHandler(AlreadyBookingForThisDateException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } - @ExceptionHandler(EmployeeDataNotFoundException.class) - ResponseEntity employeeDataNotFoundExceptionHandler(EmployeeDataNotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } - - @ExceptionHandler(EmployeeIsBlockedException.class) - ResponseEntity employeeIsBlockedExceptionHandler(EmployeeIsBlockedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.LOCKED); - } - - @ExceptionHandler(SelfChangeException.class) - ResponseEntity selfChangeExceptionHandler(SelfChangeException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_ACCEPTABLE); - }*/ } diff --git a/src/main/java/com/example/nto/exception/PlaceAlreadyBookedException.java b/src/main/java/com/example/nto/exception/PlaceAlreadyBookedException.java new file mode 100644 index 0000000..afde31f --- /dev/null +++ b/src/main/java/com/example/nto/exception/PlaceAlreadyBookedException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class PlaceAlreadyBookedException extends RuntimeException { + public PlaceAlreadyBookedException(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..a4701a5 --- /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/repository/BookingRepository.java b/src/main/java/com/example/nto/repository/BookingRepository.java index 04b2382..a55de2b 100644 --- a/src/main/java/com/example/nto/repository/BookingRepository.java +++ b/src/main/java/com/example/nto/repository/BookingRepository.java @@ -3,10 +3,12 @@ package com.example.nto.repository; import com.example.nto.dto.BookingWithDateDto; import com.example.nto.entity.Booking; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.util.List; /** @@ -17,6 +19,13 @@ import java.util.List; */ @Repository public interface BookingRepository extends JpaRepository { - @Query("select new com.example.nto.dto.BookingWithDateDto(b.date, b.id, b.place.place) from Booking b where b.employee.code = :code order by b.date") + @Query("select new com.example.nto.dto.BookingWithDateDto(b.date, b.place.id, b.place.place) from Booking b where b.employee.code = :code order by b.date") List getBookingsByCode(@Param("code") String code); + + @Query("select count(b) > 0 from Booking b where b.date = :date and b.employee.code = :code") + boolean hasBookingForDate(@Param("code") String code, @Param("date") LocalDate date); + + @Modifying + @Query(value = "insert into booking (date, place_id, employee_id) select :date, :placeId, e.id from employee e where e.code = :code", nativeQuery = true) + void bookPlace(@Param("code") String code, @Param("date") LocalDate date, @Param("placeId") long placeId); } diff --git a/src/main/java/com/example/nto/repository/PlaceRepository.java b/src/main/java/com/example/nto/repository/PlaceRepository.java index d3bea1d..d9f898b 100644 --- a/src/main/java/com/example/nto/repository/PlaceRepository.java +++ b/src/main/java/com/example/nto/repository/PlaceRepository.java @@ -1,10 +1,55 @@ package com.example.nto.repository; +import com.example.nto.dto.BookedPlacesDto; +import com.example.nto.dto.PlaceRecordDto; +import com.example.nto.dto.PlaceWithDateDto; +import com.example.nto.entity.Place; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Set; + /** * TODO: ДОРАБОТАТЬ в рамках задания * ================================= * МОЖНО: Добавлять методы, аннотации, зависимости * НЕЛЬЗЯ: Изменять название класса и пакета */ -public interface PlaceRepository { +@Repository +public interface PlaceRepository extends JpaRepository { + /*with cte as ( + select (now() + s*('1day'::interval)) ::date d + from generate_series(0,3) s + ) + select dates.d as date, p.id as id, p.place_name as place + from cte dates + cross join place p + left join booking b on + p.id = b.place_id + and b.date = dates.d + where b.date is null + order by 2, 1*/ + + @Query(value = "WITH cte AS (" + + " SELECT DATEADD('DAY', s, CAST(:fromDate AS DATE)) AS d" + + " FROM SYSTEM_RANGE(0, 3) t(s)" + + ")" + + " SELECT dates.d AS date, p.id AS id, p.place_name AS place" + + " FROM cte dates" + + " CROSS JOIN place p" + + " LEFT JOIN booking b ON p.id = b.place_id AND b.date = dates.d" + + " WHERE b.date IS NULL" + + " ORDER BY dates.d, p.id", + nativeQuery = true) + List getFreePlaces(@Param("fromDate") LocalDate fromDate); + + @Query("select count(b) = 0 from Booking b where b.place.id = :placeId and b.date = :date") + boolean isFree(@Param("date") LocalDate date, @Param("placeId") long placeId); + + @Query("select count(p) > 0 from Place p where p.id = :placeId") + boolean isExist(@Param("placeId") long placeId); } diff --git a/src/main/java/com/example/nto/service/BookingService.java b/src/main/java/com/example/nto/service/BookingService.java index 31ec148..9561787 100644 --- a/src/main/java/com/example/nto/service/BookingService.java +++ b/src/main/java/com/example/nto/service/BookingService.java @@ -1,5 +1,11 @@ package com.example.nto.service; +import com.example.nto.dto.BookRequestDto; +import com.example.nto.dto.PlaceRecordDto; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + /** * TODO: ДОРАБОТАТЬ в рамках задания * ================================= @@ -7,4 +13,6 @@ package com.example.nto.service; * НЕЛЬЗЯ: Изменять название класса и пакета */ public interface BookingService { + Map> booking(String code); + void book(String code, BookRequestDto bookRequest); } 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..e86058e 100644 --- a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java +++ b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java @@ -1,6 +1,27 @@ package com.example.nto.service.impl; +import com.example.nto.dto.BookRequestDto; +import com.example.nto.dto.PlaceRecordDto; +import com.example.nto.dto.PlaceWithDateDto; +import com.example.nto.exception.AlreadyBookingForThisDateException; +import com.example.nto.exception.EmployeeNotFoundByCodeException; +import com.example.nto.exception.PlaceAlreadyBookedException; +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 jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static com.example.nto.dto.mapper.ListPlaceWithDateDtoMapper.toMapLocalDateListPlaceRecordDto; /** * TODO: ДОРАБОТАТЬ в рамках задания @@ -8,5 +29,56 @@ import com.example.nto.service.BookingService; * МОЖНО: Добавлять методы, аннотации, зависимости * НЕЛЬЗЯ: Изменять название класса и пакета */ +@Service +@RequiredArgsConstructor public class BookingServiceImpl implements BookingService { + private final EmployeeRepository employeeRepository; + private final PlaceRepository placeRepository; + private final BookingRepository bookingRepository; + + @Override + public Map> booking(String code) { + if (!employeeRepository.isExist(code)) { + throw new EmployeeNotFoundByCodeException("employee with code " + code + " not found"); + } + + LocalDate currentDate = LocalDate.now(); + + List freePlaces = placeRepository.getFreePlaces(currentDate); + + Map> result = new TreeMap<>(); + + for (int i = 0; i <= 3; i++) { + result.put(currentDate.plusDays(i), new ArrayList<>()); + } + for (PlaceWithDateDto freePlace : freePlaces) { + result.get(freePlace.getDate()).add(PlaceRecordDto.builder() + .id(freePlace.getId()) + .place(freePlace.getPlace()) + .build()); + } + + return result; + } + + @Override + @Transactional + public void book(String code, BookRequestDto bookRequest) { + if (!employeeRepository.isExist(code)) { + throw new EmployeeNotFoundByCodeException("employee with code " + code + " not found"); + } + + if (!placeRepository.isExist(bookRequest.getPlaceID())) { + throw new PlaceNotFoundException("place with id " + bookRequest.getPlaceID() + " is not found"); + } + if (bookingRepository.hasBookingForDate(code, bookRequest.getDate())) { + throw new AlreadyBookingForThisDateException("employee with code " + code + " already book place on date " + bookRequest.getDate().toString()); + } + if (!placeRepository.isFree(bookRequest.getDate(), bookRequest.getPlaceID())) { + throw new PlaceAlreadyBookedException("place with id " + bookRequest.getPlaceID() + " is already booked, please choose another one"); + } + + bookingRepository.bookPlace(code, bookRequest.getDate(), bookRequest.getPlaceID()); + } + }