From ac8ce6ba454c22bf8f1e63c185eb3a5b12127598 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Fri, 5 Dec 2025 21:05:46 +0300 Subject: [PATCH 01/14] feat: added spring data jpa, moved to jpa repositories for storing data --- docker-compose.yaml | 19 +++++ pom.xml | 7 ++ .../ru/practicum/shareit/item/ItemMapper.java | 4 +- .../shareit/item/ItemServiceImpl.java | 6 +- .../practicum/shareit/item/dto/ItemDto.java | 4 +- .../ru/practicum/shareit/item/model/Item.java | 25 +++++-- .../repository/InMemoryItemRepository.java | 56 -------------- .../item/repository/ItemRepository.java | 21 +++--- .../shareit/user/UserServiceImpl.java | 4 +- .../ru/practicum/shareit/user/model/User.java | 18 +++-- .../repository/InMemoryUserRepository.java | 74 ------------------- .../user/repository/UserRepository.java | 15 +--- src/main/resources/application.properties | 9 +-- src/main/resources/schema.sql | 19 +++++ .../resources/application-test.properties | 0 15 files changed, 101 insertions(+), 180 deletions(-) create mode 100644 docker-compose.yaml delete mode 100644 src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java delete mode 100644 src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java create mode 100644 src/main/resources/schema.sql rename src/{main => test/java}/resources/application-test.properties (100%) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2e4a2b4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,19 @@ +services: + db: + image: postgres:17.7-alpine3.23 + container_name: shareit-postgres-db + ports: + - "5432:5432" + environment: + - POSTGRES_DB=shareit + - POSTGRES_USER=shareit_user + - POSTGRES_PASSWORD=secret + volumes: + - postgres-data:/var/lib/postgresql/data/ + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 +volumes: + postgres-data: diff --git a/pom.xml b/pom.xml index ac238ab..4c9d34f 100644 --- a/pom.xml +++ b/pom.xml @@ -24,16 +24,23 @@ org.springframework.boot spring-boot-starter-web + org.springframework.boot spring-boot-starter-actuator + org.springframework.boot spring-boot-configuration-processor true + + org.springframework.boot + spring-boot-starter-data-jpa + + org.postgresql postgresql diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java index 93d3883..5fce03a 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -14,8 +14,8 @@ public ItemDto toItemDto(Item item) { item.getId(), item.getName(), item.getDescription(), - item.isAvailable(), - item.getRequest() != null ? item.getRequest().getId() : null + item.isAvailable() + // item.getRequest() != null ? item.getRequest().getId() : null ); } diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 13609f2..2c647c9 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -36,7 +36,7 @@ public ItemDto getItemOfUserById(long userId, long itemId) { public Collection getAllItemsOfUser(long userId) { getUserOrThrow(userId); - return itemRepository.findAllByUserId(userId) + return itemRepository.findAllByOwnerId(userId) .stream() .map(ItemMapper::toItemDto) .toList(); @@ -53,7 +53,6 @@ public ItemDto saveItem(long userId, NewItemDto newItem) { @Override public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) { - getUserOrThrow(userId); Item item = getItemOrThrow(itemId); if (item.getOwner().getId() != userId) { throw new ForbiddenAccessException("Only owner of item can update it"); @@ -70,7 +69,8 @@ public Collection searchItems(String query) { return Collections.emptyList(); } - return itemRepository.searchItems(query) + + return itemRepository.search(query) .stream() .map(ItemMapper::toItemDto) .toList(); 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 30863dd..674b6ac 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -4,7 +4,7 @@ public record ItemDto( Long id, String name, String description, - boolean available, - Long requestId + boolean available + // Long requestId ) { } 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 775a4cd..379bd9f 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,23 +1,32 @@ package ru.practicum.shareit.item.model; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import lombok.experimental.FieldDefaults; -import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Getter +@Setter @NoArgsConstructor @AllArgsConstructor +@Table(name = "items") +@FieldDefaults(level = AccessLevel.PRIVATE) public class Item { + @Id + @Column(name = "item_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String name; + String description; + boolean available; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id") User owner; - ItemRequest request; + // ItemRequest request; } diff --git a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java deleted file mode 100644 index a74f783..0000000 --- a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java +++ /dev/null @@ -1,56 +0,0 @@ -package ru.practicum.shareit.item.repository; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -@Repository -public class InMemoryItemRepository implements ItemRepository { - private final Map items = new HashMap<>(); - - @Override - public Optional findById(long id) { - return Optional.ofNullable(items.get(id)); - } - - @Override - public Collection findAllByUserId(long userId) { - return items.values() - .stream() - .filter(item -> item.getOwner().getId() == userId) - .toList(); - } - - @Override - public Item save(Item item) { - Long id = generateNextId(); - item.setId(id); - - items.put(id, item); - - return item; - } - - @Override - public Collection searchItems(String query) { - String lowercaseQuery = query.trim().toLowerCase(); - - return items.values() - .stream() - .filter(Item::isAvailable) - .filter(item -> item.getName().toLowerCase().contains(lowercaseQuery) - || item.getDescription().toLowerCase().contains(lowercaseQuery)) - .toList(); - } - - private Long generateNextId() { - Long nextId = items.keySet().stream() - .max(Long::compareTo) - .orElse(0L); - return ++nextId; - } -} diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index 6426fb6..c911140 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -1,16 +1,19 @@ package ru.practicum.shareit.item.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.item.model.Item; -import java.util.Collection; -import java.util.Optional; +import java.util.List; -public interface ItemRepository { - Optional findById(long id); +public interface ItemRepository extends JpaRepository { + List findAllByOwnerId(Long userId); - Collection findAllByUserId(long userId); - - Item save(Item item); - - Collection searchItems(String query); + @Query(""" + select i from Item i + where i.available = true + and (lower(i.name)) like lower(concat('%', :query, '%')) + or lower(i.description) like lower(concat('%', :query, '%')) + """) + List search(String query); } diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index bb48c34..a62f2dc 100644 --- a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -42,14 +42,14 @@ public UserDto save(NewUserDto newUser) { public UserDto update(UpdateUserDto newUser) { User user = getUserOrThrow(newUser.getId()); User updatedUser = UserMapper.updateUser(user, newUser); - userRepository.update(updatedUser); + userRepository.save(updatedUser); return UserMapper.toUserDto(updatedUser); } @Override public void delete(long id) { getUserOrThrow(id); - userRepository.delete(id); + userRepository.deleteById(id); } private User getUserOrThrow(long id) { diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java index 7cd8314..1b0b3dc 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,23 @@ package ru.practicum.shareit.user.model; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; import lombok.experimental.FieldDefaults; -@Data -@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Setter +@Getter @NoArgsConstructor @AllArgsConstructor +@Table(name = "users") +@FieldDefaults(level = AccessLevel.PRIVATE) public class User { + @Id + @Column(name = "user_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + String name; + String email; } diff --git a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java deleted file mode 100644 index 7d7bd18..0000000 --- a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java +++ /dev/null @@ -1,74 +0,0 @@ -package ru.practicum.shareit.user.repository; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.exception.DuplicateDataException; -import ru.practicum.shareit.user.model.User; - -import java.util.*; - -@Repository -public class InMemoryUserRepository implements UserRepository { - private final Map users = new HashMap<>(); - private final Set userEmails = new HashSet<>(); - - @Override - public Collection findAll() { - return Collections.unmodifiableCollection(users.values()); - } - - @Override - public Optional findById(long id) { - User user = users.get(id); - if (user == null) { - return Optional.empty(); - } - - return Optional.of(new User( - user.getId(), - user.getName(), - user.getEmail())); - } - - @Override - public User save(User user) { - Long id = generateNextId(); - user.setId(id); - - if (userEmails.contains(user.getEmail())) { - throw new DuplicateDataException("email %s already exists".formatted(user.getEmail())); - } - - users.put(id, user); - userEmails.add(user.getEmail()); - return user; - } - - @Override - public void update(User user) { - User currentUser = users.get(user.getId()); - - if (!currentUser.getEmail().equals(user.getEmail())) { - if (userEmails.contains(user.getEmail())) { - throw new DuplicateDataException("email %s already exists".formatted(user.getEmail())); - } - - userEmails.remove(currentUser.getEmail()); - userEmails.add(user.getEmail()); - } - - users.put(user.getId(), user); - } - - @Override - public void delete(long id) { - User removedUser = users.remove(id); - userEmails.remove(removedUser.getEmail()); - } - - private Long generateNextId() { - Long nextId = users.keySet().stream() - .max(Long::compareTo) - .orElse(0L); - return ++nextId; - } -} diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java index be812dc..ef10484 100644 --- a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -1,19 +1,8 @@ package ru.practicum.shareit.user.repository; +import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.user.model.User; -import java.util.Collection; -import java.util.Optional; +public interface UserRepository extends JpaRepository { -public interface UserRepository { - - Collection findAll(); - - Optional findById(long id); - - User save(User user); - - void update(User user); - - void delete(long id); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 51c5180..2304185 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://localhost:5432/shareit +spring.datasource.username=shareit_user +spring.datasource.password=secret diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..fac8439 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS users +( + user_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + + CONSTRAINT unique_user_email UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS items +( + item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(512) NOT NULL, + description VARCHAR(1024) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) ON DELETE CASCADE +); diff --git a/src/main/resources/application-test.properties b/src/test/java/resources/application-test.properties similarity index 100% rename from src/main/resources/application-test.properties rename to src/test/java/resources/application-test.properties From d6e9c2b04faeb2d9e17d048de0fd3b1999884da7 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sat, 6 Dec 2025 20:26:23 +0300 Subject: [PATCH 02/14] feat: add bookings --- .../ru/practicum/shareit/booking/Booking.java | 7 - .../shareit/booking/BookingController.java | 62 +++++++- .../shareit/booking/BookingMapper.java | 54 +++++++ .../shareit/booking/BookingService.java | 18 +++ .../shareit/booking/BookingServiceImpl.java | 142 ++++++++++++++++++ .../shareit/booking/BookingState.java | 24 +++ .../shareit/booking/dto/BookingDto.java | 15 +- .../booking/dto/BookingResponseDto.java | 16 ++ .../shareit/booking/model/Booking.java | 42 ++++++ .../shareit/booking/model/BookingStatus.java | 8 + .../booking/repository/BookingRepository.java | 79 ++++++++++ .../exception/ItemUnavailableException.java | 7 + .../handler/GlobalExceptionHandler.java | 8 + .../shareit/item/ItemServiceImpl.java | 5 +- .../shareit/user/UserServiceImpl.java | 5 + src/main/resources/schema.sql | 18 +++ 16 files changed, 493 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/booking/Booking.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingMapper.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingService.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingState.java create mode 100644 src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java create mode 100644 src/main/java/ru/practicum/shareit/booking/model/Booking.java create mode 100644 src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java create mode 100644 src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java create mode 100644 src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java 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..0536d8b 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,64 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; -/** - * TODO Sprint add-bookings. - */ +import java.util.List; + +@Validated @RestController +@RequiredArgsConstructor @RequestMapping(path = "/bookings") public class BookingController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final BookingService bookingService; + + @PostMapping + public BookingResponseDto createBooking( + @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, + @RequestBody @Valid BookingDto bookingDto + ) { + return bookingService.createBooking(bookerId, bookingDto); + } + + @PatchMapping("{bookingId}") + public BookingResponseDto approveBooking( + @PathVariable long bookingId, + @RequestParam boolean approved, + @RequestHeader(SHARER_USER_ID_HEADER) long ownerId + ) { + return bookingService.approveBooking(bookingId, approved, ownerId); + } + + @GetMapping("{bookingId}") + public BookingResponseDto getBooking( + @PathVariable long bookingId, + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + return bookingService.getBookingById(bookingId, userId); + } + + @GetMapping + public List getAllBookingsOfUser( + @RequestParam(defaultValue = "ALL") String state, + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + BookingState bookingState = BookingState.fromString(state) + .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); + return bookingService.getAllBookingsOfUser(userId, bookingState); + } + + @GetMapping("/owner") + public List getBookingsByOwner( + @RequestParam(defaultValue = "ALL") String state, + @RequestHeader(SHARER_USER_ID_HEADER) long ownerId + ) { + BookingState bookingState = BookingState.fromString(state) + .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); + return bookingService.getAllBookingsByOwner(ownerId, bookingState); + } } 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..eff29ab --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.booking; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@UtilityClass +public class BookingMapper { + public BookingResponseDto toBookingResponseDto(Booking booking) { + Long bookingId = booking.getId(); + LocalDateTime bookingStartTime = booking.getStartTime(); + LocalDateTime bookingEndTime = booking.getEndTime(); + Long itemId = booking.getItem().getId(); + String itemName = booking.getItem().getName(); + String itemDescription = booking.getItem().getDescription(); + boolean itemAvailable = booking.getItem().isAvailable(); + ItemDto itemDto = new ItemDto( + itemId, + itemName, + itemDescription, + itemAvailable + ); + Long userId = booking.getBooker().getId(); + String userName = booking.getBooker().getName(); + String userEmail = booking.getBooker().getEmail(); + UserDto userDto = new UserDto(userId, userName, userEmail); + BookingStatus bookingStatus = booking.getStatus(); + return new BookingResponseDto( + bookingId, + bookingStartTime, + bookingEndTime, + itemDto, + userDto, + bookingStatus + ); + } + + public static Booking fromDto(BookingDto bookingDto, User booker, Item item) { + return new Booking(null, + bookingDto.start(), + bookingDto.end(), + item, + booker, + BookingStatus.WAITING); + } +} 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..4497a45 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,18 @@ +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 createBooking(long bookerId, BookingDto bookingDto); + + BookingResponseDto approveBooking(long bookingId, boolean approved, long ownerId); + + BookingResponseDto getBookingById(long bookingId, long userId); + + List getAllBookingsOfUser(long userId, BookingState bookingState); + + List getAllBookingsByOwner(long ownerId, BookingState bookingState); +} 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..c8d04a1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -0,0 +1,142 @@ +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.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.ItemUnavailableException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final ItemRepository itemRepository; + private final UserRepository userRepository; + + @Override + @Transactional + public BookingResponseDto createBooking(long bookerId, BookingDto bookingDto) { + User booker = getUserOrThrow(bookerId); + Item item = getItemOrThrow(bookingDto.itemId()); + + if (!item.isAvailable()) { + throw new ItemUnavailableException("Item with id %d is not available for booking"); + } + + checkBookingDates(bookingDto.start(), bookingDto.end()); + + Booking booking = BookingMapper.fromDto(bookingDto, booker, item); + Booking savedBooking = bookingRepository.save(booking); + return BookingMapper.toBookingResponseDto(savedBooking); + } + + @Override + @Transactional + public BookingResponseDto approveBooking(long bookingId, boolean approved, long ownerId) { + Booking booking = getBookingOrThrow(bookingId); + if (booking.getItem().getOwner().getId() != ownerId) { + throw new ForbiddenAccessException("Booking can only be approved by owner of item"); + } + + getUserOrThrow(ownerId); + + booking.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + Booking saved = bookingRepository.save(booking); + return BookingMapper.toBookingResponseDto(saved); + } + + @Override + public BookingResponseDto getBookingById(long bookingId, long userId) { + Booking booking = getBookingOrThrow(bookingId); + getUserOrThrow(userId); + + if (booking.getItem().getOwner().getId() != userId + && booking.getBooker().getId() != userId) { + throw new ForbiddenAccessException("No access to booking"); + } + + return BookingMapper.toBookingResponseDto(booking); + } + + @Override + public List getAllBookingsOfUser(long userId, BookingState bookingState) { + getUserOrThrow(userId); + + List bookings = switch (bookingState) { + case ALL -> bookingRepository.findByBookerIdOrderByStartTimeDesc(userId); + case CURRENT -> bookingRepository.findCurrentByBooker(userId, LocalDateTime.now()); + case PAST -> bookingRepository.findByBookerIdAndEndTimeBeforeOrderByStartTimeDesc( + userId, LocalDateTime.now()); + case FUTURE -> bookingRepository.findByBookerIdAndStartTimeAfterOrderByStartTimeDesc( + userId, LocalDateTime.now()); + case WAITING -> bookingRepository.findByBookerIdAndStatusOrderByStartTimeDesc( + userId, BookingStatus.WAITING); + case REJECTED -> bookingRepository.findByBookerIdAndStatusOrderByStartTimeDesc( + userId, BookingStatus.REJECTED); + }; + return bookings.stream() + .map(BookingMapper::toBookingResponseDto) + .toList(); + } + + @Override + public List getAllBookingsByOwner(long ownerId, BookingState bookingState) { + getUserOrThrow(ownerId); + + List bookings = switch (bookingState) { + case ALL -> bookingRepository.findByItemOwnerIdOrderByStartTimeDesc(ownerId); + case CURRENT -> bookingRepository.findCurrentByOwner(ownerId, LocalDateTime.now()); + case PAST -> bookingRepository.findByItemOwnerIdAndEndTimeBeforeOrderByStartTimeDesc( + ownerId, LocalDateTime.now()); + case FUTURE -> bookingRepository.findByItemOwnerIdAndStartTimeAfterOrderByStartTimeDesc( + ownerId, LocalDateTime.now()); + case WAITING -> bookingRepository.findByItemOwnerIdAndStatusOrderByStartTimeDesc( + ownerId, BookingStatus.WAITING); + case REJECTED -> bookingRepository.findByItemOwnerIdAndStatusOrderByStartTimeDesc( + ownerId, BookingStatus.REJECTED); + }; + + return bookings.stream() + .map(BookingMapper::toBookingResponseDto) + .toList(); + } + + private User getUserOrThrow(long userId) { + return userRepository.findById(userId) + .orElseThrow(NotFoundException.supplier("User with id:%d not found", userId)); + } + + private Item getItemOrThrow(long itemId) { + return itemRepository.findById(itemId) + .orElseThrow(NotFoundException.supplier("Item with id:%d not found", itemId)); + } + + private Booking getBookingOrThrow(long bookingId) { + return bookingRepository.findById(bookingId) + .orElseThrow(NotFoundException.supplier("Booking with id:%d not found", bookingId)); + } + + private void checkBookingDates(LocalDateTime start, LocalDateTime end) { + if (start.isAfter(end)) { + throw new IllegalArgumentException("Start booking date is after end date"); + } + + if (start.equals(end)) { + throw new IllegalArgumentException("Booking start and end date must not be the same"); + } + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/src/main/java/ru/practicum/shareit/booking/BookingState.java new file mode 100644 index 0000000..0d14b1e --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.booking; + +import java.util.Optional; + +public enum BookingState { + ALL, + CURRENT, + PAST, + FUTURE, + WAITING, + REJECTED; + + public static Optional fromString(String state) { + return Optional.ofNullable(switch (state.toUpperCase()) { + case "ALL" -> ALL; + case "CURRENT" -> CURRENT; + case "PAST" -> PAST; + case "FUTURE" -> FUTURE; + case "WAITING" -> WAITING; + case "REJECTED" -> REJECTED; + default -> null; + }); + } +} 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..2e091b9 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,14 @@ package ru.practicum.shareit.booking.dto; -/** - * TODO Sprint add-bookings. - */ -public class BookingDto { +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record BookingDto( + long itemId, + @FutureOrPresent @NotNull LocalDateTime start, + @Future @NotNull LocalDateTime end +) { } 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..b705ae1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.booking.dto; + +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +public record BookingResponseDto(Long id, + LocalDateTime start, + LocalDateTime end, + ItemDto item, + UserDto booker, + BookingStatus status +) { +} 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..dd725f8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,42 @@ +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.model.User; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "bookings") +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "booking_id") + Long id; + + @Column(name = "start_time") + LocalDateTime startTime; + + @Column(name = "end_time") + LocalDateTime endTime; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id") + User booker; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + BookingStatus status = BookingStatus.WAITING; + +} diff --git a/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java new file mode 100644 index 0000000..adad7d1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.model; + +public enum BookingStatus { + WAITING, + APPROVED, + REJECTED, + CANCELLED +} diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 0000000..bcf3f47 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,79 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; + +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + //booker methods + // ALL + List findByBookerIdOrderByStartTimeDesc(Long bookerId); + + // CURRENT + @Query(""" + SELECT b FROM Booking b + WHERE b.booker.id = :userId + AND :now BETWEEN b.startTime AND b.endTime + ORDER BY b.startTime DESC + """) + List findCurrentByBooker( + Long userId, + LocalDateTime now + ); + + // PAST + List findByBookerIdAndEndTimeBeforeOrderByStartTimeDesc( + Long bookerId, + LocalDateTime now + ); + + // FUTURE + List findByBookerIdAndStartTimeAfterOrderByStartTimeDesc( + Long bookerId, + LocalDateTime now + ); + + // WAITING / REJECTED + List findByBookerIdAndStatusOrderByStartTimeDesc( + Long bookerId, + BookingStatus status + ); + + //owner methods + // ALL + List findByItemOwnerIdOrderByStartTimeDesc(Long ownerId); + + // CURRENT + @Query(""" + SELECT b FROM Booking b + WHERE b.item.owner.id = :ownerId + AND :now BETWEEN b.startTime AND b.endTime + ORDER BY b.startTime DESC + """) + List findCurrentByOwner( + Long ownerId, + LocalDateTime now + ); + + // PAST + List findByItemOwnerIdAndEndTimeBeforeOrderByStartTimeDesc( + Long ownerId, + LocalDateTime now + ); + + // FUTURE + List findByItemOwnerIdAndStartTimeAfterOrderByStartTimeDesc( + Long ownerId, + LocalDateTime now + ); + + // WAITING / REJECTED + List findByItemOwnerIdAndStatusOrderByStartTimeDesc( + Long ownerId, + BookingStatus status + ); +} diff --git a/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java b/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java new file mode 100644 index 0000000..5d97c04 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ItemUnavailableException extends RuntimeException { + public ItemUnavailableException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index 8e95630..1e1df2b 100644 --- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import ru.practicum.shareit.exception.DuplicateDataException; import ru.practicum.shareit.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.ItemUnavailableException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.dto.ErrorResponse; import ru.practicum.shareit.exception.dto.ValidationErrorResponse; @@ -65,6 +66,13 @@ public ErrorResponse onNotFoundException(NotFoundException ex) { return new ErrorResponse("not found", ex.getMessage()); } + @ExceptionHandler(ItemUnavailableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onItemUnavailableException(ItemUnavailableException ex) { + log.warn("Item unavailable exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("unavailable", ex.getMessage()); + } + @ExceptionHandler(DuplicateDataException.class) @ResponseStatus(HttpStatus.CONFLICT) public ErrorResponse onDuplicateDataException(DuplicateDataException ex) { diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 2c647c9..685fdfe 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.exception.ForbiddenAccessException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.item.dto.ItemDto; @@ -18,6 +19,7 @@ @Slf4j @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; @@ -43,6 +45,7 @@ public Collection getAllItemsOfUser(long userId) { } @Override + @Transactional public ItemDto saveItem(long userId, NewItemDto newItem) { User owner = getUserOrThrow(userId); Item item = ItemMapper.toItem(newItem); @@ -52,6 +55,7 @@ public ItemDto saveItem(long userId, NewItemDto newItem) { } @Override + @Transactional public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) { Item item = getItemOrThrow(itemId); if (item.getOwner().getId() != userId) { @@ -69,7 +73,6 @@ public Collection searchItems(String query) { return Collections.emptyList(); } - return itemRepository.search(query) .stream() .map(ItemMapper::toItemDto) diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index a62f2dc..d6df065 100644 --- a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.user.dto.NewUserDto; import ru.practicum.shareit.user.dto.UpdateUserDto; @@ -14,6 +15,7 @@ @Slf4j @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; @@ -32,6 +34,7 @@ public UserDto findById(long id) { } @Override + @Transactional public UserDto save(NewUserDto newUser) { User user = UserMapper.toUser(newUser); user = userRepository.save(user); @@ -39,6 +42,7 @@ public UserDto save(NewUserDto newUser) { } @Override + @Transactional public UserDto update(UpdateUserDto newUser) { User user = getUserOrThrow(newUser.getId()); User updatedUser = UserMapper.updateUser(user, newUser); @@ -47,6 +51,7 @@ public UserDto update(UpdateUserDto newUser) { } @Override + @Transactional public void delete(long id) { getUserOrThrow(id); userRepository.deleteById(id); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index fac8439..5076b73 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -17,3 +17,21 @@ CREATE TABLE IF NOT EXISTS items CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) ON DELETE CASCADE ); + +CREATE TABLE IF NOT EXISTS bookings +( + booking_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_time TIMESTAMP WITHOUT TIME ZONE NULL, + end_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + item_id BIGINT NOT NULL, + booker_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL CHECK (status IN ( + 'WAITING', + 'APPROVED', + 'REJECTED', + 'CANCELED')) + DEFAULT 'WAITING', + + CONSTRAINT fk_item FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE, + CONSTRAINT fk_booker FOREIGN KEY (booker_id) REFERENCES users(user_id) ON DELETE CASCADE +); From 53c9fb877343bb40e43a6429ed15a477990a3c81 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 13:17:13 +0300 Subject: [PATCH 03/14] feat: added booking info for itemdto when requesting all items --- .../shareit/booking/BookingMapper.java | 16 +++++++- .../shareit/booking/dto/BookingInfoDto.java | 11 ++++++ .../booking/repository/BookingRepository.java | 38 ++++++++++++++++++ .../shareit/item/ItemController.java | 3 +- .../ru/practicum/shareit/item/ItemMapper.java | 26 ++++++++++++- .../practicum/shareit/item/ItemService.java | 3 +- .../shareit/item/ItemServiceImpl.java | 39 ++++++++++++++++++- .../practicum/shareit/item/dto/ItemDto.java | 1 - .../shareit/item/dto/ItemWithBookingDto.java | 13 +++++++ 9 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java index eff29ab..cf0d650 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -2,6 +2,7 @@ import lombok.experimental.UtilityClass; import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingInfoDto; import ru.practicum.shareit.booking.dto.BookingResponseDto; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; @@ -11,6 +12,7 @@ import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; +import java.util.Optional; @UtilityClass public class BookingMapper { @@ -43,7 +45,7 @@ public BookingResponseDto toBookingResponseDto(Booking booking) { ); } - public static Booking fromDto(BookingDto bookingDto, User booker, Item item) { + public Booking fromDto(BookingDto bookingDto, User booker, Item item) { return new Booking(null, bookingDto.start(), bookingDto.end(), @@ -51,4 +53,16 @@ public static Booking fromDto(BookingDto bookingDto, User booker, Item item) { booker, BookingStatus.WAITING); } + + public Optional toBookingInfoDto(Booking booking) { + if (booking == null) { + return Optional.empty(); + } + return Optional.of(new BookingInfoDto( + booking.getId(), + booking.getBooker().getId(), + booking.getStartTime(), + booking.getEndTime() + )); + } } diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java new file mode 100644 index 0000000..9fa9780 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.booking.dto; + +import java.time.LocalDateTime; + +public record BookingInfoDto( + Long id, + Long bookerId, + LocalDateTime start, + LocalDateTime end +) { +} diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index bcf3f47..49f5929 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -9,6 +9,43 @@ import java.util.List; public interface BookingRepository extends JpaRepository { + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.id IN :itemIds + AND b.status = 'APPROVED' + AND b.endTime < :now + AND b.endTime = ( + SELECT MAX(b2.endTime) + FROM Booking b2 + WHERE b2.item.id = b.item.id + AND b2.status = 'APPROVED' + AND b2.endTime < :now + ) + """) + List findLastApprovedBookingsForItems( + List itemIds, + LocalDateTime now + ); + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.id IN :itemIds + AND b.status = 'APPROVED' + AND b.startTime > :now + AND b.startTime = ( + SELECT MIN(b2.startTime) + FROM Booking b2 + WHERE b2.item.id = b.item.id + AND b2.status = 'APPROVED' + AND b2.startTime > :now + ) + """) + List findNextApprovedBookingsForItems( + List itemIds, + LocalDateTime now + ); + //booker methods // ALL List findByBookerIdOrderByStartTimeDesc(Long bookerId); @@ -76,4 +113,5 @@ List findByItemOwnerIdAndStatusOrderByStartTimeDesc( Long ownerId, BookingStatus status ); + } diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 36ff3be..2ef7eb3 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -6,6 +6,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemWithBookingDto; import ru.practicum.shareit.item.dto.NewItemDto; import ru.practicum.shareit.item.dto.UpdateItemDto; @@ -29,7 +30,7 @@ public ItemDto getItem( } @GetMapping - public Collection getItems( + public Collection getItems( @RequestHeader(SHARER_USER_ID_HEADER) long userId ) { return itemService.getAllItemsOfUser(userId); diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java index 5fce03a..641d79a 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -1,11 +1,17 @@ package ru.practicum.shareit.item; import lombok.experimental.UtilityClass; +import ru.practicum.shareit.booking.BookingMapper; +import ru.practicum.shareit.booking.dto.BookingInfoDto; +import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemWithBookingDto; import ru.practicum.shareit.item.dto.NewItemDto; import ru.practicum.shareit.item.dto.UpdateItemDto; import ru.practicum.shareit.item.model.Item; +import java.time.LocalDateTime; + @UtilityClass public class ItemMapper { @@ -15,7 +21,6 @@ public ItemDto toItemDto(Item item) { item.getName(), item.getDescription(), item.isAvailable() - // item.getRequest() != null ? item.getRequest().getId() : null ); } @@ -42,4 +47,23 @@ public Item updateItem(Item item, UpdateItemDto updateItemDto) { return item; } + + public static ItemWithBookingDto toItemWithBookingDatesDto( + Item item, Booking nextBooking, Booking lastBooking) { + Long itemId = item.getId(); + String itemName = item.getName(); + String itemDescription = item.getDescription(); + boolean itemAvailable = item.isAvailable(); + BookingInfoDto lastBookingInfoDto = BookingMapper.toBookingInfoDto(lastBooking).orElse(null); + BookingInfoDto nextBookingInfoDto = BookingMapper.toBookingInfoDto(nextBooking).orElse(null); + return new ItemWithBookingDto( + itemId, + itemName, + itemDescription, + itemAvailable, + lastBookingInfoDto, + nextBookingInfoDto + ); + } + } diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java index 86db07f..320120e 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.item; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemWithBookingDto; import ru.practicum.shareit.item.dto.NewItemDto; import ru.practicum.shareit.item.dto.UpdateItemDto; @@ -9,7 +10,7 @@ public interface ItemService { ItemDto getItemOfUserById(long userId, long itemId); - Collection getAllItemsOfUser(long userId); + Collection getAllItemsOfUser(long userId); ItemDto saveItem(long userId, NewItemDto newItem); diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 685fdfe..08260fe 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -4,9 +4,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; import ru.practicum.shareit.exception.ForbiddenAccessException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemWithBookingDto; import ru.practicum.shareit.item.dto.NewItemDto; import ru.practicum.shareit.item.dto.UpdateItemDto; import ru.practicum.shareit.item.model.Item; @@ -14,8 +17,13 @@ import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; +import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Service @@ -24,6 +32,7 @@ public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; + private final BookingRepository bookingRepository; @Override public ItemDto getItemOfUserById(long userId, long itemId) { @@ -35,12 +44,38 @@ public ItemDto getItemOfUserById(long userId, long itemId) { } @Override - public Collection getAllItemsOfUser(long userId) { + public Collection getAllItemsOfUser(long userId) { getUserOrThrow(userId); + List itemsByOwner = itemRepository.findAllByOwnerId(userId); + List itemsIds = itemsByOwner.stream() + .map(Item::getId) + .toList(); + + LocalDateTime currentTime = LocalDateTime.now(); + + List lastBookingsOfItems = bookingRepository.findLastApprovedBookingsForItems( + itemsIds, currentTime); + List nextBookingsOfItems = bookingRepository.findNextApprovedBookingsForItems( + itemsIds, currentTime); + + Map lastBookingsOfItemsById = lastBookingsOfItems.stream() + .collect(Collectors.toMap( + booking -> booking.getItem().getId(), + Function.identity() + )); + Map nextBookingsOfItemsById = nextBookingsOfItems.stream() + .collect(Collectors.toMap( + booking -> booking.getItem().getId(), + Function.identity() + )); return itemRepository.findAllByOwnerId(userId) .stream() - .map(ItemMapper::toItemDto) + .map(item -> ItemMapper.toItemWithBookingDatesDto( + item, + lastBookingsOfItemsById.get(item.getId()), + nextBookingsOfItemsById.get(item.getId()) + )) .toList(); } 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 674b6ac..004bf66 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -5,6 +5,5 @@ public record ItemDto( String name, String description, boolean available - // Long requestId ) { } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java new file mode 100644 index 0000000..ae5109e --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.booking.dto.BookingInfoDto; + +public record ItemWithBookingDto( + Long id, + String name, + String description, + boolean available, + BookingInfoDto lastBooking, + BookingInfoDto nextBooking +) { +} From 1a276b01620afd5903cbec1512e60b513a23e828 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 14:57:07 +0300 Subject: [PATCH 04/14] feat: add comments --- .../shareit/booking/BookingController.java | 2 + .../booking/repository/BookingRepository.java | 9 ++ .../exception/ItemCommentException.java | 7 ++ .../handler/GlobalExceptionHandler.java | 12 ++- .../shareit/item/ItemController.java | 18 ++-- .../practicum/shareit/item/ItemService.java | 9 +- .../shareit/item/ItemServiceImpl.java | 88 +++++++++++++------ .../shareit/item/dto/CommentDto.java | 11 +++ .../shareit/item/dto/ItemWithBookingDto.java | 5 +- .../shareit/item/dto/NewCommentDto.java | 6 ++ .../shareit/item/mapper/CommentMapper.java | 32 +++++++ .../shareit/item/{ => mapper}/ItemMapper.java | 12 ++- .../practicum/shareit/item/model/Comment.java | 48 ++++++++++ .../item/repository/CommentRepository.java | 11 +++ src/main/resources/schema.sql | 12 +++ 15 files changed, 238 insertions(+), 44 deletions(-) create mode 100644 src/main/java/ru/practicum/shareit/exception/ItemCommentException.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/CommentDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java rename src/main/java/ru/practicum/shareit/item/{ => mapper}/ItemMapper.java (84%) create mode 100644 src/main/java/ru/practicum/shareit/item/model/Comment.java create mode 100644 src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index 0536d8b..e4bc428 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.booking.dto.BookingDto; @@ -18,6 +19,7 @@ public class BookingController { private final BookingService bookingService; @PostMapping + @ResponseStatus(HttpStatus.CREATED) public BookingResponseDto createBooking( @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, @RequestBody @Valid BookingDto bookingDto diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 49f5929..8afe836 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -7,6 +7,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; public interface BookingRepository extends JpaRepository { @@ -47,6 +48,14 @@ List findNextApprovedBookingsForItems( ); //booker methods + + Optional findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( + Long bookerId, + Long itemId, + BookingStatus status, + LocalDateTime endTime + ); + // ALL List findByBookerIdOrderByStartTimeDesc(Long bookerId); diff --git a/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java b/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java new file mode 100644 index 0000000..7ce178a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ItemCommentException extends RuntimeException { + public ItemCommentException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index 1e1df2b..bbeb0b4 100644 --- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -7,10 +7,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import ru.practicum.shareit.exception.DuplicateDataException; -import ru.practicum.shareit.exception.ForbiddenAccessException; -import ru.practicum.shareit.exception.ItemUnavailableException; -import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.*; import ru.practicum.shareit.exception.dto.ErrorResponse; import ru.practicum.shareit.exception.dto.ValidationErrorResponse; import ru.practicum.shareit.exception.dto.Violation; @@ -86,4 +83,11 @@ public ErrorResponse onForbiddenAccessException(ForbiddenAccessException ex) { log.warn("Forbidden access exception occurred while processing request {}", ex.getMessage()); return new ErrorResponse("forbidden", ex.getMessage()); } + + @ExceptionHandler(ItemCommentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onItemCommentException(ItemCommentException ex) { + log.warn("Item comment exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("cant comment", ex.getMessage()); + } } diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index 2ef7eb3..7da600b 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -3,12 +3,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemWithBookingDto; -import ru.practicum.shareit.item.dto.NewItemDto; -import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.dto.*; import java.util.Collection; @@ -22,7 +20,7 @@ public class ItemController { private final ItemService itemService; @GetMapping("/{itemId}") - public ItemDto getItem( + public ItemWithBookingDto getItem( @RequestHeader(SHARER_USER_ID_HEADER) long userId, @PathVariable long itemId ) { @@ -59,4 +57,14 @@ public Collection searchItems( ) { return itemService.searchItems(query); } + + @PostMapping("/{itemId}/comment") + @ResponseStatus(HttpStatus.CREATED) + public CommentDto createComment( + @RequestHeader(SHARER_USER_ID_HEADER) long authorId, + @PathVariable long itemId, + @RequestBody @Valid NewCommentDto comment + ) { + return itemService.createComment(authorId, itemId, comment); + } } diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java index 320120e..e7ad410 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -1,14 +1,11 @@ package ru.practicum.shareit.item; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemWithBookingDto; -import ru.practicum.shareit.item.dto.NewItemDto; -import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.dto.*; import java.util.Collection; public interface ItemService { - ItemDto getItemOfUserById(long userId, long itemId); + ItemWithBookingDto getItemOfUserById(long userId, long itemId); Collection getAllItemsOfUser(long userId); @@ -17,4 +14,6 @@ public interface ItemService { ItemDto updateItem(long userId, long itemId, UpdateItemDto updatedItem); Collection searchItems(String query); + + CommentDto createComment(long bookerId, long itemId, NewCommentDto newComment); } diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 08260fe..5fe4e30 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -5,23 +5,23 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.booking.repository.BookingRepository; import ru.practicum.shareit.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.ItemCommentException; import ru.practicum.shareit.exception.NotFoundException; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemWithBookingDto; -import ru.practicum.shareit.item.dto.NewItemDto; -import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.item.mapper.CommentMapper; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; import java.time.LocalDateTime; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -33,14 +33,21 @@ public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; @Override - public ItemDto getItemOfUserById(long userId, long itemId) { + public ItemWithBookingDto getItemOfUserById(long userId, long itemId) { getUserOrThrow(userId); - return itemRepository.findById(itemId) - .map(ItemMapper::toItemDto) + Item item = itemRepository.findById(itemId) .orElseThrow(NotFoundException.supplier("item with id %d not found", itemId)); + List itemIdList = List.of(item.getId()); + Map> commentsOfItems = getCommentsOfItems(itemIdList); + return ItemMapper.toItemWithBookingDatesDto( + item, + null, + null, + commentsOfItems.get(itemId)); } @Override @@ -52,31 +59,45 @@ public Collection getAllItemsOfUser(long userId) { .toList(); LocalDateTime currentTime = LocalDateTime.now(); + Map lastBookingsOfItemsById = getLastBookingsMap(itemsIds, currentTime); + Map nextBookingsOfItemsById = getNextBookingMap(itemsIds, currentTime); + Map> commentsByItemId = getCommentsOfItems(itemsIds); - List lastBookingsOfItems = bookingRepository.findLastApprovedBookingsForItems( - itemsIds, currentTime); + return itemRepository.findAllByOwnerId(userId) + .stream() + .map(item -> ItemMapper.toItemWithBookingDatesDto( + item, + lastBookingsOfItemsById.get(item.getId()), + nextBookingsOfItemsById.get(item.getId()), + commentsByItemId.get(item.getId()) + )) + .toList(); + } + + private Map getNextBookingMap(List itemsIds, LocalDateTime currentTime) { List nextBookingsOfItems = bookingRepository.findNextApprovedBookingsForItems( itemsIds, currentTime); - - Map lastBookingsOfItemsById = lastBookingsOfItems.stream() + return nextBookingsOfItems.stream() .collect(Collectors.toMap( booking -> booking.getItem().getId(), Function.identity() )); - Map nextBookingsOfItemsById = nextBookingsOfItems.stream() + } + + private Map> getCommentsOfItems(List itemsIds) { + List itemsComments = commentRepository.findAllByItem_IdIn(itemsIds); + return itemsComments.stream() + .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); + } + + private Map getLastBookingsMap(List itemsIds, LocalDateTime currentTime) { + List lastBookingsOfItems = bookingRepository.findLastApprovedBookingsForItems( + itemsIds, currentTime); + return lastBookingsOfItems.stream() .collect(Collectors.toMap( booking -> booking.getItem().getId(), Function.identity() )); - - return itemRepository.findAllByOwnerId(userId) - .stream() - .map(item -> ItemMapper.toItemWithBookingDatesDto( - item, - lastBookingsOfItemsById.get(item.getId()), - nextBookingsOfItemsById.get(item.getId()) - )) - .toList(); } @Override @@ -114,6 +135,23 @@ public Collection searchItems(String query) { .toList(); } + @Override + @Transactional + public CommentDto createComment(long authorId, long itemId, NewCommentDto newComment) { + User author = getUserOrThrow(authorId); + Item item = getItemOrThrow(itemId); + Optional bookingOptional = bookingRepository.findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( + authorId, itemId, BookingStatus.APPROVED, LocalDateTime.now() + ); + + if (bookingOptional.isEmpty()) { + throw new ItemCommentException("You can't comment item that you haven't booked"); + } + Comment comment = CommentMapper.toEntity(newComment, author, item, LocalDateTime.now()); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.toDto(savedComment); + } + private Item getItemOrThrow(long itemId) { return itemRepository.findById(itemId).orElseThrow( NotFoundException.supplier("Item with id %d not found", itemId) 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..5c2a30a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +import java.time.LocalDateTime; + +public record CommentDto( + Long id, + String text, + String authorName, + LocalDateTime created +) { +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java index ae5109e..f742247 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java @@ -2,12 +2,15 @@ import ru.practicum.shareit.booking.dto.BookingInfoDto; +import java.util.List; + public record ItemWithBookingDto( Long id, String name, String description, boolean available, BookingInfoDto lastBooking, - BookingInfoDto nextBooking + BookingInfoDto nextBooking, + List comments ) { } diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java new file mode 100644 index 0000000..2eba2c9 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; + +public record NewCommentDto(@NotBlank String text) { +} 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..bd4cca0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.item.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.NewCommentDto; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@UtilityClass +public class CommentMapper { + public CommentDto toDto(Comment comment) { + return new CommentDto( + comment.getId(), + comment.getText(), + comment.getAuthor().getName(), + comment.getCreatedAt() + ); + } + + public Comment toEntity(NewCommentDto dto, User author, Item item, LocalDateTime now) { + return new Comment( + null, + dto.text(), + item, + author, + now + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java similarity index 84% rename from src/main/java/ru/practicum/shareit/item/ItemMapper.java rename to src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java index 641d79a..4020849 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.item; +package ru.practicum.shareit.item.mapper; import lombok.experimental.UtilityClass; import ru.practicum.shareit.booking.BookingMapper; @@ -8,9 +8,10 @@ import ru.practicum.shareit.item.dto.ItemWithBookingDto; import ru.practicum.shareit.item.dto.NewItemDto; import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; -import java.time.LocalDateTime; +import java.util.List; @UtilityClass public class ItemMapper { @@ -49,7 +50,7 @@ public Item updateItem(Item item, UpdateItemDto updateItemDto) { } public static ItemWithBookingDto toItemWithBookingDatesDto( - Item item, Booking nextBooking, Booking lastBooking) { + Item item, Booking nextBooking, Booking lastBooking, List comments) { Long itemId = item.getId(); String itemName = item.getName(); String itemDescription = item.getDescription(); @@ -62,7 +63,10 @@ public static ItemWithBookingDto toItemWithBookingDatesDto( itemDescription, itemAvailable, lastBookingInfoDto, - nextBookingInfoDto + nextBookingInfoDto, + comments == null ? null : comments.stream() + .map(CommentMapper::toDto) + .toList() ); } 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..c534170 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.*; +import lombok.experimental.FieldDefaults; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "comments") +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id", nullable = false) + Long id; + + @Size(max = 1024) + @NotNull + @Column(name = "text", nullable = false, length = 1024) + String text; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "item_id", nullable = false) + Item item; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "author_id", nullable = false) + User author; + + @NotNull + @Column(name = "created_at", nullable = false) + LocalDateTime createdAt; + +} diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java new file mode 100644 index 0000000..f2f8dcb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.Collection; +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findAllByItem_IdIn(Collection itemIds); +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 5076b73..67db65a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -35,3 +35,15 @@ CREATE TABLE IF NOT EXISTS bookings CONSTRAINT fk_item FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE, CONSTRAINT fk_booker FOREIGN KEY (booker_id) REFERENCES users(user_id) ON DELETE CASCADE ); + +CREATE TABLE IF NOT EXISTS comments +( + comment_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(1024) NOT NULL, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + CONSTRAINT fk_item FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE, + CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES users(user_id) ON DELETE CASCADE +); From 0a6609b375e591b73d24acca1592d1373c19cb49 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 15:45:31 +0300 Subject: [PATCH 05/14] fix:moved switch in BookingState --- src/main/java/ru/practicum/shareit/booking/BookingState.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/src/main/java/ru/practicum/shareit/booking/BookingState.java index 0d14b1e..b59f4ff 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingState.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -11,7 +11,7 @@ public enum BookingState { REJECTED; public static Optional fromString(String state) { - return Optional.ofNullable(switch (state.toUpperCase()) { + BookingState bookingState = switch (state.toUpperCase()) { case "ALL" -> ALL; case "CURRENT" -> CURRENT; case "PAST" -> PAST; @@ -19,6 +19,7 @@ public static Optional fromString(String state) { case "WAITING" -> WAITING; case "REJECTED" -> REJECTED; default -> null; - }); + }; + return Optional.ofNullable(bookingState); } } From c23d00eefbf11784ed7f14a6c9698d6165c78be3 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 15:55:24 +0300 Subject: [PATCH 06/14] feat: add dockerfile --- Dockerfile | 37 +++++++++++++++++++++++ docker-compose.yaml | 24 ++++++++++++++- src/main/resources/application.properties | 6 ++-- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0d7f86d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM maven:3.9.11-amazoncorretto-21 AS builder +WORKDIR /application + +COPY pom.xml ./ + +ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository" +RUN mvn dependency:go-offline -B +COPY src ./src + +RUN mvn clean package -DskipTests + +FROM amazoncorretto:21.0.8-alpine AS layers +WORKDIR /application +COPY --from=builder /application/target/*.jar app.jar +RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted + +FROM amazoncorretto:21.0.8-alpine +VOLUME /tmp +RUN adduser -S spring-user +USER spring-user + +WORKDIR /application + +COPY --from=layers /application/extracted/dependencies/ ./ +COPY --from=layers /application/extracted/spring-boot-loader/ ./ +COPY --from=layers /application/extracted/snapshot-dependencies/ ./ +COPY --from=layers /application/extracted/application/ ./ + +RUN java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app.jar & exit 0 + +ENV JAVA_CDS_OPTS="-XX:SharedArchiveFile=app.jsa -Xlog:class+load:file=/tmp/classload.log" +ENV JAVA_ERROR_FILE_OPTS="-XX:ErrorFile=/tmp/java_error.log" + +ENTRYPOINT java \ + $JAVA_ERROR_FILE_OPTS \ + $JAVA_CDS_OPTS \ + -jar app.jar diff --git a/docker-compose.yaml b/docker-compose.yaml index 2e4a2b4..62c0038 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,8 @@ services: db: image: postgres:17.7-alpine3.23 - container_name: shareit-postgres-db + container_name: postgres-db + restart: unless-stopped ports: - "5432:5432" environment: @@ -15,5 +16,26 @@ services: timeout: 5s interval: 5s retries: 10 + shareit: + image: shareit:latest + build: . + container_name: shareit-app + restart: unless-stopped + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/shareit + SPRING_DATASOURCE_USERNAME: shareit_user + SPRING_DATASOURCE_PASSWORD: secret + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 + interval: 30s + timeout: 5s + start_period: 30s + retries: 5 + depends_on: + db: + condition: service_healthy + volumes: postgres-data: diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2304185..013b62a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,6 +8,6 @@ logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5432/shareit -spring.datasource.username=shareit_user -spring.datasource.password=secret +spring.datasource.url=${SPRING_DATASOURCE_URL} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} From b915e9256a3ed665f0dd6b25e62badd54b6ca546 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 15:59:11 +0300 Subject: [PATCH 07/14] fix: test properties --- src/test/java/resources/application-test.properties | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/resources/application-test.properties b/src/test/java/resources/application-test.properties index 9e9bc4b..f112337 100644 --- a/src/test/java/resources/application-test.properties +++ b/src/test/java/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:shareit +spring.datasource.username=test +spring.datasource.password=test From 78d4869d9a3354ca786e498e7839f07f70de55c9 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 16:01:57 +0300 Subject: [PATCH 08/14] fix: init test script --- src/test/java/resources/application-test.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/resources/application-test.properties b/src/test/java/resources/application-test.properties index f112337..e69a03a 100644 --- a/src/test/java/resources/application-test.properties +++ b/src/test/java/resources/application-test.properties @@ -1,6 +1,5 @@ spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always logging.level.org.springframework.orm.jpa=INFO logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE @@ -10,3 +9,5 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.url=jdbc:h2:mem:shareit spring.datasource.username=test spring.datasource.password=test + +spring.sql.init.mode=never From 3cc80906c7ec18f8c4ca7eded46ad476770beee7 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Sun, 7 Dec 2025 16:34:16 +0300 Subject: [PATCH 09/14] fix: tests --- .../resources/application-test.properties | 12 ++++++------ src/main/resources/application.properties | 4 ++-- src/test/java/ru/practicum/shareit/ShareItTests.java | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) rename src/{test/java => main}/resources/application-test.properties (60%) diff --git a/src/test/java/resources/application-test.properties b/src/main/resources/application-test.properties similarity index 60% rename from src/test/java/resources/application-test.properties rename to src/main/resources/application-test.properties index e69a03a..09fc2b7 100644 --- a/src/test/java/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -1,13 +1,13 @@ -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.format_sql=true 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 +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.sql.init.mode=never -spring.datasource.driverClassName=org.h2.Driver +spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:shareit -spring.datasource.username=test -spring.datasource.password=test - -spring.sql.init.mode=never +spring.datasource.username=sa +spring.datasource.password=password diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 013b62a..a627601 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=validate spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always @@ -7,7 +7,7 @@ logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..06d43e9 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -2,12 +2,14 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; -@SpringBootTest +@SpringBootTest() +@TestPropertySource(locations = "classpath:application-test.properties") class ShareItTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } From 6423c45991459b8a7b9c079d24f7595e366c6744 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Mon, 8 Dec 2025 13:01:53 +0300 Subject: [PATCH 10/14] fix: remove validation from comment entity --- .../practicum/shareit/item/model/Comment.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java index c534170..b347833 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -1,8 +1,6 @@ package ru.practicum.shareit.item.model; import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.*; import lombok.experimental.FieldDefaults; import org.hibernate.annotations.OnDelete; @@ -21,28 +19,23 @@ public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "comment_id", nullable = false) + @Column(name = "comment_id") Long id; - @Size(max = 1024) - @NotNull - @Column(name = "text", nullable = false, length = 1024) + @Column(name = "text") String text; - @NotNull - @ManyToOne(fetch = FetchType.LAZY, optional = false) + @ManyToOne(fetch = FetchType.LAZY) @OnDelete(action = OnDeleteAction.CASCADE) - @JoinColumn(name = "item_id", nullable = false) + @JoinColumn(name = "item_id") Item item; - @NotNull - @ManyToOne(fetch = FetchType.LAZY, optional = false) + @ManyToOne(fetch = FetchType.LAZY) @OnDelete(action = OnDeleteAction.CASCADE) - @JoinColumn(name = "author_id", nullable = false) + @JoinColumn(name = "author_id") User author; - @NotNull - @Column(name = "created_at", nullable = false) + @Column(name = "created_at") LocalDateTime createdAt; } From 85e91f75d1457915e4b330d3c564c480cdae627d Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Mon, 8 Dec 2025 13:38:38 +0300 Subject: [PATCH 11/14] feat: add booking intersection check --- .../shareit/booking/BookingServiceImpl.java | 15 +++++++++++++++ .../booking/repository/BookingRepository.java | 1 + .../exception/BookingIntersectionException.java | 7 +++++++ .../exception/handler/GlobalExceptionHandler.java | 7 +++++++ 4 files changed, 30 insertions(+) create mode 100644 src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java index c8d04a1..9349fa0 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -8,6 +8,7 @@ import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.BookingStatus; import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exception.BookingIntersectionException; import ru.practicum.shareit.exception.ForbiddenAccessException; import ru.practicum.shareit.exception.ItemUnavailableException; import ru.practicum.shareit.exception.NotFoundException; @@ -38,6 +39,7 @@ public BookingResponseDto createBooking(long bookerId, BookingDto bookingDto) { } checkBookingDates(bookingDto.start(), bookingDto.end()); + checkBookingIntersections(bookingDto); Booking booking = BookingMapper.fromDto(bookingDto, booker, item); Booking savedBooking = bookingRepository.save(booking); @@ -130,6 +132,19 @@ private Booking getBookingOrThrow(long bookingId) { .orElseThrow(NotFoundException.supplier("Booking with id:%d not found", bookingId)); } + private void checkBookingIntersections(BookingDto bookingDto) { + List bookingsOfItem = bookingRepository.findAllByItem_Id(bookingDto.itemId()); + LocalDateTime start = bookingDto.start(); + LocalDateTime end = bookingDto.end(); + + for (Booking booking : bookingsOfItem) { + if (start.isBefore(booking.getEndTime()) && end.isAfter(booking.getStartTime())) { + throw new BookingIntersectionException(""" + You can't book this item because booking dates intersects with another booking"""); + } + } + } + private void checkBookingDates(LocalDateTime start, LocalDateTime end) { if (start.isAfter(end)) { throw new IllegalArgumentException("Start booking date is after end date"); diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 8afe836..07a2b20 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -123,4 +123,5 @@ List findByItemOwnerIdAndStatusOrderByStartTimeDesc( BookingStatus status ); + List findAllByItem_Id(Long itemId); } diff --git a/src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java b/src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java new file mode 100644 index 0000000..5381686 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class BookingIntersectionException extends RuntimeException { + public BookingIntersectionException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index bbeb0b4..bb825a9 100644 --- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -90,4 +90,11 @@ public ErrorResponse onItemCommentException(ItemCommentException ex) { log.warn("Item comment exception occurred while processing request {}", ex.getMessage()); return new ErrorResponse("cant comment", ex.getMessage()); } + + @ExceptionHandler(BookingIntersectionException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onBookingIntersectionException(BookingIntersectionException ex) { + log.warn("Booking intersection exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("booking intersection", ex.getMessage()); + } } From 8ee8376ce400334d7b27619de5f8f7b88f94a42e Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Mon, 8 Dec 2025 13:42:41 +0300 Subject: [PATCH 12/14] feat: add sorting in comments selection --- src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java | 2 +- .../practicum/shareit/item/repository/CommentRepository.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 5fe4e30..01ef6ab 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -85,7 +85,7 @@ private Map getNextBookingMap(List itemsIds, LocalDateTime } private Map> getCommentsOfItems(List itemsIds) { - List itemsComments = commentRepository.findAllByItem_IdIn(itemsIds); + List itemsComments = commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(itemsIds); return itemsComments.stream() .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); } diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java index f2f8dcb..9230136 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -7,5 +7,6 @@ import java.util.List; public interface CommentRepository extends JpaRepository { - List findAllByItem_IdIn(Collection itemIds); + + List findAllByItem_IdInOrderByCreatedAtDesc(Collection ids); } From 6eabd6fa8c0904ba8d8ef39d6bc45d59f8c5ddf0 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Mon, 8 Dec 2025 14:10:06 +0300 Subject: [PATCH 13/14] fix: last bookings and next bookings now calculated in app --- .../booking/repository/BookingRepository.java | 38 +-------- .../shareit/item/ItemServiceImpl.java | 80 ++++++++++++------- 2 files changed, 52 insertions(+), 66 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 07a2b20..4f69bbc 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -6,47 +6,13 @@ import ru.practicum.shareit.booking.model.BookingStatus; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import java.util.Optional; public interface BookingRepository extends JpaRepository { - @Query(""" - SELECT b FROM Booking b - WHERE b.item.id IN :itemIds - AND b.status = 'APPROVED' - AND b.endTime < :now - AND b.endTime = ( - SELECT MAX(b2.endTime) - FROM Booking b2 - WHERE b2.item.id = b.item.id - AND b2.status = 'APPROVED' - AND b2.endTime < :now - ) - """) - List findLastApprovedBookingsForItems( - List itemIds, - LocalDateTime now - ); - - @Query(""" - SELECT b FROM Booking b - WHERE b.item.id IN :itemIds - AND b.status = 'APPROVED' - AND b.startTime > :now - AND b.startTime = ( - SELECT MIN(b2.startTime) - FROM Booking b2 - WHERE b2.item.id = b.item.id - AND b2.status = 'APPROVED' - AND b2.startTime > :now - ) - """) - List findNextApprovedBookingsForItems( - List itemIds, - LocalDateTime now - ); - + List findAllByItem_IdInAndStatus(Collection ids, BookingStatus status); //booker methods Optional findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 01ef6ab..7ed4a50 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -22,7 +22,6 @@ import java.time.LocalDateTime; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; @Slf4j @@ -59,8 +58,10 @@ public Collection getAllItemsOfUser(long userId) { .toList(); LocalDateTime currentTime = LocalDateTime.now(); - Map lastBookingsOfItemsById = getLastBookingsMap(itemsIds, currentTime); - Map nextBookingsOfItemsById = getNextBookingMap(itemsIds, currentTime); + List bookingsOfItems = bookingRepository.findAllByItem_IdInAndStatus( + itemsIds, BookingStatus.APPROVED); + Map lastBookingsOfItemsById = getLastBookingsMap(bookingsOfItems, currentTime); + Map nextBookingsOfItemsById = getNextBookingsMap(bookingsOfItems, currentTime); Map> commentsByItemId = getCommentsOfItems(itemsIds); return itemRepository.findAllByOwnerId(userId) @@ -74,32 +75,6 @@ public Collection getAllItemsOfUser(long userId) { .toList(); } - private Map getNextBookingMap(List itemsIds, LocalDateTime currentTime) { - List nextBookingsOfItems = bookingRepository.findNextApprovedBookingsForItems( - itemsIds, currentTime); - return nextBookingsOfItems.stream() - .collect(Collectors.toMap( - booking -> booking.getItem().getId(), - Function.identity() - )); - } - - private Map> getCommentsOfItems(List itemsIds) { - List itemsComments = commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(itemsIds); - return itemsComments.stream() - .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); - } - - private Map getLastBookingsMap(List itemsIds, LocalDateTime currentTime) { - List lastBookingsOfItems = bookingRepository.findLastApprovedBookingsForItems( - itemsIds, currentTime); - return lastBookingsOfItems.stream() - .collect(Collectors.toMap( - booking -> booking.getItem().getId(), - Function.identity() - )); - } - @Override @Transactional public ItemDto saveItem(long userId, NewItemDto newItem) { @@ -122,7 +97,6 @@ public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) { return ItemMapper.toItemDto(updatedItem); } - @Override public Collection searchItems(String query) { if (query.isBlank()) { @@ -152,6 +126,52 @@ public CommentDto createComment(long authorId, long itemId, NewCommentDto newCom return CommentMapper.toDto(savedComment); } + private Map getNextBookingsMap(List bookings, LocalDateTime currentTime) { + Map bookingsByItemId = new HashMap<>(); + + for (Booking booking : bookings) { + if (!booking.getStartTime().isAfter(currentTime)) { + continue; + } + + Long itemId = booking.getItem().getId(); + Booking existing = bookingsByItemId.get(itemId); + + if (existing == null + || booking.getStartTime().isBefore(existing.getStartTime())) { + bookingsByItemId.put(itemId, booking); + } + } + + return bookingsByItemId; + } + + private Map> getCommentsOfItems(List itemsIds) { + List itemsComments = commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(itemsIds); + return itemsComments.stream() + .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); + } + + private Map getLastBookingsMap(List bookings, LocalDateTime currentTime) { + Map bookingsByItemId = new HashMap<>(); + + for (Booking booking : bookings) { + if (!booking.getEndTime().isAfter(currentTime)) { + continue; + } + + Long itemId = booking.getItem().getId(); + Booking existing = bookingsByItemId.get(itemId); + + if (existing == null + || booking.getEndTime().isAfter(existing.getEndTime())) { + bookingsByItemId.put(itemId, booking); + } + } + + return bookingsByItemId; + } + private Item getItemOrThrow(long itemId) { return itemRepository.findById(itemId).orElseThrow( NotFoundException.supplier("Item with id %d not found", itemId) From db22ef4122ff60935bcba979536da9b344d620bf Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Tue, 9 Dec 2025 20:07:59 +0300 Subject: [PATCH 14/14] fix: change finding intersections to sql code --- .../shareit/booking/BookingServiceImpl.java | 21 +++++++++++-------- .../booking/repository/BookingRepository.java | 15 ++++++++++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java index 9349fa0..edb08a9 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -133,15 +133,18 @@ private Booking getBookingOrThrow(long bookingId) { } private void checkBookingIntersections(BookingDto bookingDto) { - List bookingsOfItem = bookingRepository.findAllByItem_Id(bookingDto.itemId()); - LocalDateTime start = bookingDto.start(); - LocalDateTime end = bookingDto.end(); - - for (Booking booking : bookingsOfItem) { - if (start.isBefore(booking.getEndTime()) && end.isAfter(booking.getStartTime())) { - throw new BookingIntersectionException(""" - You can't book this item because booking dates intersects with another booking"""); - } + List intersections = + bookingRepository.findApprovedIntersectingBookings( + bookingDto.itemId(), + bookingDto.start(), + bookingDto.end() + ); + + if (!intersections.isEmpty()) { + throw new BookingIntersectionException( + "You can't book this item because booking dates intersect with another booking" + ); + } } diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java index 4f69bbc..082c9d3 100644 --- a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -13,6 +13,20 @@ public interface BookingRepository extends JpaRepository { List findAllByItem_IdInAndStatus(Collection ids, BookingStatus status); + + @Query(""" + SELECT b FROM Booking b + WHERE b.item.id = :itemId + AND b.status = 'APPROVED' + AND :start < b.endTime + AND :end > b.startTime + """) + List findApprovedIntersectingBookings( + Long itemId, + LocalDateTime start, + LocalDateTime end + ); + //booker methods Optional findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( @@ -89,5 +103,4 @@ List findByItemOwnerIdAndStatusOrderByStartTimeDesc( BookingStatus status ); - List findAllByItem_Id(Long itemId); }