diff --git a/pom.xml b/pom.xml index c890aeb..2b0e3ee 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,11 @@ runtime + + org.springframework.boot + spring-boot-starter-data-jpa + + org.projectlombok lombok diff --git a/src/main/java/ru/practicum/shareit/Constants.java b/src/main/java/ru/practicum/shareit/Constants.java new file mode 100644 index 0000000..efa7be6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/Constants.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit; + +public class Constants { + public static final String USER_ID_HEADER = "X-Sharer-User-Id"; + public static final String ALL = "ALL"; +} diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java deleted file mode 100644 index 2d9c666..0000000 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.booking; - -/** - * TODO Sprint add-bookings. - */ -public class Booking { -} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..501fc78 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,66 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.service.BookingService; + +import java.util.Collection; + +import static ru.practicum.shareit.Constants.ALL; +import static ru.practicum.shareit.Constants.USER_ID_HEADER; /** * TODO Sprint add-bookings. */ @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { + + private final BookingService bookingService; + + @PostMapping + public Booking createBooking(@RequestHeader(USER_ID_HEADER) @Positive Long userId, + @Valid @RequestBody BookingDto bookingDto) { + + return bookingService.createBooking(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public Booking updateBookingStatus( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long bookingId, + @RequestParam Boolean approved) { + + return bookingService.updateBookingStatus(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public Booking getBooking( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long bookingId) { + + return bookingService.getBooking(userId, bookingId); + } + + @GetMapping + public Collection findByBookerAndState( + @RequestHeader(USER_ID_HEADER) @Positive Long bookerId, + @RequestParam(defaultValue = ALL) BookingState state) { + + return bookingService.findByBookerAndState(bookerId, state); + } + + @GetMapping("/owner") + public Collection findByOwnerAndState( + @RequestHeader(USER_ID_HEADER) @Positive Long ownerId, + @RequestParam(defaultValue = ALL) BookingState state) { + + return bookingService.findByOwnerAndState(ownerId, state); + } } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java new file mode 100644 index 0000000..2b63de2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.booking; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.model.Booking; + +public class BookingMapper { + + public static Booking toBooking(BookingDto bookingDto) { + Booking booking = new Booking(); + + booking.setStart(bookingDto.getStart()); + booking.setEnd(bookingDto.getEnd()); + + return booking; + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java new file mode 100644 index 0000000..07fa13c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.booking.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.dto.LastAndNextDate; +import ru.practicum.shareit.booking.enums.BookingStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface BookingRepository extends JpaRepository { + + @Query(""" + SELECT + b.item.id as itemId, + MAX(CASE WHEN b.start < CURRENT_TIMESTAMP THEN b.start ELSE NULL END) as lastBooking, + MIN(CASE WHEN b.start > CURRENT_TIMESTAMP THEN b.start ELSE NULL END) as nextBooking + FROM Booking b + WHERE b.item.ownerId = ?1 + GROUP BY b.item.id + """) + List findLastAndNextDatesByOwnerId(Long ownerId); + + Boolean existsByItemIdAndBookerIdAndStatusIsAndEndBefore(Long itemId, Long bookerId, + BookingStatus status, LocalDateTime now); + + // by booker + List findByBookerIdOrderByEndDesc(Long bookerId); + + List findByBookerIdAndStatusIsOrderByEndDesc(Long bookerId, BookingStatus status); + + List findByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(Long bookerId, + LocalDateTime start, + LocalDateTime end); + + List findByBookerIdAndStartIsAfterOrderByEndDesc(Long bookerId, LocalDateTime date); + + List findByBookerIdAndEndIsBeforeOrderByEndDesc(Long bookerId, LocalDateTime date); + + // by owner + List findByItemOwnerIdOrderByEndDesc(Long ownerId); + + List findByItemOwnerIdAndStatusIsOrderByEndDesc(Long ownerId, BookingStatus status); + + List findByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(Long ownerId, + LocalDateTime start, + LocalDateTime end); + + List findByItemOwnerIdAndStartIsAfterOrderByEndDesc(Long ownerId, LocalDateTime date); + + List findByItemOwnerIdAndEndIsBeforeOrderByEndDesc(Long ownerId, LocalDateTime date); +} 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 861de9e..14e48fa 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -1,7 +1,21 @@ package ru.practicum.shareit.booking.dto; -/** - * TODO Sprint add-bookings. - */ +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data public class BookingDto { + @NotNull + @Positive + private Long itemId; + @NotNull + @FutureOrPresent + private LocalDateTime start; + @NotNull + @FutureOrPresent + private LocalDateTime end; } diff --git a/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java b/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java new file mode 100644 index 0000000..fb6d2af --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.booking.dto; + +import java.time.LocalDateTime; + +public interface LastAndNextDate { + Long getItemId(); + + LocalDateTime getLastBooking(); + + LocalDateTime getNextBooking(); +} diff --git a/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java b/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java new file mode 100644 index 0000000..8222368 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.booking.enums; + +public enum BookingState { + ALL, CURRENT, PAST, FUTURE, WAITING, REJECTED +} diff --git a/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java new file mode 100644 index 0000000..d7314c8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.booking.enums; + +public enum BookingStatus { + WAITING, REJECTED, APPROVED +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java new file mode 100644 index 0000000..7668627 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,35 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "bookings", schema = "public") +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "start_date") + private LocalDateTime start; + @Column(name = "end_date") + private LocalDateTime end; + @ManyToOne + @JoinColumn(name = "item_id") + private Item item; + @ManyToOne + @JoinColumn(name = "booker_id") + private User booker; + @Column(name = "status") + @Enumerated(EnumType.STRING) + private BookingStatus status; +} 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..3743d05 --- /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.model.Booking; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingState; + +import java.util.Collection; + +public interface BookingService { + Booking createBooking(Long bookerId, BookingDto bookingDto); + + Booking updateBookingStatus(Long userId, Long bookingId, Boolean approved); + + Booking getBooking(Long userId, Long bookingId); + + Collection findByBookerAndState(Long bookerId, BookingState state); + + Collection findByOwnerAndState(Long ownerId, BookingState 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..b90fb4d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,140 @@ +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.BookingMapper; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingState; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.validation.exceptions.BadRequestException; +import ru.practicum.shareit.validation.exceptions.ForbiddenException; +import ru.practicum.shareit.validation.exceptions.NotFoundException; + +import java.time.LocalDateTime; +import java.util.Collection; + +@Service +@RequiredArgsConstructor +public class BookingServiceImpl implements BookingService { + + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Override + @Transactional + public Booking createBooking(Long bookerId, BookingDto bookingDto) { + throwIfDatesInvalid(bookingDto); + + User user = userRepository.findById(bookerId) + .orElseThrow(() -> new NotFoundException("Пользователь с id " + bookerId + " не найден")); + Item item = itemRepository.findById(bookingDto.getItemId()) + .orElseThrow(() -> new NotFoundException("Предмет с id " + bookingDto.getItemId() + " не найден")); + + if (!item.getIsAvailable()) { + throw new BadRequestException("Предмет не доступен"); + } + + Booking booking = BookingMapper.toBooking(bookingDto); + booking.setItem(item); + booking.setBooker(user); + booking.setStatus(BookingStatus.WAITING); + + return bookingRepository.save(booking); + } + + @Override + @Transactional + public Booking updateBookingStatus(Long userId, Long bookingId, Boolean approved) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с id " + bookingId + " не найдено")); + + if (!isOwner(booking, userId)) { + throw new ForbiddenException("Только владелец предмета может поменять статус"); + } + + if (approved) { + booking.setStatus(BookingStatus.APPROVED); + } else { + booking.setStatus(BookingStatus.REJECTED); + } + + return bookingRepository.save(booking); + } + + @Override + public Booking getBooking(Long userId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование с id " + bookingId + " не найдено")); + + if (!isBooker(booking, userId) && !isOwner(booking, userId)) { + throw new ForbiddenException("Только владелец или арендатор могут посмотреть бронирование"); + } + + return booking; + } + + @Override + @Transactional(readOnly = true) + public Collection findByBookerAndState(Long bookerId, BookingState state) { + if (!userRepository.existsById(bookerId)) { + throw new NotFoundException("Пользователь с id " + bookerId + " не найден"); + } + + LocalDateTime now = LocalDateTime.now(); + + return switch (state) { + case REJECTED -> + bookingRepository.findByBookerIdAndStatusIsOrderByEndDesc(bookerId, BookingStatus.REJECTED); + case WAITING -> bookingRepository.findByBookerIdAndStatusIsOrderByEndDesc(bookerId, BookingStatus.WAITING); + case CURRENT -> + bookingRepository.findByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(bookerId, now, now); + case PAST -> bookingRepository.findByBookerIdAndEndIsBeforeOrderByEndDesc(bookerId, now); + case FUTURE -> bookingRepository.findByBookerIdAndStartIsAfterOrderByEndDesc(bookerId, now); + default -> bookingRepository.findByBookerIdOrderByEndDesc(bookerId); + }; + } + + @Override + @Transactional(readOnly = true) + public Collection findByOwnerAndState(Long ownerId, BookingState state) { + if (!userRepository.existsById(ownerId)) { + throw new NotFoundException("Пользователь с id " + ownerId + " не найден"); + } + + LocalDateTime now = LocalDateTime.now(); + + return switch (state) { + case REJECTED -> + bookingRepository.findByItemOwnerIdAndStatusIsOrderByEndDesc(ownerId, BookingStatus.REJECTED); + case WAITING -> + bookingRepository.findByItemOwnerIdAndStatusIsOrderByEndDesc(ownerId, BookingStatus.WAITING); + case CURRENT -> + bookingRepository.findByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(ownerId, now, now); + case PAST -> bookingRepository.findByItemOwnerIdAndEndIsBeforeOrderByEndDesc(ownerId, now); + case FUTURE -> bookingRepository.findByItemOwnerIdAndStartIsAfterOrderByEndDesc(ownerId, now); + default -> bookingRepository.findByItemOwnerIdOrderByEndDesc(ownerId); + }; + } + + private void throwIfDatesInvalid(BookingDto bookingDto) { + if (!bookingDto.getStart().isBefore(bookingDto.getEnd())) { + throw new BadRequestException("Начало не может быть после конца"); + } + } + + private boolean isBooker(Booking booking, Long userId) { + return booking.getBooker().getId().equals(userId); + } + + private boolean isOwner(Booking booking, Long userId) { + return booking.getItem().getOwnerId().equals(userId); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 4cdcdc7..ba963fe 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -4,6 +4,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.validation.OnCreate; @@ -11,15 +13,13 @@ import java.util.Collection; -/** - * TODO Sprint add-controllers. - */ +import static ru.practicum.shareit.Constants.USER_ID_HEADER; + @RestController @RequestMapping("/items") @RequiredArgsConstructor public class ItemController { - private static final String USER_ID_HEADER = "X-Sharer-User-Id"; private final ItemService itemService; @GetMapping("/{itemId}") @@ -52,4 +52,11 @@ public ItemDto updateItem( return itemService.updateItem(userId, itemId, itemDto); } + @PostMapping("/{itemId}/comment") + public CommentDto addComment(@RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long itemId, + @RequestBody CommentCreate commentText) { + + return itemService.addComment(userId, itemId, commentText); + } } diff --git a/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java new file mode 100644 index 0000000..0e65b07 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.dao; + +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; + +public interface CommentRepository extends JpaRepository { + + Collection findByItemId(Long itemId); + + @Query("SELECT c FROM Comment c JOIN FETCH c.item WHERE c.item.ownerId = ?1") + Collection findByItemOwnerId(Long ownerId); +} diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java index d785762..5592d22 100644 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java @@ -1,18 +1,21 @@ package ru.practicum.shareit.item.dao; +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 { - Optional getItem(Long itemId); +public interface ItemRepository extends JpaRepository { - Collection searchItems(String text); + Collection findByOwnerId(Long userId); - Collection getItems(Long userId); - - Item addItem(Item item); - - Optional updateItem(Item item); + @Query(""" + SELECT i + FROM Item i + WHERE i.isAvailable = true + AND (LOWER(i.name) LIKE LOWER(%:text%) + OR LOWER(i.description) LIKE LOWER(%:text%)) + """) + Collection searchByNameOrDescription(String text); } diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryInMemory.java b/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryInMemory.java deleted file mode 100644 index e899f9b..0000000 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemRepositoryInMemory.java +++ /dev/null @@ -1,78 +0,0 @@ -package ru.practicum.shareit.item.dao; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -@Repository -public class ItemRepositoryInMemory implements ItemRepository { - - private final Map storage = new ConcurrentHashMap<>(); - private final AtomicLong idCounter = new AtomicLong(1L); - - @Override - public Optional getItem(Long itemId) { - return Optional.ofNullable(storage.get(itemId)); - } - - @Override - public Collection searchItems(String text) { - return storage.values() - .stream() - .filter(Item::getIsAvailable) - .filter(item -> - item.getName().toLowerCase(Locale.ROOT).contains(text) || - item.getDescription().toLowerCase(Locale.ROOT).contains(text)) - .toList(); - } - - @Override - public Collection getItems(Long userId) { - return storage.values() - .stream() - .filter(item -> Objects.equals(item.getOwnerId(), userId)) - .toList(); - } - - @Override - public Item addItem(Item item) { - Long id = idCounter.getAndIncrement(); - item.setId(id); - storage.put(id, item); - - return item; - } - - @Override - public Optional updateItem(Item newItem) { - if (!storage.containsKey(newItem.getId())) { - return Optional.empty(); - } - - Item oldItem = storage.get(newItem.getId()); - patchItem(oldItem, newItem); - - return Optional.of(oldItem); - } - - private void patchItem(Item oldItem, Item newItem) { - String name = newItem.getName(); - String description = newItem.getDescription(); - Boolean isAvailable = newItem.getIsAvailable(); - - if (name != null) { - oldItem.setName(name); - } - - if (description != null) { - oldItem.setDescription(description); - } - - if (isAvailable != null) { - oldItem.setIsAvailable(isAvailable); - } - } -} diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java b/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java new file mode 100644 index 0000000..3d52cde --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.item.dto; + +public record CommentCreate(String text) { +} 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..1a228f2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.item.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class CommentDto { + private Long id; + private Long itemId; + private String authorName; + private String text; + private 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 c26a3ca..f5de282 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -9,9 +9,10 @@ import ru.practicum.shareit.validation.OnCreate; import ru.practicum.shareit.validation.OnUpdate; -/** - * TODO Sprint add-controllers. - */ +import java.time.LocalDateTime; +import java.util.Collection; + + @Data @AllArgsConstructor @NoArgsConstructor @@ -25,4 +26,7 @@ public class ItemDto { private String description; @NotNull(groups = OnCreate.class, message = "Статус должен быть заполнен") private Boolean available; + private LocalDateTime lastBooking; + private LocalDateTime nextBooking; + private Collection comments; } diff --git a/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java new file mode 100644 index 0000000..1d69853 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.item.mappers; + +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.model.Comment; + +public class CommentMapper { + public static CommentDto toCommentDto(Comment comment) { + CommentDto commentDto = new CommentDto(); + commentDto.setId(comment.getId()); + commentDto.setItemId(comment.getItem().getId()); + commentDto.setAuthorName(comment.getAuthor().getName()); + commentDto.setText(comment.getText()); + commentDto.setCreated(comment.getCreated()); + + return commentDto; + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java similarity index 61% rename from src/main/java/ru/practicum/shareit/item/ItemMapper.java rename to src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java index 05c0b4d..e886ca2 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.item; +package ru.practicum.shareit.item.mappers; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; @@ -21,4 +21,18 @@ public static ItemDto toItemDto(Item item) { itemDto.setAvailable(item.getIsAvailable()); return itemDto; } + + public static Item merge(Item item, ItemDto itemDto) { + if (itemDto.getName() != null) { + item.setName(itemDto.getName()); + } + if (itemDto.getDescription() != null) { + item.setDescription(itemDto.getDescription()); + } + if (itemDto.getAvailable() != null) { + item.setIsAvailable(itemDto.getAvailable()); + } + + return item; + } } 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..8fe06e4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "comments", schema = "public") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "item_id") + private Item item; + @ManyToOne + @JoinColumn(name = "author_id") + private User author; + @Column(name = "content") + private String text; + @Column(name = "created_at") + private 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 93f135d..5b85018 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,19 +1,25 @@ package ru.practicum.shareit.item.model; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -/** - * TODO Sprint add-controllers. - */ @Data @AllArgsConstructor @NoArgsConstructor +@Entity +@Table(name = "items", schema = "public") public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "name", nullable = false) private String name; + @Column(name = "description", nullable = false) private String description; + @Column(name = "available", nullable = false) private Boolean isAvailable; + @Column(name = "owner_id", nullable = false) private Long ownerId; } 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 d5ac715..d2add2d 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -1,5 +1,7 @@ package ru.practicum.shareit.item.service; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import java.util.Collection; @@ -14,4 +16,6 @@ public interface ItemService { ItemDto addItem(Long userId, ItemDto itemDto); ItemDto updateItem(Long userId, Long itemId, ItemDto itemDto); + + CommentDto addComment(Long userId, Long itemId, CommentCreate commentText); } \ No newline at end of file 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 4b633b8..d8cd5eb 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -2,34 +2,52 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import ru.practicum.shareit.item.ItemMapper; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.LastAndNextDate; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.item.dao.CommentRepository; import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.mappers.CommentMapper; +import ru.practicum.shareit.item.mappers.ItemMapper; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.user.dao.UserRepository; -import ru.practicum.shareit.validation.NotFoundException; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.validation.exceptions.BadRequestException; +import ru.practicum.shareit.validation.exceptions.NotFoundException; +import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; -import java.util.Locale; -import java.util.Optional; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; - private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final BookingRepository bookingRepository; + private final UserService userService; @Override + @Transactional(readOnly = true) public ItemDto getItem(Long itemId) { - Optional maybeItem = itemRepository.getItem(itemId); - if (maybeItem.isPresent()) { - return ItemMapper.toItemDto(maybeItem.get()); - } + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Предмет с id " + itemId + " не найден")); + + ItemDto itemDto = ItemMapper.toItemDto(item); + Collection comments = commentRepository.findByItemId(itemId); - throw new NotFoundException("Предмет с id " + itemId + " не найден"); + return enrichWithComments(itemDto, comments); } @Override @@ -38,55 +56,149 @@ public Collection searchItems(String text) { return Collections.emptyList(); } - text = text.toLowerCase(Locale.ROOT); - - return itemRepository.searchItems(text) + return itemRepository.searchByNameOrDescription(text) .stream() .map(ItemMapper::toItemDto) .toList(); } @Override - public Collection getItems(Long userId) { - throwIfUserNotFound(userId); + @Transactional(readOnly = true) + public Collection getItems(Long ownerId) { + userService.throwIfUserNotFound(ownerId); - Collection items = itemRepository.getItems(userId); + Collection items = itemRepository.findByOwnerId(ownerId); - return items.stream() - .map(ItemMapper::toItemDto) - .toList(); + Map datesMap = getDatesMap(ownerId); + Map> commentsMap = getCommentsMap(ownerId); + + return enrichWithDatesAndComments(items, datesMap, commentsMap); } @Override + @Transactional public ItemDto addItem(Long userId, ItemDto itemDto) { - throwIfUserNotFound(userId); + userService.throwIfUserNotFound(userId); Item item = ItemMapper.toItem(itemDto); item.setOwnerId(userId); - return ItemMapper.toItemDto(itemRepository.addItem(item)); + return ItemMapper.toItemDto(itemRepository.save(item)); } @Override + @Transactional public ItemDto updateItem(Long userId, Long itemId, ItemDto itemDto) { - throwIfUserNotFound(userId); + userService.throwIfUserNotFound(userId); - Item item = ItemMapper.toItem(itemDto); - item.setOwnerId(userId); - item.setId(itemId); + Item currentItem = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Предмет с id " + itemId + " не найден")); + + currentItem.setOwnerId(userId); + ItemMapper.merge(currentItem, itemDto); + + return ItemMapper.toItemDto(itemRepository.save(currentItem)); + } - Optional maybeItem = itemRepository.updateItem(item); + @Override + @Transactional + public CommentDto addComment(Long userId, Long itemId, CommentCreate commentText) { + + User user = userService.getUser(userId); + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Предмет с id " + itemId + " не найден")); - if (maybeItem.isPresent()) { - return ItemMapper.toItemDto(maybeItem.get()); + if (!bookingRepository.existsByItemIdAndBookerIdAndStatusIsAndEndBefore( + itemId, userId, BookingStatus.APPROVED, LocalDateTime.now())) { + throw new BadRequestException("Пользователь никогда не брал предмет в аренду"); } - throw new NotFoundException("Предмет с id " + itemId + " не найден"); + Comment comment = Comment.builder() + .text(commentText.text()) + .item(item) + .author(user) + .created(LocalDateTime.now()) + .build(); + + return CommentMapper.toCommentDto(commentRepository.save(comment)); } - private void throwIfUserNotFound(Long userId) { - if (!userRepository.contains(userId)) { - throw new NotFoundException("Пользователь с id " + userId + " не найден"); - } + /** + * Обогащает коллекцию ItemDto датами бронирований и комментариями. + * + * @param items исходная коллекция предметов + * @param datesMap таблица последних и следующих бронирований по Id предмета + * @param commentsMap таблица комментариев по Id предмета + * @return обогащенная коллекция ItemDto + */ + private Collection enrichWithDatesAndComments(Collection items, + Map datesMap, + Map> commentsMap) { + + return items.stream() + .map(item -> { + ItemDto dto = ItemMapper.toItemDto(item); + + LastAndNextDate dates = datesMap.get(dto.getId()); + if (dates != null) { + dto.setLastBooking(dates.getLastBooking()); + dto.setNextBooking(dates.getNextBooking()); + } + + dto.setComments(commentsMap.getOrDefault(dto.getId(), Collections.emptyList())); + + return dto; + }) + .toList(); + } + + /** + * Обогащает ItemDto комментариями к предмету. + * + * @param dto DTO предмета для обогащения + * @return обогащенный ItemDto с комментариями + */ + private ItemDto enrichWithComments(ItemDto dto, Collection comments) { + Collection commentsDto = comments.stream() + .map(CommentMapper::toCommentDto) + .toList(); + + dto.setComments(commentsDto); + + return dto; + } + + /** + * Создает таблицу последних и следующих бронирований для предметов владельца. + * + * @param ownerId ID владельца предметов + * @return {@code Map} где ключ - Id предмета + */ + private Map getDatesMap(Long ownerId) { + List dates = bookingRepository.findLastAndNextDatesByOwnerId(ownerId); + + return dates.stream() + .collect(Collectors.toMap( + LastAndNextDate::getItemId, + Function.identity() + )); + } + + /** + * Создает таблицу комментариев для всех предметов владельца. + * + * @param ownerId ID владельца предметов + * @return {@code Map} где ключ - Id предмета + */ + private Map> getCommentsMap(Long ownerId) { + Collection comments = commentRepository.findByItemOwnerId(ownerId); + return comments.stream() + .collect(Collectors.groupingBy( + comment -> comment.getItem().getId(), + Collectors.mapping( + CommentMapper::toCommentDto, + Collectors.toList() + ) + )); } } diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java index 7adf898..0bdf44f 100644 --- a/src/main/java/ru/practicum/shareit/user/UserMapper.java +++ b/src/main/java/ru/practicum/shareit/user/UserMapper.java @@ -10,4 +10,14 @@ public static User toUser(UserDto userDto) { user.setEmail(userDto.getEmail()); return user; } + + public static User merge(User user, UserDto userDto) { + if (userDto.getName() != null) { + user.setName(userDto.getName()); + } + if (userDto.getEmail() != null) { + user.setEmail(userDto.getEmail()); + } + return user; + } } diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java b/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java index 2f185ef..d90d213 100644 --- a/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java @@ -1,17 +1,7 @@ package ru.practicum.shareit.user.dao; +import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.user.model.User; -import java.util.Optional; - -public interface UserRepository { - Optional getUser(Long userId); - - User addUser(User user); - - Optional updateUser(User user); - - boolean deleteUser(Long userId); - - boolean contains(Long userId); +public interface UserRepository extends JpaRepository { } diff --git a/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryInMemory.java b/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryInMemory.java deleted file mode 100644 index aadb2c1..0000000 --- a/src/main/java/ru/practicum/shareit/user/dao/UserRepositoryInMemory.java +++ /dev/null @@ -1,82 +0,0 @@ -package ru.practicum.shareit.user.dao; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.validation.NotUniqueException; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -@Repository -public class UserRepositoryInMemory implements UserRepository { - - private final Map storage = new ConcurrentHashMap<>(); - private final AtomicLong idCounter = new AtomicLong(1L); - - @Override - public Optional getUser(Long userId) { - return Optional.ofNullable(storage.get(userId)); - } - - @Override - public User addUser(User user) { - if (isEmailTaken(user.getEmail())) { - throw new NotUniqueException("Email " + user.getEmail() + " уже занят"); - } - - Long id = idCounter.getAndIncrement(); - user.setId(id); - storage.put(id, user); - - return user; - } - - @Override - public Optional updateUser(User newUser) { - if (!storage.containsKey(newUser.getId())) { - return Optional.empty(); - } - - User oldUser = storage.get(newUser.getId()); - patchUser(oldUser, newUser); - - return Optional.of(oldUser); - - } - - @Override - public boolean deleteUser(Long userId) { - User user = storage.remove(userId); - - return user != null; - } - - @Override - public boolean contains(Long userId) { - return storage.containsKey(userId); - } - - private boolean isEmailTaken(String email) { - return storage.values() - .stream() - .anyMatch(user -> user.getEmail().equals(email)); - } - - private void patchUser(User oldUser, User newUser) { - String name = newUser.getName(); - String email = newUser.getEmail(); - - if (name != null) { - oldUser.setName(name); - } - - if (email != null) { - if (isEmailTaken(email)) { - throw new NotUniqueException("Email " + email + " уже занят"); - } - oldUser.setEmail(email); - } - } -} 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 0a07207..122e90c 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,17 +1,21 @@ package ru.practicum.shareit.user.model; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -/** - * TODO Sprint add-controllers. - */ @Data @AllArgsConstructor @NoArgsConstructor +@Entity +@Table(name = "users", schema = "public") public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "name") private String name; + @Column(name = "email", unique = true) private String email; } diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/src/main/java/ru/practicum/shareit/user/service/UserService.java index ba3e769..bcc94cf 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserService.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserService.java @@ -11,4 +11,6 @@ public interface UserService { User updateUser(Long userId, UserDto userDto); void deleteUser(Long userId); + + void throwIfUserNotFound(Long userId); } 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 82250d9..2e0f389 100644 --- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -6,9 +6,7 @@ import ru.practicum.shareit.user.dao.UserRepository; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.validation.NotFoundException; - -import java.util.Optional; +import ru.practicum.shareit.validation.exceptions.NotFoundException; @Service @RequiredArgsConstructor @@ -18,31 +16,32 @@ public class UserServiceImpl implements UserService { @Override public User getUser(Long userId) { - Optional maybeUser = userRepository.getUser(userId); - - return maybeUser.orElseThrow(() -> new NotFoundException("Пользователь с id " + userId + " не найден")); + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с id " + userId + " не найден")); } @Override public User addUser(UserDto userDto) { - return userRepository.addUser(UserMapper.toUser(userDto)); + return userRepository.save(UserMapper.toUser(userDto)); } @Override public User updateUser(Long userId, UserDto userDto) { - User user = UserMapper.toUser(userDto); - user.setId(userId); + User currentUser = getUser(userId); - Optional maybeUser = userRepository.updateUser(user); + UserMapper.merge(currentUser, userDto); - return maybeUser.orElseThrow(() -> new NotFoundException("Пользователь с id " + userId + " не найден")); + return userRepository.save(currentUser); } @Override public void deleteUser(Long userId) { - boolean isDeleted = userRepository.deleteUser(userId); + throwIfUserNotFound(userId); + userRepository.deleteById(userId); + } - if (!isDeleted) { + public void throwIfUserNotFound(Long userId) { + if (!userRepository.existsById(userId)) { throw new NotFoundException("Пользователь с id " + userId + " не найден"); } } diff --git a/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java index 64b34f4..7d246e7 100644 --- a/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java +++ b/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java @@ -1,11 +1,16 @@ package ru.practicum.shareit.validation; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import ru.practicum.shareit.validation.exceptions.BadRequestException; +import ru.practicum.shareit.validation.exceptions.ForbiddenException; +import ru.practicum.shareit.validation.exceptions.NotFoundException; +import ru.practicum.shareit.validation.exceptions.NotUniqueException; import java.util.Collections; import java.util.HashMap; @@ -37,4 +42,25 @@ public ResponseEntity> handleNotFoundException(NotFoundExcep public ResponseEntity> handleNotUniqueException(NotUniqueException ex) { return ResponseEntity.status(HttpStatus.CONFLICT).body(Collections.singletonMap("error", ex.getMessage())); } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity> handleBadRequestException(BadRequestException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Collections.singletonMap("error", ex.getMessage())); + } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity> handleForbiddenException(ForbiddenException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Collections.singletonMap("error", ex.getMessage())); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleDataIntegrityViolationException(ConstraintViolationException ex) { + + if (ex.getConstraintName() != null && ex.getConstraintName().equals("uq_user_email")) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Collections.singletonMap("error", "Email is taken")); + } + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Collections.singletonMap("error", "BAD_REQUEST")); + } } diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java b/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java new file mode 100644 index 0000000..c2c28ca --- /dev/null +++ b/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.validation.exceptions; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java b/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java new file mode 100644 index 0000000..b58a3ef --- /dev/null +++ b/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.validation.exceptions; + +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/validation/NotFoundException.java b/src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java similarity index 72% rename from src/main/java/ru/practicum/shareit/validation/NotFoundException.java rename to src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java index d47b817..9fa76ae 100644 --- a/src/main/java/ru/practicum/shareit/validation/NotFoundException.java +++ b/src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.validation; +package ru.practicum.shareit.validation.exceptions; public class NotFoundException extends RuntimeException { public NotFoundException(String message) { diff --git a/src/main/java/ru/practicum/shareit/validation/NotUniqueException.java b/src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java similarity index 72% rename from src/main/java/ru/practicum/shareit/validation/NotUniqueException.java rename to src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java index 7881e2a..ff5edd0 100644 --- a/src/main/java/ru/practicum/shareit/validation/NotUniqueException.java +++ b/src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.validation; +package ru.practicum.shareit.validation.exceptions; public class NotUniqueException extends RuntimeException { public NotUniqueException(String message) { diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 9e9bc4b..9295397 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.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.username=dbuser +spring.datasource.password=12345 \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 51c5180..0478d27 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,8 +7,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://0.0.0.0:5432/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..832ce04 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,39 @@ +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 items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + CONSTRAINT pk_item PRIMARY KEY (id), + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE +); + +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 NOT NULL, + booker_id BIGINT NOT NULL, + status VARCHAR(255) 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, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE +);