diff --git a/pom.xml b/pom.xml index d85b478..585edda 100644 --- a/pom.xml +++ b/pom.xml @@ -2,6 +2,7 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent @@ -25,18 +26,37 @@ org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-validation org.springframework.boot - spring-boot-starter-validation + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + org.projectlombok lombok true + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/src/main/java/ru/practicum/shareit/ShareItApp.java index 8bda1c7..0592a88 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/src/main/java/ru/practicum/shareit/ShareItApp.java @@ -2,9 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@SpringBootApplication public class ShareItApp { public static void main(String[] args) { SpringApplication.run(ShareItApp.class, args); 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..bc4a7fe 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,48 @@ 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.BookingResponseDto; + +import jakarta.validation.Valid; +import java.util.List; -/** - * TODO Sprint add-bookings. - */ @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { -} + private final BookingService bookingService; + private static final String USER_ID_HEADER = "X-Sharer-User-Id"; + + @PostMapping + public BookingResponseDto create(@RequestHeader(USER_ID_HEADER) Long userId, + @Valid @RequestBody BookingDto bookingDto) { + return bookingService.create(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public BookingResponseDto approve(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId, + @RequestParam boolean approved) { + return bookingService.approve(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public BookingResponseDto getById(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId) { + return bookingService.getById(userId, bookingId); + } + + @GetMapping + public List getAllByBooker(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByBooker(userId, state); + } + + @GetMapping("/owner") + public List getAllByOwner(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByOwner(userId, state); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java new file mode 100644 index 0000000..bfbb225 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.booking; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + + @Query("select b from Booking b where b.booker.id = ?1 order by b.start desc") + List findUserBookings(Long bookerId); + + @Query("select b from Booking b where b.booker.id = ?1 and b.status = ?2 order by b.start desc") + List findUserStatus(Long bookerId, Status status); + + @Query("select b from Booking b where b.booker.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findUserCurrent(Long bookerId, LocalDateTime now); + + @Query("select b from Booking b where b.booker.id = ?1 and b.end < ?2 order by b.start desc") + List findUserPast(Long bookerId, LocalDateTime now); + + @Query("select b from Booking b where b.booker.id = ?1 and b.start > ?2 order by b.start desc") + List findUserFuture(Long bookerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.owner.id = ?1 order by b.start desc") + List findOwnerBookings(Long ownerId); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.status = ?2 order by b.start desc") + List findOwnerStatus(Long ownerId, Status status); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findOwnerCurrent(Long ownerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.end < ?2 order by b.start desc") + List findOwnerPast(Long ownerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start > ?2 order by b.start desc") + List findOwnerFuture(Long ownerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.id = ?1 order by b.start asc") + List findItemBookings(Long itemId); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java new file mode 100644 index 0000000..935b068 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.booking; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; + +import java.util.List; + +public interface BookingService { + + BookingResponseDto create(Long userId, BookingDto bookingDto); + + BookingResponseDto approve(Long userId, Long bookingId, boolean approved); + + BookingResponseDto getById(Long userId, Long bookingId); + + List getAllByBooker(Long userId, String state); + + List getAllByOwner(Long userId, String state); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java new file mode 100644 index 0000000..7cf0a92 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -0,0 +1,100 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Transactional + @Override + public BookingResponseDto create(Long userId, BookingDto bookingDto) { + User booker = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + Item item = itemRepository.findById(bookingDto.getItemId()).orElseThrow(() -> new NotFoundException("Item not found")); + if (!item.getAvailable()) throw new ValidationException("Not available"); + if (item.getOwner().getId().equals(userId)) throw new NotFoundException("Owner cannot book"); + if (bookingDto.getEnd().isBefore(bookingDto.getStart()) || bookingDto.getEnd().equals(bookingDto.getStart())) { + throw new ValidationException("Wrong dates"); + } + Booking booking = new Booking(); + booking.setStart(bookingDto.getStart()); + booking.setEnd(bookingDto.getEnd()); + booking.setItem(item); + booking.setBooker(booker); + booking.setStatus(Status.WAITING); + return BookingMapper.toBookingResponse(bookingRepository.save(booking)); + } + + @Transactional + @Override + public BookingResponseDto approve(Long userId, Long bookingId, boolean approved) { + Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException("Not found")); + if (!booking.getItem().getOwner().getId().equals(userId)) throw new ValidationException("Not an owner"); + if (booking.getStatus() != Status.WAITING) throw new ValidationException("Already changed"); + booking.setStatus(approved ? Status.APPROVED : Status.REJECTED); + return BookingMapper.toBookingResponse(bookingRepository.save(booking)); + } + + @Override + public BookingResponseDto getById(Long userId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException("Not found")); + if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { + throw new NotFoundException("No access"); + } + return BookingMapper.toBookingResponse(booking); + } + + @Override + public List getAllByBooker(Long userId, String state) { + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + LocalDateTime now = LocalDateTime.now(); + List bookings; + switch (state.toUpperCase()) { + case "ALL": bookings = bookingRepository.findUserBookings(userId); break; + case "CURRENT": bookings = bookingRepository.findUserCurrent(userId, now); break; + case "PAST": bookings = bookingRepository.findUserPast(userId, now); break; + case "FUTURE": bookings = bookingRepository.findUserFuture(userId, now); break; + case "WAITING": bookings = bookingRepository.findUserStatus(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findUserStatus(userId, Status.REJECTED); break; + default: throw new ValidationException("Unknown state: " + state); + } + return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); + } + + @Override + public List getAllByOwner(Long userId, String state) { + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + LocalDateTime now = LocalDateTime.now(); + List bookings; + switch (state.toUpperCase()) { + case "ALL": bookings = bookingRepository.findOwnerBookings(userId); break; + case "CURRENT": bookings = bookingRepository.findOwnerCurrent(userId, now); break; + case "PAST": bookings = bookingRepository.findOwnerPast(userId, now); break; + case "FUTURE": bookings = bookingRepository.findOwnerFuture(userId, now); break; + case "WAITING": bookings = bookingRepository.findOwnerStatus(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findOwnerStatus(userId, Status.REJECTED); break; + default: throw new ValidationException("Unknown state: " + state); + } + return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); + } +} \ No newline at end of file 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..ffb2238 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,27 @@ package ru.practicum.shareit.booking.dto; -/** - * TODO Sprint add-bookings. - */ +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class BookingDto { -} + + @NotNull + Long itemId; + + @NotNull + @FutureOrPresent + LocalDateTime start; + + @NotNull + @Future + LocalDateTime end; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java new file mode 100644 index 0000000..cd6366d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class BookingResponseDto { + + Long id; + LocalDateTime start; + LocalDateTime end; + ItemDto item; + UserDto booker; + Status status; +} \ No newline at end of file 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..8576851 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.mapper; + +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.user.mapper.UserMapper; + +public class BookingMapper { + + public static BookingResponseDto toBookingResponse(Booking booking) { + return new BookingResponseDto( + booking.getId(), + booking.getStart(), + booking.getEnd(), + ItemMapper.toItemDto(booking.getItem()), + UserMapper.toUserDto(booking.getBooker()), + booking.getStatus() + ); + } +} \ No newline at end of file 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..5693ce5 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,39 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name = "bookings") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "start_date", nullable = false) + LocalDateTime start; + + @Column(name = "end_date", nullable = false) + LocalDateTime end; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id", nullable = false) + User booker; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + Status status; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/Status.java b/src/main/java/ru/practicum/shareit/booking/model/Status.java new file mode 100644 index 0000000..f51353b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Status.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.model; + +public enum Status { + WAITING, + APPROVED, + REJECTED, + CANCELED +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java b/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java index c74fc89..d4d9f03 100644 --- a/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java +++ b/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java @@ -3,11 +3,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.resource.NoResourceFoundException; import java.util.Map; @@ -29,10 +27,17 @@ public Map handleConflict(final ConflictException e) { return Map.of("error", e.getMessage()); } - @ExceptionHandler({MethodArgumentNotValidException.class, MissingRequestHeaderException.class}) + @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) - public Map handleValidation(final Exception e) { - log.error("400 Bad Request: {}", e.getMessage()); + public Map handleValidation(final ValidationException e) { + log.error("400 Bad Request (Validation): {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleMethodArgumentNotValid(final MethodArgumentNotValidException e) { + log.error("400 Bad Request (Argument Not Valid): {}", e.getMessage()); return Map.of("error", "Ошибка валидации данных"); } @@ -42,10 +47,4 @@ public Map handleThrowable(final Throwable e) { log.error("500 Internal Server Error: ", e); return Map.of("error", "Произошла непредвиденная ошибка"); } - - @ExceptionHandler(NoResourceFoundException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public Map handleNoResourceFound(final NoResourceFoundException e) { - return Map.of("error", "Путь не найден: " + e.getResourcePath()); - } } \ No newline at end of file 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..067eded --- /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); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/CommentRepository.java new file mode 100644 index 0000000..43f4499 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.item; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findAllByItemId(Long itemId); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 8d42ead..d667bda 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import jakarta.validation.Valid; @@ -27,11 +28,6 @@ public ItemDto update(@RequestHeader(SHARER_USER_ID) Long userId, return itemService.update(userId, itemId, itemDto); } - @GetMapping("/{itemId}") - public ItemDto getById(@PathVariable Long itemId) { - return itemService.getById(itemId); - } - @GetMapping public List getAllByOwner(@RequestHeader(SHARER_USER_ID) Long userId) { return itemService.getAllByOwnerId(userId); @@ -41,4 +37,17 @@ public List getAllByOwner(@RequestHeader(SHARER_USER_ID) Long userId) { public List search(@RequestParam String text) { return itemService.search(text); } + + @GetMapping("/{itemId}") + public ItemDto getById(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId) { + return itemService.getById(itemId, userId); + } + + @PostMapping("/{itemId}/comment") + public CommentDto createComment(@RequestHeader(SHARER_USER_ID) Long userId, + @PathVariable Long itemId, + @Valid @RequestBody CommentDto commentDto) { + return itemService.createComment(userId, itemId, commentDto); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/ItemRepository.java index 868fe5f..a3839c1 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/ItemRepository.java @@ -1,18 +1,20 @@ package ru.practicum.shareit.item; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ru.practicum.shareit.item.model.Item; -import java.util.List; -import java.util.Optional; - -public interface ItemRepository { - - Item save(Item item); - Item update(Item item); +import java.util.List; - Optional findById(Long id); +public interface ItemRepository extends JpaRepository { List findAllByOwnerId(Long ownerId); - List search(String text); + @Query("SELECT it FROM Item it " + + "WHERE it.available = true " + + "AND (LOWER(it.name) LIKE LOWER(CONCAT('%', :text, '%')) " + + "OR LOWER(it.description) LIKE LOWER(CONCAT('%', :text, '%')))") + List searchByText(@Param("text") String text); + } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java b/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java deleted file mode 100644 index 32ab51c..0000000 --- a/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package ru.practicum.shareit.item; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; -import java.util.stream.Collectors; - -@Repository -public class ItemRepositoryImpl implements ItemRepository { - private final Map items = new HashMap<>(); - private long idCounter = 1; - - @Override - public Item save(Item item) { - item.setId(idCounter++); - items.put(item.getId(), item); - return item; - } - - @Override - public Item update(Item item) { - items.put(item.getId(), item); - return item; - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(items.get(id)); - } - - @Override - public List findAllByOwnerId(Long ownerId) { - return items.values().stream() - .filter(item -> item.getOwnerId().equals(ownerId)) - .collect(Collectors.toList()); - } - - @Override - public List search(String text) { - if (text == null || text.isBlank()) { - return Collections.emptyList(); - } - String lowerCaseText = text.toLowerCase(); - - return items.values().stream() - .filter(item -> Boolean.TRUE.equals(item.getAvailable())) // Безопасная проверка на true - .filter(item -> (item.getName() != null && item.getName().toLowerCase().contains(lowerCaseText)) || - (item.getDescription() != null && item.getDescription().toLowerCase().contains(lowerCaseText))) - .collect(Collectors.toList()); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java index 5a8ed97..c907238 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -1,6 +1,8 @@ package ru.practicum.shareit.item; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; + import java.util.List; public interface ItemService { @@ -9,9 +11,11 @@ public interface ItemService { ItemDto update(Long userId, Long itemId, ItemDto itemDto); - ItemDto getById(Long itemId); + ItemDto getById(Long itemId, Long userId); List getAllByOwnerId(Long ownerId); List search(String text); + + CommentDto createComment(Long userId, Long itemId, CommentDto commentDto); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index dcfe0b4..582c770 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -2,76 +2,106 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.BookingRepository; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; 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.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.user.User; import ru.practicum.shareit.user.UserRepository; - -import java.util.List; +import java.time.LocalDateTime; +import java.util.*; import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; - private final UserRepository userRepository; // Нужно для проверки существования юзера + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; + @Transactional @Override public ItemDto create(Long userId, ItemDto itemDto) { - checkUserExists(userId); - Item item = ItemMapper.toItem(itemDto, userId); + User owner = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + Item item = ItemMapper.toItem(itemDto, owner); return ItemMapper.toItemDto(itemRepository.save(item)); } + @Transactional @Override public ItemDto update(Long userId, Long itemId, ItemDto itemDto) { - checkUserExists(userId); - Item existingItem = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Вещь не найдена")); - - // Обновлять может только владелец. Тесты Postman ожидают 404, если владелец не совпадает. - if (!existingItem.getOwnerId().equals(userId)) { - throw new NotFoundException("Вы не являетесь владельцем этой вещи"); - } - - // Частичное обновление (PATCH) - if (itemDto.getName() != null && !itemDto.getName().isBlank()) { - existingItem.setName(itemDto.getName()); - } - if (itemDto.getDescription() != null && !itemDto.getDescription().isBlank()) { - existingItem.setDescription(itemDto.getDescription()); - } - if (itemDto.getAvailable() != null) { - existingItem.setAvailable(itemDto.getAvailable()); - } - - return ItemMapper.toItemDto(itemRepository.update(existingItem)); + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); + if (!item.getOwner().getId().equals(userId)) throw new NotFoundException("Not an owner"); + if (itemDto.getName() != null) item.setName(itemDto.getName()); + if (itemDto.getDescription() != null) item.setDescription(itemDto.getDescription()); + if (itemDto.getAvailable() != null) item.setAvailable(itemDto.getAvailable()); + return ItemMapper.toItemDto(itemRepository.save(item)); } @Override - public ItemDto getById(Long itemId) { - Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Вещь не найдена")); - return ItemMapper.toItemDto(item); + public ItemDto getById(Long itemId, Long userId) { + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); + ItemDto dto = ItemMapper.toItemDto(item); + dto.setComments(commentRepository.findAllByItemId(itemId).stream() + .map(CommentMapper::toCommentDto).collect(Collectors.toList())); + if (item.getOwner().getId().equals(userId)) { + setBookings(dto, bookingRepository.findItemBookings(itemId)); + } + return dto; } @Override public List getAllByOwnerId(Long ownerId) { - return itemRepository.findAllByOwnerId(ownerId).stream() - .map(ItemMapper::toItemDto) - .collect(Collectors.toList()); + return itemRepository.findAllByOwnerId(ownerId).stream().map(item -> { + ItemDto dto = ItemMapper.toItemDto(item); + dto.setComments(commentRepository.findAllByItemId(item.getId()).stream() + .map(CommentMapper::toCommentDto).collect(Collectors.toList())); + setBookings(dto, bookingRepository.findItemBookings(item.getId())); + return dto; + }).sorted(Comparator.comparing(ItemDto::getId)).collect(Collectors.toList()); } @Override public List search(String text) { - return itemRepository.search(text).stream() - .map(ItemMapper::toItemDto) - .collect(Collectors.toList()); + if (text == null || text.isBlank()) return Collections.emptyList(); + return itemRepository.searchByText(text).stream() + .map(ItemMapper::toItemDto).collect(Collectors.toList()); + } + + @Transactional + @Override + public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { + User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); + List userBookings = bookingRepository.findUserPast(userId, LocalDateTime.now()); + boolean hasFinishedBooking = userBookings.stream() + .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); + if (!hasFinishedBooking) throw new ValidationException("No finished bookings"); + Comment comment = new Comment(); + comment.setText(commentDto.getText()); + comment.setItem(item); + comment.setAuthor(user); + comment.setCreated(LocalDateTime.now()); + return CommentMapper.toCommentDto(commentRepository.save(comment)); } - private void checkUserExists(Long userId) { - userRepository.findById(userId) - .orElseThrow(() -> new NotFoundException("Пользователь с ID " + userId + " не найден")); + private void setBookings(ItemDto dto, List bookings) { + LocalDateTime now = LocalDateTime.now(); + Booking last = bookings.stream().filter(b -> b.getStart().isBefore(now)) + .reduce((first, second) -> second).orElse(null); + Booking next = bookings.stream().filter(b -> b.getStart().isAfter(now)) + .findFirst().orElse(null); + if (last != null) dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); + if (next != null) dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); } } \ No newline at end of file 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..f50e263 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CommentDto { + Long id; + @NotBlank + String text; + String authorName; + LocalDateTime created; +} \ No newline at end of file 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 a581693..3a66de4 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -1,9 +1,10 @@ package ru.practicum.shareit.item.dto; -import lombok.*; -import lombok.experimental.FieldDefaults; // Нужно импортировать это import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.FieldDefaults; +import java.util.List; @Data @AllArgsConstructor @@ -11,13 +12,21 @@ @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemDto { Long id; - - @NotBlank(message = "Название не может быть пустым") + @NotBlank String name; - - @NotBlank(message = "Описание не может быть пустым") + @NotBlank String description; - - @NotNull(message = "Статус доступности обязателен") + @NotNull Boolean available; + BookingShortDto lastBooking; + BookingShortDto nextBooking; + List comments; + + @Data + @AllArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class BookingShortDto { + Long id; + Long bookerId; + } } \ No newline at end of file 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..9eef3d8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.mapper; + +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.model.Comment; + +public class CommentMapper { + public static CommentDto toCommentDto(Comment comment) { + return new CommentDto( + comment.getId(), + comment.getText(), + comment.getAuthor().getName(), + comment.getCreated() + ); + } +} \ No newline at end of file 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 ef56554..e368b4d 100644 --- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -2,6 +2,8 @@ import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; +import java.util.ArrayList; public class ItemMapper { public static ItemDto toItemDto(Item item) { @@ -9,17 +11,21 @@ public static ItemDto toItemDto(Item item) { item.getId(), item.getName(), item.getDescription(), - item.getAvailable() + item.getAvailable(), + null, + null, + new ArrayList<>() ); } - public static Item toItem(ItemDto itemDto, Long ownerId) { + public static Item toItem(ItemDto itemDto, User owner) { return new Item( itemDto.getId(), itemDto.getName(), itemDto.getDescription(), itemDto.getAvailable(), - ownerId + owner, + null ); } } \ No newline at end of file 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..1bb5d6c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,34 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name = "comments") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(nullable = false) + String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) // Маппинг на твой SQL + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) // Маппинг на твой SQL + User author; + + @Column(nullable = false) + LocalDateTime created; +} \ No newline at end of file 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 1f9aee1..38b036c 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,16 +1,45 @@ package ru.practicum.shareit.item.model; -import lombok.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.User; @Data -@AllArgsConstructor +@Entity +@Table(name = "items") @NoArgsConstructor +@AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + + @Column(nullable = false) String name; + + @Column(nullable = false) String description; + + @Column(name = "is_available", nullable = false) Boolean available; - Long ownerId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) + User owner; + + @Column(name = "request_id") + Long requestId; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java index 0728398..a976191 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/User.java @@ -1,15 +1,31 @@ package ru.practicum.shareit.user; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; @Data +@Entity +@Table(name = "users") +@NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + + @Column(nullable = false) String name; + + @Column(nullable = false, unique = true) String email; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserRepository.java b/src/main/java/ru/practicum/shareit/user/UserRepository.java index 18db724..97fdbc6 100644 --- a/src/main/java/ru/practicum/shareit/user/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/UserRepository.java @@ -1,22 +1,11 @@ package ru.practicum.shareit.user; -import ru.practicum.shareit.user.User; -import java.util.List; -import java.util.Optional; - -public interface UserRepository { - - User save(User user); +import org.springframework.data.jpa.repository.JpaRepository; - User update(User user); - - Optional findById(Long id); - - List findAll(); +import java.util.Optional; - void deleteById(Long id); +public interface UserRepository extends JpaRepository { - boolean existsByEmail(String email); + Optional findByEmail(String email); - boolean existsByEmailAndNotId(String email, Long id); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java b/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java deleted file mode 100644 index bd6f995..0000000 --- a/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package ru.practicum.shareit.user; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.user.User; - -import java.util.*; - -@Repository -public class UserRepositoryImpl implements UserRepository { - private final Map users = new HashMap<>(); - private long idCounter = 1; - - @Override - public User save(User user) { - user.setId(idCounter++); - users.put(user.getId(), user); - return user; - } - - @Override - public User update(User user) { - users.put(user.getId(), user); - return user; - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(users.get(id)); - } - - @Override - public List findAll() { - return new ArrayList<>(users.values()); - } - - @Override - public void deleteById(Long id) { - users.remove(id); - } - - @Override - public boolean existsByEmail(String email) { - return users.values().stream() - .anyMatch(user -> user.getEmail().equals(email)); - } - - @Override - public boolean existsByEmailAndNotId(String email, Long id) { - return users.values().stream() - .anyMatch(user -> user.getEmail().equals(email) && !user.getId().equals(id)); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index c105645..78c835d 100644 --- a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -2,45 +2,51 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.exception.ConflictException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.mapper.UserMapper; -import ru.practicum.shareit.user.User; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; + @Transactional @Override public UserDto create(UserDto userDto) { - if (userRepository.existsByEmail(userDto.getEmail())) { + Optional existingUser = userRepository.findByEmail(userDto.getEmail()); + if (existingUser.isPresent()) { throw new ConflictException("Email уже используется"); } User user = UserMapper.toUser(userDto); return UserMapper.toUserDto(userRepository.save(user)); } + @Transactional @Override public UserDto update(Long id, UserDto userDto) { - User existingUser = userRepository.findById(id) + User userToUpdate = userRepository.findById(id) .orElseThrow(() -> new NotFoundException("Пользователь не найден")); if (userDto.getEmail() != null && !userDto.getEmail().isBlank()) { - if (userRepository.existsByEmailAndNotId(userDto.getEmail(), id)) { + Optional userWithSameEmail = userRepository.findByEmail(userDto.getEmail()); + if (userWithSameEmail.isPresent() && !userWithSameEmail.get().getId().equals(id)) { throw new ConflictException("Email уже используется другим пользователем"); } - existingUser.setEmail(userDto.getEmail()); + userToUpdate.setEmail(userDto.getEmail()); } if (userDto.getName() != null && !userDto.getName().isBlank()) { - existingUser.setName(userDto.getName()); + userToUpdate.setName(userDto.getName()); } - return UserMapper.toUserDto(userRepository.update(existingUser)); + return UserMapper.toUserDto(userRepository.save(userToUpdate)); } @Override @@ -57,6 +63,7 @@ public List getAll() { .collect(Collectors.toList()); } + @Transactional @Override public void deleteById(Long id) { userRepository.deleteById(id); diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java index c188010..8efd577 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -7,10 +7,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; @Data @AllArgsConstructor +@NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class UserDto { Long id; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b0f09a3..56b6039 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,16 +1,14 @@ server.port=8080 -#spring.jpa.hibernate.ddl-auto=none -#spring.jpa.properties.hibernate.format_sql=true -#spring.sql.init.mode=always +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:h2:mem:shareit;DB_CLOSE_DELAY=-1} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:sa} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:} -#logging.level.org.springframework.orm.jpa=INFO -#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.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false + +spring.sql.init.mode=always + +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always \ 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..2e2e066 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,41 @@ +DROP TABLE IF EXISTS comments CASCADE; +DROP TABLE IF EXISTS bookings CASCADE; +DROP TABLE IF EXISTS items CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +CREATE TABLE users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL UNIQUE +); + +CREATE TABLE items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(1000) NOT NULL, + is_available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + request_id BIGINT, + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + 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(20) NOT NULL, + CONSTRAINT fk_booking_item FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE, + CONSTRAINT fk_booking_booker FOREIGN KEY (booker_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(2000) NOT NULL, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT fk_comment_item FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE, + CONSTRAINT fk_comment_author FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE CASCADE +); \ No newline at end of file