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
new file mode 100644
index 0000000..62c0038
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,41 @@
+services:
+ db:
+ image: postgres:17.7-alpine3.23
+ container_name: postgres-db
+ restart: unless-stopped
+ 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
+ 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/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/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..e4bc428 100644
--- a/src/main/java/ru/practicum/shareit/booking/BookingController.java
+++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java
@@ -1,12 +1,66 @@
package ru.practicum.shareit.booking;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+import 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;
+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
+ @ResponseStatus(HttpStatus.CREATED)
+ 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..cf0d650
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java
@@ -0,0 +1,68 @@
+package ru.practicum.shareit.booking;
+
+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;
+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;
+import java.util.Optional;
+
+@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 Booking fromDto(BookingDto bookingDto, User booker, Item item) {
+ return new Booking(null,
+ bookingDto.start(),
+ bookingDto.end(),
+ 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/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..edb08a9
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java
@@ -0,0 +1,160 @@
+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.BookingIntersectionException;
+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());
+ checkBookingIntersections(bookingDto);
+
+ 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 checkBookingIntersections(BookingDto bookingDto) {
+ 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"
+ );
+
+ }
+ }
+
+ 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..b59f4ff
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java
@@ -0,0 +1,25 @@
+package ru.practicum.shareit.booking;
+
+import java.util.Optional;
+
+public enum BookingState {
+ ALL,
+ CURRENT,
+ PAST,
+ FUTURE,
+ WAITING,
+ REJECTED;
+
+ public static Optional fromString(String state) {
+ BookingState bookingState = switch (state.toUpperCase()) {
+ case "ALL" -> ALL;
+ case "CURRENT" -> CURRENT;
+ case "PAST" -> PAST;
+ case "FUTURE" -> FUTURE;
+ case "WAITING" -> WAITING;
+ case "REJECTED" -> REJECTED;
+ default -> null;
+ };
+ return Optional.ofNullable(bookingState);
+ }
+}
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/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/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..082c9d3
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java
@@ -0,0 +1,106 @@
+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.Collection;
+import java.util.List;
+import java.util.Optional;
+
+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(
+ Long bookerId,
+ Long itemId,
+ BookingStatus status,
+ LocalDateTime endTime
+ );
+
+ // 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/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/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/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..bb825a9 100644
--- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
+++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
@@ -7,9 +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.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;
@@ -65,6 +63,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) {
@@ -78,4 +83,18 @@ 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());
+ }
+
+ @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());
+ }
}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java
index 36ff3be..7da600b 100644
--- a/src/main/java/ru/practicum/shareit/item/ItemController.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemController.java
@@ -3,11 +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.NewItemDto;
-import ru.practicum.shareit.item.dto.UpdateItemDto;
+import ru.practicum.shareit.item.dto.*;
import java.util.Collection;
@@ -21,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
) {
@@ -29,7 +28,7 @@ public ItemDto getItem(
}
@GetMapping
- public Collection getItems(
+ public Collection getItems(
@RequestHeader(SHARER_USER_ID_HEADER) long userId
) {
return itemService.getAllItemsOfUser(userId);
@@ -58,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/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java
deleted file mode 100644
index 93d3883..0000000
--- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package ru.practicum.shareit.item;
-
-import lombok.experimental.UtilityClass;
-import ru.practicum.shareit.item.dto.ItemDto;
-import ru.practicum.shareit.item.dto.NewItemDto;
-import ru.practicum.shareit.item.dto.UpdateItemDto;
-import ru.practicum.shareit.item.model.Item;
-
-@UtilityClass
-public class ItemMapper {
-
- public ItemDto toItemDto(Item item) {
- return new ItemDto(
- item.getId(),
- item.getName(),
- item.getDescription(),
- item.isAvailable(),
- item.getRequest() != null ? item.getRequest().getId() : null
- );
- }
-
- public Item toItem(NewItemDto newItemDto) {
- Item item = new Item();
- item.setName(newItemDto.name());
- item.setDescription(newItemDto.description());
- item.setAvailable(newItemDto.available());
- return item;
- }
-
- public Item updateItem(Item item, UpdateItemDto updateItemDto) {
- if (updateItemDto.hasName()) {
- item.setName(updateItemDto.name());
- }
-
- if (updateItemDto.hasDescription()) {
- item.setDescription(updateItemDto.description());
- }
-
- if (updateItemDto.hasAvailable()) {
- item.setAvailable(updateItemDto.available());
- }
-
- return item;
- }
-}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java
index 86db07f..e7ad410 100644
--- a/src/main/java/ru/practicum/shareit/item/ItemService.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemService.java
@@ -1,19 +1,19 @@
package ru.practicum.shareit.item;
-import ru.practicum.shareit.item.dto.ItemDto;
-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);
+ Collection getAllItemsOfUser(long userId);
ItemDto saveItem(long userId, NewItemDto newItem);
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 13609f2..7ed4a50 100644
--- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
@@ -3,46 +3,80 @@
import lombok.RequiredArgsConstructor;
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.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.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.util.Collection;
-import java.util.Collections;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
@Slf4j
@Service
+@Transactional(readOnly = true)
@RequiredArgsConstructor
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
- 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();
- return itemRepository.findAllByUserId(userId)
+ LocalDateTime currentTime = LocalDateTime.now();
+ 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)
.stream()
- .map(ItemMapper::toItemDto)
+ .map(item -> ItemMapper.toItemWithBookingDatesDto(
+ item,
+ lastBookingsOfItemsById.get(item.getId()),
+ nextBookingsOfItemsById.get(item.getId()),
+ commentsByItemId.get(item.getId())
+ ))
.toList();
}
@Override
+ @Transactional
public ItemDto saveItem(long userId, NewItemDto newItem) {
User owner = getUserOrThrow(userId);
Item item = ItemMapper.toItem(newItem);
@@ -52,8 +86,8 @@ public ItemDto saveItem(long userId, NewItemDto newItem) {
}
@Override
+ @Transactional
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");
@@ -63,19 +97,81 @@ public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) {
return ItemMapper.toItemDto(updatedItem);
}
-
@Override
public Collection searchItems(String query) {
if (query.isBlank()) {
return Collections.emptyList();
}
- return itemRepository.searchItems(query)
+ return itemRepository.search(query)
.stream()
.map(ItemMapper::toItemDto)
.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 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)
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/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
index 30863dd..004bf66 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,6 @@ public record ItemDto(
Long id,
String name,
String description,
- boolean available,
- Long requestId
+ boolean available
) {
}
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..f742247
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java
@@ -0,0 +1,16 @@
+package ru.practicum.shareit.item.dto;
+
+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,
+ 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/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java
new file mode 100644
index 0000000..4020849
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java
@@ -0,0 +1,73 @@
+package ru.practicum.shareit.item.mapper;
+
+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.Comment;
+import ru.practicum.shareit.item.model.Item;
+
+import java.util.List;
+
+@UtilityClass
+public class ItemMapper {
+
+ public ItemDto toItemDto(Item item) {
+ return new ItemDto(
+ item.getId(),
+ item.getName(),
+ item.getDescription(),
+ item.isAvailable()
+ );
+ }
+
+ public Item toItem(NewItemDto newItemDto) {
+ Item item = new Item();
+ item.setName(newItemDto.name());
+ item.setDescription(newItemDto.description());
+ item.setAvailable(newItemDto.available());
+ return item;
+ }
+
+ public Item updateItem(Item item, UpdateItemDto updateItemDto) {
+ if (updateItemDto.hasName()) {
+ item.setName(updateItemDto.name());
+ }
+
+ if (updateItemDto.hasDescription()) {
+ item.setDescription(updateItemDto.description());
+ }
+
+ if (updateItemDto.hasAvailable()) {
+ item.setAvailable(updateItemDto.available());
+ }
+
+ return item;
+ }
+
+ public static ItemWithBookingDto toItemWithBookingDatesDto(
+ Item item, Booking nextBooking, Booking lastBooking, List comments) {
+ 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,
+ 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..b347833
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java
@@ -0,0 +1,41 @@
+package ru.practicum.shareit.item.model;
+
+import jakarta.persistence.*;
+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")
+ Long id;
+
+ @Column(name = "text")
+ String text;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @JoinColumn(name = "item_id")
+ Item item;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @JoinColumn(name = "author_id")
+ User author;
+
+ @Column(name = "created_at")
+ LocalDateTime createdAt;
+
+}
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/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java
new file mode 100644
index 0000000..9230136
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java
@@ -0,0 +1,12 @@
+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_IdInOrderByCreatedAtDesc(Collection ids);
+}
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..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,17 +42,19 @@ public UserDto save(NewUserDto newUser) {
}
@Override
+ @Transactional
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
+ @Transactional
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-test.properties b/src/main/resources/application-test.properties
index 9e9bc4b..09fc2b7 100644
--- a/src/main/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
-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
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
+spring.sql.init.mode=never
-# TODO Append connection to H2 DB
-#spring.datasource.driverClassName
-#spring.datasource.url
-#spring.datasource.username
-#spring.datasource.password
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.url=jdbc:h2:mem:shareit
+spring.datasource.username=sa
+spring.datasource.password=password
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 51c5180..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,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.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/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 0000000..67db65a
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,49 @@
+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
+);
+
+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
+);
+
+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
+);
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() {
+ }
}