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