diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b155d4e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,17 @@ +services: + db: + image: postgres:16.1 + container_name: postgres + ports: + - "5432:5432" + volumes: + - ./volumes/postgres:/var/lib/postgresql/data/ + environment: + - POSTGRES_DB=shareit + - POSTGRES_USER=sa + - POSTGRES_PASSWORD=password + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/pom.xml b/pom.xml index b771773..10fe1c2 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,11 @@ true + + org.springframework.boot + spring-boot-starter-data-jpa + + org.postgresql postgresql diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..b4de626 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,51 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.RequestBookingDto; +import ru.practicum.shareit.booking.service.BookingService; + +import java.util.Collection; + +import static ru.practicum.shareit.util.Constants.USER_ID_HEADER; -/** - * TODO Sprint add-bookings. - */ @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { + + private final BookingService bookingService; + + @PostMapping + public BookingDto create(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestBody RequestBookingDto booking) { + return bookingService.create(userId, booking); + } + + @PatchMapping("/{bookingId}") + public BookingDto updateStatus(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId, + @RequestParam Boolean approved) { + return bookingService.updateStatus(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public BookingDto getById(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId) { + return bookingService.getById(userId, bookingId); + } + + @GetMapping + public Collection getAllByBooker(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByBooker(userId, state); + } + + @GetMapping("/owner") + public Collection getAllByOwner(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByOwner(userId, state); + } + } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/src/main/java/ru/practicum/shareit/booking/BookingState.java new file mode 100644 index 0000000..db2af19 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -0,0 +1,29 @@ +package ru.practicum.shareit.booking; + +import lombok.Getter; +import ru.practicum.shareit.exception.ValidationException; + +import java.util.Arrays; + +@Getter +public enum BookingState { + ALL("ALL"), + CURRENT("CURRENT"), + PAST("PAST"), + FUTURE("FUTURE"), + WAITING("WAITING"), + REJECTED("REJECTED"); + + private final String value; + + BookingState(String value) { + this.value = value; + } + + public static BookingState from(String text) { + return Arrays.stream(values()) + .filter(s -> s.value.equalsIgnoreCase(text)) + .findFirst() + .orElseThrow(() -> new ValidationException("Неизвестный статус бронирования: " + text)); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java index 848cf61..5f8db58 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -5,8 +5,8 @@ import lombok.Data; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.booking.BookingStatus; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.item.dto.ShortItemDto; +import ru.practicum.shareit.user.dto.ShortUserDto; import java.time.LocalDateTime; @@ -17,7 +17,7 @@ public class BookingDto { Long id; LocalDateTime start; LocalDateTime end; - Item item; - User booker; + ShortItemDto item; + ShortUserDto booker; BookingStatus status; } diff --git a/src/main/java/ru/practicum/shareit/booking/dto/RequestBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/RequestBookingDto.java new file mode 100644 index 0000000..2df9014 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/RequestBookingDto.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.FutureOrPresent; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class RequestBookingDto { + Long itemId; + @FutureOrPresent + LocalDateTime start; + LocalDateTime end; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/ShortBookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/ShortBookingDto.java new file mode 100644 index 0000000..abc5efb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/ShortBookingDto.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.FutureOrPresent; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ShortBookingDto { + @FutureOrPresent + LocalDateTime start; + LocalDateTime end; +} diff --git a/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java new file mode 100644 index 0000000..2fd635c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java @@ -0,0 +1,43 @@ +package ru.practicum.shareit.booking.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.RequestBookingDto; +import ru.practicum.shareit.booking.dto.ShortBookingDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dto.ShortItemDto; +import ru.practicum.shareit.user.dto.ShortUserDto; + +@UtilityClass +public class BookingMapper { + + public static BookingDto toBookingDto(Booking booking) { + return BookingDto.builder() + .id(booking.getId()) + .start(booking.getStart()) + .end(booking.getEnd()) + .item(ShortItemDto.builder() + .id(booking.getItem().getId()) + .name(booking.getItem().getName()) + .build()) + .booker(ShortUserDto.builder() + .id(booking.getBooker().getId()) + .build()) + .status(booking.getStatus()) + .build(); + } + + public static Booking toBookingFromRequest(RequestBookingDto bookingDto) { + return Booking.builder() + .start(bookingDto.getStart()) + .end(bookingDto.getEnd()) + .build(); + } + + public static ShortBookingDto toShortBookingDto(Booking booking) { + return ShortBookingDto.builder() + .start(booking.getStart()) + .end(booking.getEnd()) + .build(); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index 1fd5353..adaec90 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,8 +1,7 @@ package ru.practicum.shareit.booking.model; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.booking.BookingStatus; import ru.practicum.shareit.item.model.Item; @@ -13,11 +12,29 @@ @Data @Builder @FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "bookings") public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + + @Column(name = "start_date") LocalDateTime start; + + @Column(name = "end_date") LocalDateTime end; + + @ManyToOne + @JoinColumn(name = "item_id") Item item; + + @ManyToOne + @JoinColumn(name = "booker_id") User booker; + + @Enumerated(EnumType.STRING) BookingStatus status; } diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 0000000..8c1f2ff --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,73 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + Collection findAllByBookerIdOrderByStartDesc(Long bookerId); + + Collection findAllByBookerIdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); + + Collection findAllByBookerIdAndEndBeforeOrderByStartDesc(Long bookerId, LocalDateTime dateTime); + + Collection findAllByBookerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime dateTime); + + @Query(""" + SELECT b FROM Booking b + WHERE b.booker.id = :bookerId + AND b.start <= CURRENT_TIMESTAMP + AND b.end >= CURRENT_TIMESTAMP + ORDER BY b.start DESC + """) + Collection findAllCurrentBookingsByBookerId(Long bookerId); + + Collection findAllByItemOwnerIdOrderByStartDesc(Long ownerId); + + Collection findAllByItemOwnerIdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); + + Collection findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(Long bookerId, LocalDateTime dateAfterEnd); + + Collection findAllByItemOwnerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime dateBeforeStart); + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.owner.id = :ownerId + AND b.start <= CURRENT_TIMESTAMP + AND b.end >= CURRENT_TIMESTAMP + ORDER BY b.start DESC + """) + Collection findAllCurrentBookingsByItemOwnerId(Long ownerId); + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.id IN :itemIds + AND b.status = 'APPROVED' + AND (b.end < CURRENT_TIMESTAMP OR b.start > CURRENT_TIMESTAMP) + ORDER BY b.item.id, b.start + """) + Collection findAllBookingsForItems(List itemIds); + + @Query(""" + SELECT b FROM Booking b + JOIN FETCH b.item + JOIN FETCH b.booker + WHERE b.item.id = :itemId + AND b.booker.id = :userId + AND b.status = 'APPROVED' + AND b.end < CURRENT_TIMESTAMP + """) + Collection findPastBookingsForUserByItemId(Long userId, Long itemId); + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.id = :itemId + AND b.status = 'APPROVED' + """) + List findAllByItemId(Long itemId); +} diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java new file mode 100644 index 0000000..3c05348 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.booking.service; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.RequestBookingDto; + +import java.util.Collection; + +public interface BookingService { + + BookingDto create(Long userId, RequestBookingDto booking); + + BookingDto updateStatus(Long userId, Long bookingId, Boolean approved); + + BookingDto getById(Long userId, Long bookingId); + + Collection getAllByBooker(Long userId, String state); + + Collection getAllByOwner(Long userId, String state); +} diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java new file mode 100644 index 0000000..d588846 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,128 @@ +package ru.practicum.shareit.booking.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.RequestBookingDto; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exception.ForbiddenException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookingServiceImpl implements BookingService { + + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Override + @Transactional + public BookingDto create(Long userId, RequestBookingDto bookingRequest) { + User booker = throwIfUserNotExist(userId); + Item item = throwIfItemNotExist(bookingRequest.getItemId()); + if (!item.isAvailable()) { + throw new ValidationException("Предмет уже забронирован"); + } + Booking booking = BookingMapper.toBookingFromRequest(bookingRequest); + booking.setBooker(booker); + booking.setItem(item); + booking.setStatus(BookingStatus.WAITING); + return BookingMapper.toBookingDto(bookingRepository.save(booking)); + } + + @Override + @Transactional + public BookingDto updateStatus(Long userId, Long bookingId, Boolean approved) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с номером: %s не найдено".formatted(bookingId))); + if (!Objects.equals(userId, booking.getItem().getOwner().getId())) { + throw new ForbiddenException("Изменить статус бронирования может только владелец предмета"); + } + + booking.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + return BookingMapper.toBookingDto(booking); + } + + @Override + public BookingDto getById(Long userId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с номером: %s не найдено".formatted(bookingId))); + + if (!Objects.equals(booking.getBooker().getId(), userId) && + !Objects.equals(booking.getItem().getOwner().getId(), userId)) { + throw new ForbiddenException("Запросить информацию о бронировании может только создатель брони или " + + "владелец предмета"); + } + + return BookingMapper.toBookingDto(booking); + } + + @Override + public Collection getAllByBooker(Long userId, String state) { + throwIfUserNotExist(userId); + BookingState bookingState = BookingState.from(state); + Collection bookings = switch (bookingState) { + case ALL -> bookingRepository.findAllByBookerIdOrderByStartDesc(userId); + case WAITING -> bookingRepository + .findAllByBookerIdAndStatusOrderByStartDesc(userId, BookingStatus.WAITING); + case REJECTED -> bookingRepository + .findAllByBookerIdAndStatusOrderByStartDesc(userId, BookingStatus.REJECTED); + case PAST -> bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, LocalDateTime.now()); + case FUTURE -> bookingRepository + .findAllByBookerIdAndStartAfterOrderByStartDesc(userId, LocalDateTime.now()); + case CURRENT -> bookingRepository.findAllCurrentBookingsByBookerId(userId); + }; + + return bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + + @Override + public Collection getAllByOwner(Long userId, String state) { + throwIfUserNotExist(userId); + BookingState bookingState = BookingState.from(state); + Collection bookings = switch (bookingState) { + case ALL -> bookingRepository.findAllByItemOwnerIdOrderByStartDesc(userId); + case WAITING -> bookingRepository + .findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, BookingStatus.WAITING); + case REJECTED -> bookingRepository + .findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, BookingStatus.REJECTED); + case PAST -> bookingRepository + .findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(userId, LocalDateTime.now()); + case FUTURE -> bookingRepository + .findAllByItemOwnerIdAndStartAfterOrderByStartDesc(userId, LocalDateTime.now()); + case CURRENT -> bookingRepository.findAllCurrentBookingsByItemOwnerId(userId); + }; + + return bookings.stream() + .map(BookingMapper::toBookingDto) + .toList(); + } + + private User throwIfUserNotExist(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(id))); + } + + private Item throwIfItemNotExist(Long id) { + return itemRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Предмет с ID: %s не найден".formatted(id))); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/ExceptionsHandler.java b/src/main/java/ru/practicum/shareit/exception/ExceptionsHandler.java index 3f9689f..c84e105 100644 --- a/src/main/java/ru/practicum/shareit/exception/ExceptionsHandler.java +++ b/src/main/java/ru/practicum/shareit/exception/ExceptionsHandler.java @@ -31,5 +31,12 @@ public ErrorResponse handleForbiddenError(ForbiddenException exception) { return new ErrorResponse("Доступ запрещен:", exception.getMessage()); } + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationError(ValidationException exception) { + log.error("Ошибка валидации данных: {}", exception.getMessage()); + return new ErrorResponse("Ошибка запроса: ", exception.getMessage()); + } + public record ErrorResponse(String error, String details) {} } diff --git a/src/main/java/ru/practicum/shareit/exception/ValidationException.java b/src/main/java/ru/practicum/shareit/exception/ValidationException.java new file mode 100644 index 0000000..59043da --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index d024aa8..8b17d01 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -4,27 +4,30 @@ import jakarta.validation.constraints.Positive; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoWithBookingsAndComments; import ru.practicum.shareit.item.dto.UpdateItemDto; import ru.practicum.shareit.item.service.ItemService; import java.util.Collection; +import static ru.practicum.shareit.util.Constants.USER_ID_HEADER; + @RestController @RequestMapping("/items") @AllArgsConstructor public class ItemController { - private static final String USER_ID_HEADER = "X-Sharer-User-Id"; private final ItemService itemService; @GetMapping - public Collection getAllByUser(@RequestHeader(USER_ID_HEADER) Long userId) { + public Collection getAllByUser(@RequestHeader(USER_ID_HEADER) Long userId) { return itemService.getAllByUser(userId); } @GetMapping("/{itemId}") - public ItemDto getById(@RequestHeader(USER_ID_HEADER) Long userId, + public ItemDtoWithBookingsAndComments getById(@RequestHeader(USER_ID_HEADER) Long userId, @PathVariable Long itemId) { return itemService.getById(userId, itemId); } @@ -43,9 +46,15 @@ public ItemDto update(@RequestHeader(USER_ID_HEADER) Long userId, } @GetMapping("/search") - public Collection search(@RequestHeader(USER_ID_HEADER) Long userId, - @RequestParam(name = "text") String text) { - return itemService.search(userId, text); + public Collection search(@RequestParam(name = "text") String text) { + return itemService.search(text); + } + + @PostMapping("{itemId}/comment") + public CommentDto addComment(@RequestHeader(USER_ID_HEADER) Long authorId, + @PathVariable @Positive Long itemId, + @RequestBody @Valid CommentDto commentRequest) { + return itemService.addComment(authorId, itemId, commentRequest); } } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java new file mode 100644 index 0000000..35a2fa8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CommentDto { + Long id; + + @NotNull + String text; + + String authorName; + + LocalDateTime created; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index 949c43e..4608dd9 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -7,16 +7,24 @@ import lombok.Data; import lombok.experimental.FieldDefaults; +import java.util.Collection; + @Data @Builder @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemDto { Long id; + @NotBlank String name; + @NotBlank String description; + @NotNull Boolean available; + Long requestId; + + Collection comments; } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithBookingsAndComments.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithBookingsAndComments.java new file mode 100644 index 0000000..2823c73 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDtoWithBookingsAndComments.java @@ -0,0 +1,28 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.booking.dto.ShortBookingDto; + +import java.util.Collection; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemDtoWithBookingsAndComments { + Long id; + @NotBlank + String name; + @NotBlank + String description; + @NotNull + Boolean available; + Long requestId; + ShortBookingDto lastBooking; + ShortBookingDto nextBooking; + Collection comments; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ShortItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ShortItemDto.java new file mode 100644 index 0000000..2ecadbf --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ShortItemDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ShortItemDto { + private Long id; + private String name; +} diff --git a/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java new file mode 100644 index 0000000..49055da --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java @@ -0,0 +1,28 @@ +package ru.practicum.shareit.item.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.model.Comment; + +import java.time.LocalDateTime; + +@UtilityClass +public class CommentMapper { + + public CommentDto toCommentDto(Comment comment) { + return CommentDto.builder() + .id(comment.getId()) + .text(comment.getText()) + .authorName(comment.getAuthor().getName()) + .created(comment.getCreated()) + .build(); + } + + public Comment toComment(CommentDto commentDto) { + return Comment.builder() + .id(commentDto.getId()) + .text(commentDto.getText()) + .created(commentDto.getCreated() == null ? LocalDateTime.now() : commentDto.getCreated()) + .build(); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java index cf7c9a0..b50085c 100644 --- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -1,9 +1,18 @@ package ru.practicum.shareit.item.mapper; import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoWithBookingsAndComments; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + @UtilityClass public class ItemMapper { @@ -22,7 +31,38 @@ public Item toItem(ItemDto itemDto) { .id(itemDto.getId()) .name(itemDto.getName()) .description(itemDto.getDescription()) - .available(itemDto.getAvailable()) + .isAvailable(itemDto.getAvailable()) + .build(); + } + + public ItemDtoWithBookingsAndComments toItemDtoWithBookingsAndComments(Item item, + List bookings, + Collection comments) { + Booking lastBooking = null; + Booking nextBooking = null; + + if (bookings != null && !bookings.isEmpty()) { + + lastBooking = bookings.stream() + .filter(b -> b.getEnd().isBefore(LocalDateTime.now())) + .max(Comparator.comparing(Booking::getEnd)) + .orElse(null); + + nextBooking = bookings.stream() + .filter(b -> b.getStart().isAfter(LocalDateTime.now())) + .min(Comparator.comparing(Booking::getStart)) + .orElse(null); + } + + return ItemDtoWithBookingsAndComments.builder() + .id(item.getId()) + .name(item.getName()) + .description(item.getDescription()) + .available(item.isAvailable()) + .requestId(item.getRequest() == null ? null : item.getRequest().getId()) + .lastBooking(lastBooking == null ? null : BookingMapper.toShortBookingDto(lastBooking)) + .nextBooking(nextBooking == null ? null : BookingMapper.toShortBookingDto(nextBooking)) + .comments(comments.stream().map(CommentMapper::toCommentDto).toList()) .build(); } } diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java new file mode 100644 index 0000000..959926b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + String text; + + @ManyToOne + @JoinColumn(name = "item_id") + Item item; + + @ManyToOne + @JoinColumn(name = "author_id") + User author; + + LocalDateTime created; +} diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index 3896da0..4fe84d6 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.item.model; +import jakarta.persistence.*; import lombok.*; import lombok.experimental.FieldDefaults; import ru.practicum.shareit.request.ItemRequest; @@ -10,11 +11,25 @@ @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "items") public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String name; + String description; - boolean available; + + @Column(name = "is_available") + boolean isAvailable; + + @ManyToOne + @JoinColumn(name = "owner_id") User owner; + + @ManyToOne + @JoinColumn(name = "request_id") ItemRequest request; } diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java new file mode 100644 index 0000000..5f101e6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.item.model.Comment; + +import java.util.Collection; +import java.util.List; + +public interface CommentRepository extends JpaRepository { + @Query(""" + SELECT c FROM Comment c + JOIN FETCH c.author + WHERE c.item.id = :itemId + """) + Collection findAllByItemId(Long itemId); + + @Query(""" + SELECT c FROM Comment c + JOIN FETCH c.author + WHERE c.item.id IN :itemIds + """) + Collection findAllByItemIds(List itemIds); +} diff --git a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java deleted file mode 100644 index 293cde5..0000000 --- a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.practicum.shareit.item.repository; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.dto.UpdateItemDto; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; - -@Repository -public class InMemoryItemRepository implements ItemRepository { - - private final HashMap items = new HashMap<>(); - - @Override - public Optional getById(Long itemId) { - return Optional.ofNullable(items.get(itemId)); - } - - @Override - public Collection getByUserId(Long id) { - return items.values().stream() - .filter(item -> Objects.equals(item.getOwner().getId(), id)) - .toList(); - } - - @Override - public Item create(Item item) { - item.setId(generateNextId()); - items.put(item.getId(), item); - return item; - } - - @Override - public Item update(Item item, UpdateItemDto updateItem) { - if (updateItem.getName() != null && !updateItem.getName().isBlank()) { - item.setName(updateItem.getName()); - } - - if (updateItem.getDescription() != null && !updateItem.getDescription().isBlank()) { - item.setDescription(updateItem.getDescription()); - } - - if (updateItem.getAvailable() != null) { - item.setAvailable(updateItem.getAvailable()); - } - return item; - } - - @Override - public Collection searchItems(String text) { - if (text.isBlank()) return Collections.emptyList(); - - return items.values().stream() - .filter(Item::isAvailable) - .filter(item -> item.getName().toLowerCase().contains(text.toLowerCase()) || - item.getDescription().toLowerCase().contains(text.toLowerCase())) - .toList(); - } - - private Long generateNextId() { - Long nextId = items.keySet().stream() - .max(Long::compareTo) - .orElse(0L); - return ++nextId; - } -} diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index e016f1b..24ba577 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -1,20 +1,20 @@ package ru.practicum.shareit.item.repository; -import ru.practicum.shareit.item.dto.UpdateItemDto; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.item.model.Item; import java.util.Collection; -import java.util.Optional; -public interface ItemRepository { +public interface ItemRepository extends JpaRepository { - Optional getById(Long itemId); + Collection findAllByOwnerId(Long userId); - Item create(Item item); - - Item update(Item item, UpdateItemDto updateItem); - - Collection getByUserId(Long id); - - Collection searchItems(String text); + @Query(""" + SELECT i FROM Item i + WHERE i.isAvailable = true + AND (UPPER(i.name) LIKE UPPER(CONCAT('%', ?1, '%')) + OR UPPER(i.description) LIKE UPPER(CONCAT('%', ?1, '%'))) + """) + Collection search(String text); } diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java index bd796f8..8b62cc0 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -1,19 +1,23 @@ package ru.practicum.shareit.item.service; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoWithBookingsAndComments; import ru.practicum.shareit.item.dto.UpdateItemDto; import java.util.Collection; public interface ItemService { - Collection getAllByUser(Long userId); + Collection getAllByUser(Long userId); - ItemDto getById(Long userId, Long id); + ItemDtoWithBookingsAndComments getById(Long userId, Long id); ItemDto create(Long userId, ItemDto item); ItemDto update(Long userId, Long itemId, UpdateItemDto item); - Collection search(Long userId, String text); + Collection search(String text); + + CommentDto addComment(Long authorId, Long itemId, CommentDto commentRequest); } diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index 61c4b67..840da1c 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -2,76 +2,145 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; import ru.practicum.shareit.exception.ForbiddenException; import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemDtoWithBookingsAndComments; import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.mapper.CommentMapper; import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @AllArgsConstructor +@Transactional(readOnly = true) public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; @Override - public Collection getAllByUser(Long userId) { + public Collection getAllByUser(Long userId) { throwIfUserNotExist(userId); - return itemRepository.getByUserId(userId).stream() - .map(ItemMapper::toItemDto) + Collection items = itemRepository.findAllByOwnerId(userId); + if (items.isEmpty()) { + return Collections.emptyList(); + } + List itemsIds = items.stream().map(Item::getId).toList(); + Collection bookings = bookingRepository.findAllBookingsForItems(itemsIds); + + Map> bookingsByItem = bookings.stream() + .collect(Collectors.groupingBy(booking -> booking.getItem().getId())); + + Collection comments = commentRepository.findAllByItemIds(itemsIds); + Map> commentsByItem = comments.stream() + .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); + + return items.stream() + .map(item -> ItemMapper.toItemDtoWithBookingsAndComments(item, + bookingsByItem.getOrDefault(item.getId(), List.of()), + commentsByItem.getOrDefault(item.getId(), List.of()))) .toList(); } @Override - public ItemDto getById(Long userId, Long itemId) { + public ItemDtoWithBookingsAndComments getById(Long userId, Long itemId) { throwIfUserNotExist(userId); - return itemRepository.getById(itemId) - .map(ItemMapper::toItemDto) - .orElseThrow(() -> new NotFoundException("Вещь с ID: %s не найдена".formatted(itemId))); + Item item = throwIfItemNotExist(itemId); + Collection comments = commentRepository.findAllByItemId(itemId); + // если юзер не является собственников, не передаем информацию о бронированиях + List bookings = item.getOwner().getId().equals(userId) + ? bookingRepository.findAllByItemId(itemId) + : List.of(); + return itemRepository.findById(itemId) + .map(i -> ItemMapper.toItemDtoWithBookingsAndComments(i, bookings, comments)) + .orElseThrow(() -> new NotFoundException("Предмет с ID: %s не найден".formatted(itemId))); } @Override + @Transactional public ItemDto create(Long userId, ItemDto item) { User owner = throwIfUserNotExist(userId); Item newItem = ItemMapper.toItem(item); newItem.setOwner(owner); - newItem = itemRepository.create(newItem); + newItem = itemRepository.save(newItem); return ItemMapper.toItemDto(newItem); } @Override + @Transactional public ItemDto update(Long userId, Long itemId, UpdateItemDto updateItem) { throwIfUserNotExist(userId); Item item = throwIfItemNotExist(itemId); if (!userId.equals(item.getOwner().getId())) { throw new ForbiddenException("Обновлять вещь может только ее владелец."); } - Item updatedItem = itemRepository.update(item, updateItem); - return ItemMapper.toItemDto(updatedItem); + if (updateItem.getName() != null && !updateItem.getName().isBlank()) { + item.setName(updateItem.getName()); + } + + if (updateItem.getDescription() != null && !updateItem.getDescription().isBlank()) { + item.setDescription(updateItem.getDescription()); + } + + if (updateItem.getAvailable() != null) { + item.setAvailable(updateItem.getAvailable()); + } + + return ItemMapper.toItemDto(itemRepository.save(item)); } @Override - public Collection search(Long userId, String text) { - throwIfUserNotExist(userId); - return itemRepository.searchItems(text).stream() + public Collection search(String text) { + if (text == null || text.isBlank()) { + return Collections.emptyList(); + } + return itemRepository.search(text).stream() .map(ItemMapper::toItemDto) .toList(); } + @Override + @Transactional + public CommentDto addComment(Long authorId, Long itemId, CommentDto commentRequest) { + Item item = throwIfItemNotExist(itemId); + User author = throwIfUserNotExist(authorId); + Collection pastUserBookings = bookingRepository.findPastBookingsForUserByItemId(authorId, itemId); + if (pastUserBookings.isEmpty()) { + throw new ValidationException("Пользователь не бронировал предмет с ID: %s".formatted(itemId)); + } + Comment comment = CommentMapper.toComment(commentRequest); + comment.setItem(item); + comment.setAuthor(author); + comment = commentRepository.save(comment); + return CommentMapper.toCommentDto(comment); + } + private User throwIfUserNotExist(Long id) { - return userRepository.getById(id) + return userRepository.findById(id) .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(id))); } private Item throwIfItemNotExist(Long id) { - return itemRepository.getById(id) - .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(id))); + return itemRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Предмет с ID: %s не найден".formatted(id))); } } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java index 614c368..59579d9 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/ItemRequest.java @@ -1,19 +1,30 @@ package ru.practicum.shareit.request; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; @Data @Builder @FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "requests") public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String description; - Long requesterId; + + @ManyToOne + @JoinColumn(name = "requestor_id") + User requester; + LocalDateTime created; } diff --git a/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java b/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java index 15d8368..116e5d6 100644 --- a/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java +++ b/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java @@ -20,7 +20,6 @@ public ItemRequest toItemRequest(ItemRequestDto itemRequestDto) { return ItemRequest.builder() .id(itemRequestDto.getId()) .description(itemRequestDto.getDescription()) - .requesterId(itemRequestDto.getId()) .created(itemRequestDto.getCreated()) .build(); } diff --git a/src/main/java/ru/practicum/shareit/user/dto/ShortUserDto.java b/src/main/java/ru/practicum/shareit/user/dto/ShortUserDto.java new file mode 100644 index 0000000..707cacb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/ShortUserDto.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.user.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ShortUserDto { + private Long id; +} diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java index 8e078bf..cc902a2 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.user.model; +import jakarta.persistence.*; import lombok.*; import lombok.experimental.FieldDefaults; @@ -8,7 +9,11 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "users") public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; String name; String email; diff --git a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java deleted file mode 100644 index 0dd6b71..0000000 --- a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java +++ /dev/null @@ -1,72 +0,0 @@ -package ru.practicum.shareit.user.repository; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.exception.NotFoundException; -import ru.practicum.shareit.exception.DuplicateValidationException; -import ru.practicum.shareit.user.model.User; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Objects; -import java.util.Optional; - -@Repository -public class InMemoryUserRepository implements UserRepository { - - private final HashMap users = new HashMap<>(); - - @Override - public Collection getAll() { - return users.values(); - } - - @Override - public Optional getById(Long userId) { - return Optional.ofNullable(users.get(userId)); - } - - @Override - public User create(User user) { - user.setId(generateNextId()); - throwIfEmailTaken(user); - users.put(user.getId(), user); - return user; - } - - @Override - public User update(User userUpdate) { - User user = throwIfUserNotExist(userUpdate.getId()); - throwIfEmailTaken(userUpdate); - if (userUpdate.getName() != null) user.setName(userUpdate.getName()); - if (userUpdate.getEmail() != null) user.setEmail(userUpdate.getEmail()); - return user; - } - - @Override - public void delete(Long id) { - throwIfUserNotExist(id); - users.remove(id); - } - - private Long generateNextId() { - Long nextId = users.keySet().stream() - .max(Long::compareTo) - .orElse(0L); - return ++nextId; - } - - private void throwIfEmailTaken(User user) { - boolean isEmailTaken = users.values().stream() - .filter(existedUser -> !Objects.equals(existedUser.getId(), user.getId())) - .anyMatch(existedUser -> Objects.equals(existedUser.getEmail(), user.getEmail())); - if (isEmailTaken) { - throw new DuplicateValidationException("Пользователь с email: '%s' уже существует" - .formatted(user.getEmail())); - } - } - - private User throwIfUserNotExist(Long id) { - return getById(id) - .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(id))); - } -} diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java index 3648312..c79d1e1 100644 --- a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -1,19 +1,10 @@ package ru.practicum.shareit.user.repository; +import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.user.model.User; -import java.util.Collection; import java.util.Optional; -public interface UserRepository { - - Collection getAll(); - - Optional getById(Long userId); - - User create(User user); - - User update(User user); - - void delete(Long id); +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java index c7043cb..266f582 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -2,6 +2,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.exception.DuplicateValidationException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.user.dto.UpdateUserDto; import ru.practicum.shareit.user.dto.UserDto; @@ -10,43 +12,67 @@ import ru.practicum.shareit.user.repository.UserRepository; import java.util.Collection; +import java.util.Objects; +import java.util.Optional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Override public Collection getAll() { - return userRepository.getAll().stream() + return userRepository.findAll().stream() .map(UserMapper::toUserDto) .toList(); } @Override public UserDto getById(Long userId) { - return userRepository.getById(userId) + return userRepository.findById(userId) .map(UserMapper::toUserDto) .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(userId))); } @Override + @Transactional public UserDto create(UserDto user) { User newUser = UserMapper.toUser(user); - newUser = userRepository.create(newUser); - return UserMapper.toUserDto(newUser); + throwIfEmailTaken(newUser); + return UserMapper.toUserDto(userRepository.save(newUser)); } @Override + @Transactional public UserDto update(UpdateUserDto user) { - User updatedUser = UserMapper.toUser(user); - updatedUser = userRepository.update(updatedUser); - return UserMapper.toUserDto(updatedUser); + User updateUser = UserMapper.toUser(user); + User existUser = throwIfUserNotExist(updateUser.getId()); + throwIfEmailTaken(updateUser); + if (updateUser.getName() != null) existUser.setName(updateUser.getName()); + if (updateUser.getEmail() != null) existUser.setEmail(updateUser.getEmail()); + return UserMapper.toUserDto(userRepository.save(existUser)); } @Override + @Transactional public void delete(Long id) { - userRepository.delete(id); + userRepository.deleteById(id); + } + + private User throwIfUserNotExist(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с ID: %s не найден".formatted(id))); + } + + private void throwIfEmailTaken(User user) { + Optional userWithEmail = userRepository.findByEmail(user.getEmail()); + if (userWithEmail.isPresent()) { + if (!Objects.equals(user.getId(), userWithEmail.get().getId())) { + throw new DuplicateValidationException("Пользователь с email: '%s' уже существует" + .formatted(user.getEmail())); + } + } } } diff --git a/src/main/java/ru/practicum/shareit/util/Constants.java b/src/main/java/ru/practicum/shareit/util/Constants.java new file mode 100644 index 0000000..5e4ed49 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/util/Constants.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.util; + +public class Constants { + + public static final String USER_ID_HEADER = "X-Sharer-User-Id"; + +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 9e9bc4b..a7145cc 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -6,8 +6,7 @@ logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -# TODO Append connection to H2 DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.datasource.url=jdbc:h2:mem:./db/shareit +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 51c5180..9f62b8c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,6 @@ spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.hibernate.format_sql=true +spring.jpa.show-sql=true spring.sql.init.mode=always logging.level.org.springframework.orm.jpa=INFO @@ -7,8 +8,7 @@ logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=sa +spring.datasource.password=password \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..973de9f --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,58 @@ +DROP TABLE IF EXISTS comments; +DROP TABLE IF EXISTS bookings; +DROP TABLE IF EXISTS items; +DROP TABLE IF EXISTS requests; +DROP TABLE IF EXISTS users; + +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL, + CONSTRAINT pk_user PRIMARY KEY (id), + CONSTRAINT UQ_USER_EMAIL UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description TEXT NOT NULL, + requestor_id BIGINT, + created TIMESTAMP, + CONSTRAINT pk_request PRIMARY KEY (id), + FOREIGN KEY (requestor_id) REFERENCES users(id) ON DELETE CASCADE +); + + +CREATE TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + is_available BOOLEAN NOT NULL, + owner_id BIGINT, + request_id BIGINT, + CONSTRAINT pk_item PRIMARY KEY (id), + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (request_id) REFERENCES requests(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + item_id BIGINT, + booker_id BIGINT, + status VARCHAR(32) NOT NULL, + CONSTRAINT pk_booking PRIMARY KEY (id), + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY (booker_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + text TEXT NOT NULL, + item_id BIGINT, + author_id BIGINT, + created TIMESTAMP, + CONSTRAINT pk_comment PRIMARY KEY (id), + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file