diff --git a/pom.xml b/pom.xml
index 2db888c..64504e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,17 @@
21
-
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.postgresql
+ postgresql
+ 42.7.5
+
+
org.springframework.boot
spring-boot-starter-web
@@ -33,13 +43,6 @@
spring-boot-configuration-processor
true
-
-
- org.postgresql
- postgresql
- runtime
-
-
org.projectlombok
lombok
diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java
index bbb99f8..8326963 100644
--- a/src/main/java/ru/practicum/shareit/booking/Booking.java
+++ b/src/main/java/ru/practicum/shareit/booking/Booking.java
@@ -1,22 +1,60 @@
package ru.practicum.shareit.booking;
-import lombok.Data;
+import jakarta.persistence.*;
+import lombok.*;
+import ru.practicum.shareit.item.Item;
+import ru.practicum.shareit.user.User;
+
import java.time.LocalDateTime;
-@Data
+@Getter
+@Setter
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "bookings")
+@Builder
public class Booking {
- public enum Status {
+ public enum BookingStatus {
WAITING,
APPROVED,
REJECTED,
CANCELED
}
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
+
+ @Column(name = "start_date", nullable = false)
private LocalDateTime start;
+
+ @Column(name = "end_date", nullable = false)
private LocalDateTime end;
- private Long itemId;
- private Long bookerId;
- private Status status;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "item_id", nullable = false)
+ private Item item;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "booker_id", nullable = false)
+ private User booker;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private BookingStatus status;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Booking booking = (Booking) o;
+ return id != null && id.equals(booking.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 13;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java
index b94493d..4264461 100644
--- a/src/main/java/ru/practicum/shareit/booking/BookingController.java
+++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java
@@ -1,12 +1,50 @@
package ru.practicum.shareit.booking;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
-/**
- * TODO Sprint add-bookings.
- */
@RestController
-@RequestMapping(path = "/bookings")
+@RequiredArgsConstructor
+@RequestMapping("/bookings")
+@Slf4j
public class BookingController {
-}
+ private final BookingService bookingService;
+
+ @PostMapping
+ public BookingDto createBooking(@RequestHeader("X-Sharer-User-Id") Long userId,
+ @RequestBody BookingRequestDto request) {
+ return BookingMapper.toDto(bookingService.createBooking(userId, request));
+ }
+
+ @PatchMapping("/{bookingId}")
+ public BookingDto approveBooking(@RequestHeader("X-Sharer-User-Id") Long ownerId,
+ @PathVariable Long bookingId,
+ @RequestParam boolean approved) {
+ return BookingMapper.toDto(bookingService.approveBooking(ownerId, bookingId, approved));
+ }
+
+ @GetMapping("/{bookingId}")
+ public BookingDto getBookingById(@RequestHeader("X-Sharer-User-Id") Long userId,
+ @PathVariable Long bookingId) {
+ return BookingMapper.toDto(bookingService.getBookingById(userId, bookingId));
+ }
+
+ @GetMapping
+ public List getBookingsByUser(@RequestHeader("X-Sharer-User-Id") Long userId,
+ @RequestParam(defaultValue = "ALL") String state) {
+ return bookingService.getBookingsByUser(userId, BookingState.from(state)).stream()
+ .map(BookingMapper::toDto)
+ .toList();
+ }
+
+ @GetMapping("/owner")
+ public List getBookingsByOwner(@RequestHeader("X-Sharer-User-Id") Long userId,
+ @RequestParam(defaultValue = "ALL") String state) {
+ return bookingService.getBookingsByOwner(userId, BookingState.from(state)).stream()
+ .map(BookingMapper::toDto)
+ .toList();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/BookingDto.java
new file mode 100644
index 0000000..c0e95e4
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingDto.java
@@ -0,0 +1,23 @@
+package ru.practicum.shareit.booking;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import ru.practicum.shareit.item.ItemDto;
+import ru.practicum.shareit.user.UserDto;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BookingDto {
+ private Long id;
+ private ItemDto item;
+ private UserDto booker;
+ private LocalDateTime start;
+ private LocalDateTime end;
+ private Booking.BookingStatus status;
+}
\ No newline at end of file
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..2e7e531
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java
@@ -0,0 +1,31 @@
+package ru.practicum.shareit.booking;
+
+import ru.practicum.shareit.item.Item;
+import ru.practicum.shareit.item.ItemMapper;
+import ru.practicum.shareit.user.User;
+import ru.practicum.shareit.user.UserMapper;
+
+public class BookingMapper {
+ public static Booking toBooking(BookingRequestDto dto, Item item, User booker) {
+ return Booking.builder()
+ .start(dto.getStart())
+ .end(dto.getEnd())
+ .item(item)
+ .booker(booker)
+ .status(Booking.BookingStatus.WAITING)
+ .build();
+ }
+
+ public static BookingDto toDto(Booking booking) {
+ if (booking == null) return null;
+
+ return BookingDto.builder()
+ .id(booking.getId())
+ .start(booking.getStart())
+ .end(booking.getEnd())
+ .status(booking.getStatus())
+ .booker(UserMapper.toUserDto(booking.getBooker()))
+ .item(ItemMapper.toItemDto(booking.getItem()))
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java
new file mode 100644
index 0000000..ca6f4d3
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java
@@ -0,0 +1,57 @@
+package ru.practicum.shareit.booking;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface BookingRepository extends JpaRepository {
+ @Query("""
+ SELECT b FROM Booking b
+ WHERE b.item.id = :itemId
+ AND b.status IN ('APPROVED', 'WAITING')
+ AND b.end > :start AND b.start < :end
+ """)
+ List findOverlappingBookings(@Param("itemId") Long itemId,
+ @Param("start") LocalDateTime start,
+ @Param("end") LocalDateTime end);
+
+ @Query("""
+ SELECT b FROM Booking b
+ WHERE b.item.id = :itemId
+ AND b.start < :now
+ AND b.status = 'APPROVED'
+ ORDER BY b.start DESC LIMIT 1
+ """)
+ Booking findLastBooking(@Param("itemId") Long itemId, @Param("now") LocalDateTime now);
+
+ @Query("""
+ SELECT b FROM Booking b
+ WHERE b.item.id = :itemId
+ AND b.start > :now
+ AND b.status = 'APPROVED'
+ ORDER BY b.start ASC LIMIT 1
+ """)
+ Booking findNextBooking(@Param("itemId") Long itemId, @Param("now") LocalDateTime now);
+
+ List findByBookerIdOrderByStartDesc(Long bookerId);
+
+ List findByItemOwnerIdOrderByStartDesc(Long ownerId);
+
+ List findByBookerIdAndStatusOrderByStartDesc(Long bookerId, Booking.BookingStatus status);
+
+ List findByBookerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime now);
+
+ List findByBookerIdAndEndBeforeOrderByStartDesc(Long bookerId, LocalDateTime now);
+
+ List findByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(
+ Long bookerId, LocalDateTime now1, LocalDateTime now2);
+
+ List findByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(
+ Long ownerId, LocalDateTime now1, LocalDateTime now2);
+
+ boolean existsByItemIdAndBookerIdAndEndBeforeAndStatus(
+ Long itemId, Long userId, LocalDateTime now, Booking.BookingStatus status);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java b/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java
new file mode 100644
index 0000000..2cbb412
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java
@@ -0,0 +1,12 @@
+package ru.practicum.shareit.booking;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class BookingRequestDto {
+ private Long itemId;
+ private LocalDateTime start;
+ private LocalDateTime end;
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java
new file mode 100644
index 0000000..35ca661
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java
@@ -0,0 +1,15 @@
+package ru.practicum.shareit.booking;
+
+import java.util.List;
+
+public interface BookingService {
+ Booking createBooking(Long userId, BookingRequestDto dto);
+
+ Booking approveBooking(Long ownerId, Long bookingId, boolean approved);
+
+ Booking getBookingById(Long userId, Long bookingId);
+
+ List getBookingsByUser(Long userId, BookingState state);
+
+ List getBookingsByOwner(Long ownerId, BookingState state);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java
new file mode 100644
index 0000000..52fc38b
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java
@@ -0,0 +1,115 @@
+package ru.practicum.shareit.booking;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.shareit.exception.ConflictException;
+import ru.practicum.shareit.exception.ForbiddenException;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.exception.ValidationException;
+import ru.practicum.shareit.item.Item;
+import ru.practicum.shareit.item.ItemService;
+import ru.practicum.shareit.user.User;
+import ru.practicum.shareit.user.UserService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+
+@Slf4j
+@RequiredArgsConstructor
+@Transactional
+@Service
+public class BookingServiceImpl implements BookingService {
+ private final BookingRepository bookingRepository;
+ private final UserService userService;
+ private final ItemService itemService;
+
+ @Override
+ public Booking createBooking(Long userId, BookingRequestDto dto) {
+ User booker = userService.getUserById(userId);
+ Item item = itemService.getItemById(dto.getItemId(), userId);
+
+ if (!item.getAvailable()) {
+ throw new ValidationException("Вещь недоступна для бронирования.");
+ }
+
+ List conflicts = bookingRepository.findOverlappingBookings(item.getId(), dto.getStart(), dto.getEnd());
+ if (!conflicts.isEmpty()) {
+ throw new ValidationException("Вещь уже забронирована на указанный период.");
+ }
+
+ Booking booking = BookingMapper.toBooking(dto, item, booker);
+ booking.setStatus(Booking.BookingStatus.WAITING);
+
+ log.info("Создано бронирование на вещь с ID {}", item.getId());
+ return bookingRepository.save(booking);
+ }
+
+ @Override
+ public Booking approveBooking(Long ownerId, Long bookingId, boolean approved) {
+ Booking booking = getBookingOrThrow(bookingId);
+
+ if (!Objects.equals(booking.getItem().getOwner().getId(), ownerId)) {
+ throw new ForbiddenException("Только владелец может подтверждать бронирование");
+ }
+ if (booking.getStatus() != Booking.BookingStatus.WAITING) {
+ throw new ConflictException("Бронирование уже подтверждено или отклонено");
+ }
+
+ booking.setStatus(approved ? Booking.BookingStatus.APPROVED : Booking.BookingStatus.REJECTED);
+ return bookingRepository.save(booking);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Booking getBookingById(Long userId, Long bookingId) {
+ Booking booking = getBookingOrThrow(bookingId);
+ Long bookerId = booking.getBooker().getId();
+ Long ownerId = booking.getItem().getOwner().getId();
+
+ if (!Objects.equals(userId, bookerId) && !Objects.equals(userId, ownerId)) {
+ throw new NotFoundException("Нет доступа к бронированию");
+ }
+
+ return booking;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getBookingsByUser(Long userId, BookingState state) {
+ userService.getUserById(userId);
+ return findByState(userId, state, true);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getBookingsByOwner(Long ownerId, BookingState state) {
+ userService.getUserById(ownerId);
+ return findByState(ownerId, state, false);
+ }
+
+ private List findByState(Long id, BookingState state, boolean byBooker) {
+ LocalDateTime now = LocalDateTime.now();
+ return switch (state) {
+ case ALL -> byBooker
+ ? bookingRepository.findByBookerIdOrderByStartDesc(id)
+ : bookingRepository.findByItemOwnerIdOrderByStartDesc(id);
+ case CURRENT -> byBooker
+ ? bookingRepository.findByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(id, now, now)
+ : bookingRepository.findByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(id, now, now);
+ case FUTURE -> bookingRepository.findByBookerIdAndStartAfterOrderByStartDesc(id, now);
+ case PAST -> bookingRepository.findByBookerIdAndEndBeforeOrderByStartDesc(id, now);
+ case WAITING -> bookingRepository.findByBookerIdAndStatusOrderByStartDesc(id,
+ Booking.BookingStatus.WAITING);
+ case REJECTED -> bookingRepository.findByBookerIdAndStatusOrderByStartDesc(id,
+ Booking.BookingStatus.REJECTED);
+ };
+ }
+
+ private Booking getBookingOrThrow(Long bookingId) {
+ return bookingRepository.findById(bookingId)
+ .orElseThrow(() -> new NotFoundException("Бронирование не найдено"));
+ }
+}
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..705f825
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.booking;
+
+
+public enum BookingState {
+ ALL,
+ CURRENT,
+ PAST,
+ FUTURE,
+ WAITING,
+ REJECTED;
+
+ public static BookingState from(String value) {
+ try {
+ return BookingState.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new IllegalArgumentException("Unknown state: " + value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java
deleted file mode 100644
index 861de9e..0000000
--- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.practicum.shareit.booking.dto;
-
-/**
- * TODO Sprint add-bookings.
- */
-public class BookingDto {
-}
diff --git a/src/main/java/ru/practicum/shareit/exception/ForbiddenException.java b/src/main/java/ru/practicum/shareit/exception/ForbiddenException.java
new file mode 100644
index 0000000..db895b3
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/ForbiddenException.java
@@ -0,0 +1,8 @@
+package ru.practicum.shareit.exception;
+
+
+public class ForbiddenException extends RuntimeException {
+ public ForbiddenException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
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 dec3dd0..4dc6c4a 100644
--- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
+++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
@@ -7,6 +7,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.practicum.shareit.exception.ConflictException;
+import ru.practicum.shareit.exception.ForbiddenException;
import ru.practicum.shareit.exception.NotFoundException;
import ru.practicum.shareit.exception.ValidationException;
import ru.practicum.shareit.response.ErrorResponse;
@@ -54,6 +55,19 @@ public ResponseEntity handleConflict(
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
}
+ @ExceptionHandler({ForbiddenException.class})
+ public ResponseEntity handleConflict(
+ ForbiddenException e, HttpServletRequest request) {
+ ErrorResponse errorResponse = new ErrorResponse(
+ LocalDateTime.now(),
+ HttpStatus.FORBIDDEN.value(),
+ "Forbidden",
+ e.getMessage(),
+ request.getRequestURI()
+ );
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
+ }
+
@ExceptionHandler(Exception.class)
public ResponseEntity handleOtherExceptions(Exception e, HttpServletRequest request) {
ErrorResponse errorResponse = new ErrorResponse(
diff --git a/src/main/java/ru/practicum/shareit/item/Item.java b/src/main/java/ru/practicum/shareit/item/Item.java
new file mode 100644
index 0000000..85c8b3d
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/Item.java
@@ -0,0 +1,61 @@
+package ru.practicum.shareit.item;
+
+import jakarta.persistence.*;
+import lombok.*;
+import ru.practicum.shareit.booking.Booking;
+import ru.practicum.shareit.item.comment.Comment;
+import ru.practicum.shareit.user.User;
+
+import java.util.List;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "items")
+public class Item {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 255)
+ private String name;
+
+ @Column(nullable = false)
+ private String description;
+
+ @Column(name = "is_available", nullable = false)
+ private Boolean available;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "owner_id", nullable = false)
+ private User owner;
+
+ @Column(name = "request_id")
+ private Long requestId;
+
+ @Transient
+ private Booking lastBooking;
+
+ @Transient
+ private Booking nextBooking;
+
+ @Transient
+ private List comments;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Item item = (Item) o;
+ return id != null && id.equals(item.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 15;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java
index 4c9b176..d3dd252 100644
--- a/src/main/java/ru/practicum/shareit/item/ItemController.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemController.java
@@ -3,9 +3,8 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
-import ru.practicum.shareit.item.dto.ItemDto;
-import ru.practicum.shareit.item.mapper.ItemMapper;
-import ru.practicum.shareit.item.service.ItemService;
+import ru.practicum.shareit.item.comment.CommentDto;
+import ru.practicum.shareit.item.comment.CommentMapper;
import java.util.List;
@@ -32,8 +31,14 @@ public ItemDto update(@RequestHeader(USER_HEADER) Long userId,
}
@GetMapping("/{itemId}")
- public ItemDto getById(@PathVariable Long itemId) {
- return ItemMapper.toItemDto(itemService.getItemById(itemId));
+ public ItemDto getById(@PathVariable Long itemId,
+ @RequestHeader(value = "X-Sharer-User-Id", required = false) Long userId) {
+ Item item = itemService.getItemById(itemId, userId);
+ if (userId == null) {
+ return ItemMapper.toItemDto(item);
+ }
+
+ return ItemMapper.toItemDto(item, userId);
}
@GetMapping
@@ -49,4 +54,11 @@ public List search(@RequestParam String text) {
.map(ItemMapper::toItemDto)
.toList();
}
+
+ @PostMapping("/{itemId}/comment")
+ public CommentDto addComment(@RequestHeader("X-Sharer-User-Id") Long userId,
+ @PathVariable Long itemId,
+ @RequestBody CommentDto newComment) {
+ return CommentMapper.toDto(itemService.addComment(userId, itemId, newComment));
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/ItemDto.java
similarity index 57%
rename from src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
rename to src/main/java/ru/practicum/shareit/item/ItemDto.java
index 31c26a4..90fcb2c 100644
--- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemDto.java
@@ -1,9 +1,13 @@
-package ru.practicum.shareit.item.dto;
+package ru.practicum.shareit.item;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
+import ru.practicum.shareit.booking.BookingDto;
+import ru.practicum.shareit.item.comment.CommentDto;
+
+import java.util.List;
@Data
@Builder
@@ -18,4 +22,7 @@ public class ItemDto {
@NotNull
private Boolean available;
private Long requestId;
+ private BookingDto lastBooking;
+ private BookingDto nextBooking;
+ private List comments;
}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java
new file mode 100644
index 0000000..83a07de
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java
@@ -0,0 +1,68 @@
+package ru.practicum.shareit.item;
+
+import ru.practicum.shareit.booking.BookingMapper;
+import ru.practicum.shareit.item.comment.CommentMapper;
+import ru.practicum.shareit.user.User;
+
+import java.util.List;
+
+public class ItemMapper {
+
+ public static ItemDto toItemDto(Item item) {
+
+ return ItemDto.builder()
+ .id(item.getId())
+ .name(item.getName())
+ .description(item.getDescription())
+ .available(item.getAvailable())
+ .requestId(item.getRequestId())
+ .build();
+ }
+
+ public static ItemDto toItemDto(Item item, Long userId) {
+ ItemDto.ItemDtoBuilder builder = ItemDto.builder()
+ .id(item.getId())
+ .name(item.getName())
+ .description(item.getDescription())
+ .available(item.getAvailable())
+ .requestId(item.getRequestId());
+
+ if (item.getComments() != null) {
+ builder.comments((item.getComments().stream()
+ .map(CommentMapper::toDto)
+ .toList()));
+ } else {
+ builder.comments(List.of());
+ }
+
+ if (item.getOwner() != null && item.getOwner().getId().equals(userId)) {
+ builder.lastBooking(item.getLastBooking() != null ? BookingMapper.toDto(item.getLastBooking()) : null);
+ builder.nextBooking(item.getNextBooking() != null ? BookingMapper.toDto(item.getNextBooking()) : null);
+ }
+
+ return builder.build();
+ }
+
+ public static Item toItem(ItemDto itemDto, User owner, Long requestId) {
+ return Item.builder()
+ .name(itemDto.getName())
+ .description(itemDto.getDescription())
+ .available(itemDto.getAvailable())
+ .owner(owner)
+ .requestId(requestId)
+ .build();
+ }
+
+ public static Item updateItemFields(Item item, ItemDto itemDto) {
+ if (itemDto.getName() != null) {
+ item.setName(itemDto.getName());
+ }
+ if (itemDto.getDescription() != null) {
+ item.setDescription(itemDto.getDescription());
+ }
+ if (itemDto.getAvailable() != null) {
+ item.setAvailable(itemDto.getAvailable());
+ }
+ return item;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/ItemRepository.java
new file mode 100644
index 0000000..bbaa3ed
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemRepository.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.item;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface ItemRepository extends JpaRepository- {
+
+ List
- findAllByOwnerId(Long userId);
+
+ @Query("""
+ SELECT i FROM Item i
+ WHERE i.available = true
+ AND (LOWER(i.name) LIKE %:text% OR LOWER(i.description) LIKE %:text%)
+ """)
+ List
- search(@Param("text") String text);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java
new file mode 100644
index 0000000..897708a
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemService.java
@@ -0,0 +1,20 @@
+package ru.practicum.shareit.item;
+
+import ru.practicum.shareit.item.comment.Comment;
+import ru.practicum.shareit.item.comment.CommentDto;
+
+import java.util.List;
+
+public interface ItemService {
+ Item create(Long userId, ItemDto itemDto);
+
+ Item update(Long userId, Long itemId, ItemDto itemDto);
+
+ Item getItemById(Long itemId, Long requesterId);
+
+ List
- getAllByOwner(Long userId);
+
+ List
- search(String text);
+
+ Comment addComment(Long userId, Long itemId, CommentDto dto);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
new file mode 100644
index 0000000..ef178b0
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
@@ -0,0 +1,112 @@
+package ru.practicum.shareit.item;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.shareit.booking.Booking;
+import ru.practicum.shareit.booking.BookingRepository;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.exception.ValidationException;
+import ru.practicum.shareit.item.comment.Comment;
+import ru.practicum.shareit.item.comment.CommentDto;
+import ru.practicum.shareit.item.comment.CommentRepository;
+import ru.practicum.shareit.user.User;
+import ru.practicum.shareit.user.UserService;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+@Service
+@Transactional
+@Slf4j
+@RequiredArgsConstructor
+public class ItemServiceImpl implements ItemService {
+ private final UserService userService;
+ private final ItemRepository itemRepository;
+ private final BookingRepository bookingRepository;
+ private final CommentRepository commentRepository;
+
+ @Override
+ public Item create(Long userId, ItemDto itemDto) {
+ User owner = userService.getUserById(userId);
+ Item item = ItemMapper.toItem(itemDto, owner, null); // пока request == null
+ Item savedItem = itemRepository.save(item);
+
+ log.info("Вещь создана: {}", item);
+ return savedItem;
+ }
+
+ @Override
+ public Item update(Long userId, Long itemId, ItemDto itemDto) {
+ Item item = itemRepository.findById(itemId)
+ .orElseThrow(() -> new NotFoundException("Вещь с id " + itemId + " не найдена."));
+ if (!Objects.equals(item.getOwner().getId(), userId)) {
+ throw new NotFoundException("Редактировать может только владелец.");
+ }
+
+ Item updatedItem = ItemMapper.updateItemFields(item, itemDto);
+ log.info("Вещь обновлена: {}", item);
+ return itemRepository.save(updatedItem);
+ }
+
+
+ @Override
+ @Transactional(readOnly = true)
+ public Item getItemById(Long itemId, Long requesterId) {
+ Item item = itemRepository.findById(itemId)
+ .orElseThrow(() -> new NotFoundException("Вещь с id " + itemId + " не найдена."));
+
+ if (Objects.equals(item.getOwner().getId(), requesterId)) {
+ item.setLastBooking(bookingRepository.findLastBooking(itemId, LocalDateTime.now()));
+ item.setNextBooking(bookingRepository.findNextBooking(itemId, LocalDateTime.now()));
+ }
+
+ List comments = commentRepository.findByItemIdOrderByCreatedDesc(itemId);
+ item.setComments(comments);
+
+ return item;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public List
- getAllByOwner(Long userId) {
+ userService.getUserById(userId);
+ List
- itemsByOwner = itemRepository.findAllByOwnerId(userId);
+ log.info("Получен список всех вещей, сдаваемых пользователем с ID {}", userId);
+ return itemsByOwner;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public List
- search(String text) {
+ if (text == null || text.isBlank()) {
+ log.info("Пустой запрос для поиска");
+ return List.of();
+ }
+ return itemRepository.search(text.toLowerCase());
+ }
+
+ @Override
+ public Comment addComment(Long userId, Long itemId, CommentDto dto) {
+ Item item = getItemById(itemId, userId);
+ User author = userService.getUserById(userId);
+
+ // Проверяем есть ли хотя бы одно завершённое бронирование этой вещи этим пользователем
+ boolean hasUsedItem = bookingRepository.existsByItemIdAndBookerIdAndEndBeforeAndStatus(
+ itemId, userId, LocalDateTime.now(), Booking.BookingStatus.APPROVED);
+
+ if (!hasUsedItem) {
+ throw new ValidationException("Пользователь не брал эту вещь или бронирование не завершено");
+ }
+
+ Comment comment = Comment.builder()
+ .message(dto.getText())
+ .author(author)
+ .item(item)
+ .created(LocalDateTime.now())
+ .build();
+
+ return commentRepository.save(comment);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/comment/Comment.java b/src/main/java/ru/practicum/shareit/item/comment/Comment.java
new file mode 100644
index 0000000..4869997
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/comment/Comment.java
@@ -0,0 +1,49 @@
+package ru.practicum.shareit.item.comment;
+
+import jakarta.persistence.*;
+import lombok.*;
+import ru.practicum.shareit.item.Item;
+import ru.practicum.shareit.user.User;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "comments")
+@Builder
+public class Comment {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private String message;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "item_id", nullable = false)
+ private Item item;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "author_id", nullable = false)
+ private User author;
+
+ @Column(nullable = false)
+ private LocalDateTime created;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Comment comment = (Comment) o;
+ return id != null && id.equals(comment.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 17;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentDto.java b/src/main/java/ru/practicum/shareit/item/comment/CommentDto.java
new file mode 100644
index 0000000..ee2fcb3
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/comment/CommentDto.java
@@ -0,0 +1,15 @@
+package ru.practicum.shareit.item.comment;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+public class CommentDto {
+ private Long id;
+ private String text;
+ private String authorName;
+ private LocalDateTime created;
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java
new file mode 100644
index 0000000..b29d429
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java
@@ -0,0 +1,28 @@
+package ru.practicum.shareit.item.comment;
+
+
+import ru.practicum.shareit.item.Item;
+import ru.practicum.shareit.user.User;
+
+import java.time.LocalDateTime;
+
+public class CommentMapper {
+
+ public static CommentDto toDto(Comment comment) {
+ return CommentDto.builder()
+ .id(comment.getId())
+ .text(comment.getMessage())
+ .authorName(comment.getAuthor().getName())
+ .created(comment.getCreated())
+ .build();
+ }
+
+ public static Comment toComment(CommentDto dto, Item item, User author) {
+ return Comment.builder()
+ .message(dto.getText())
+ .item(item)
+ .author(author)
+ .created(LocalDateTime.now())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java
new file mode 100644
index 0000000..2b52c55
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java
@@ -0,0 +1,9 @@
+package ru.practicum.shareit.item.comment;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface CommentRepository extends JpaRepository {
+ List findByItemIdOrderByCreatedDesc(Long itemId);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java
deleted file mode 100644
index c7ce218..0000000
--- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package ru.practicum.shareit.item.mapper;
-
-import ru.practicum.shareit.item.dto.ItemDto;
-import ru.practicum.shareit.item.model.Item;
-import ru.practicum.shareit.request.ItemRequest;
-import ru.practicum.shareit.user.model.User;
-
-public class ItemMapper {
-
- public static ItemDto toItemDto(Item item) {
- return ItemDto.builder()
- .id(item.getId())
- .name(item.getName())
- .description(item.getDescription())
- .available(item.getAvailable())
- .requestId(item.getRequest() != null ? item.getRequest().getId() : null)
- .build();
- }
-
- public static Item toItem(ItemDto itemDto, User owner, ItemRequest itemRequest) {
- return Item.builder()
- .name(itemDto.getName())
- .description(itemDto.getDescription())
- .available(itemDto.getAvailable())
- .ownerId(owner.getId())
- .request(itemRequest)
- .build();
- }
-
- public static Item updateItemFields(Item item, ItemDto itemDto) {
- if (itemDto.getDescription() != null) {
- item.setDescription(itemDto.getDescription());
- }
- if (itemDto.getName() != null) {
- item.setName(itemDto.getName());
- }
- if (itemDto.getAvailable() != null) {
- item.setAvailable(itemDto.getAvailable());
- }
- return item;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java
deleted file mode 100644
index f0e6ad2..0000000
--- a/src/main/java/ru/practicum/shareit/item/model/Item.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package ru.practicum.shareit.item.model;
-
-import lombok.Builder;
-import lombok.Data;
-import ru.practicum.shareit.request.ItemRequest;
-
-@Data
-@Builder
-public class Item {
- private Long id;
- private String name;
- private String description;
- private Boolean available;
- private Long ownerId;
- private ItemRequest request;
-}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java
deleted file mode 100644
index 4cba349..0000000
--- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package ru.practicum.shareit.item.service;
-
-import ru.practicum.shareit.item.dto.ItemDto;
-import ru.practicum.shareit.item.model.Item;
-
-import java.util.List;
-
-public interface ItemService {
- Item create(Long userId, ItemDto itemDto);
-
- Item update(Long userId, Long itemId, ItemDto itemDto);
-
- Item getItemById(Long itemId);
-
- List
- getAllByOwner(Long userId);
-
- List
- search(String text);
-}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java
deleted file mode 100644
index 42542c7..0000000
--- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package ru.practicum.shareit.item.service;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import ru.practicum.shareit.exception.NotFoundException;
-import ru.practicum.shareit.item.dto.ItemDto;
-import ru.practicum.shareit.item.mapper.ItemMapper;
-import ru.practicum.shareit.item.model.Item;
-import ru.practicum.shareit.user.model.User;
-import ru.practicum.shareit.user.service.UserService;
-
-import java.util.*;
-import java.util.concurrent.atomic.AtomicLong;
-
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class ItemServiceImpl implements ItemService {
- private final UserService userService;
- private final Map items = new HashMap<>();
- private final AtomicLong itemIdCounter = new AtomicLong(0);
-
- @Override
- public Item create(Long userId, ItemDto itemDto) {
- User owner = userService.getUserById(userId);
- Item item = ItemMapper.toItem(itemDto, owner, null); // пока request == null
- item.setId(getNextId());
- items.put(item.getId(), item);
-
- log.info("Вещь создана: {}", item);
- return item;
- }
-
- @Override
- public Item update(Long userId, Long itemId, ItemDto itemDto) {
- Item item = getItemById(itemId);
- if (!Objects.equals(item.getOwnerId(), userId)) {
- throw new NotFoundException("Редактировать может только владелец.");
- }
-
- Item update = ItemMapper.updateItemFields(item, itemDto);
- log.info("Вещь обновлена: {}", item);
- return update;
- }
-
- @Override
- public Item getItemById(Long itemId) {
- Item item = items.get(itemId);
- if (item == null) {
- log.error("Вещь с id {} не найдена.", itemId);
- throw new NotFoundException("Вещь с id " + itemId + " не найдена.");
- }
-
- log.info("Вещь с id {} получена.", itemId);
- return item;
- }
-
- @Override
- public List
- getAllByOwner(Long userId) {
- List
- itemsByOwner = items.values().stream()
- .filter(i -> i.getOwnerId().equals(userId))
- .toList();
-
- log.info("Получен список всех вещей, сдаваемых пользователем с ID {}", userId);
- return itemsByOwner;
- }
-
- @Override
- public List
- search(String text) {
- if (text == null || text.isBlank()) {
- log.info("Пустой запрос для поиска");
- return List.of();
- }
- return findAvailableItemsByText(text);
- }
-
- public List
- findAvailableItemsByText(String text) {
- String lower = text.toLowerCase();
- return items.values().stream()
- .filter(i -> Boolean.TRUE.equals(i.getAvailable()))
- .filter(i -> i.getName().toLowerCase().contains(lower) ||
- i.getDescription().toLowerCase().contains(lower))
- .toList();
- }
-
- public long getNextId() {
- return itemIdCounter.incrementAndGet();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/ItemRequestDto.java
similarity index 64%
rename from src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java
rename to src/main/java/ru/practicum/shareit/request/ItemRequestDto.java
index 7b3ed54..c0ba079 100644
--- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java
+++ b/src/main/java/ru/practicum/shareit/request/ItemRequestDto.java
@@ -1,4 +1,4 @@
-package ru.practicum.shareit.request.dto;
+package ru.practicum.shareit.request;
/**
* TODO Sprint add-item-requests.
diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java
new file mode 100644
index 0000000..3d38212
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/User.java
@@ -0,0 +1,37 @@
+package ru.practicum.shareit.user;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "users")
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 512, unique = true)
+ private String email;
+
+ @Column(nullable = false, length = 255)
+ private String name;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ User user = (User) o;
+ return id != null && id.equals(user.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 14;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java
index 4244c8a..1cc6ab2 100644
--- a/src/main/java/ru/practicum/shareit/user/UserController.java
+++ b/src/main/java/ru/practicum/shareit/user/UserController.java
@@ -4,9 +4,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
-import ru.practicum.shareit.user.dto.UserDto;
-import ru.practicum.shareit.user.mapper.UserMapper;
-import ru.practicum.shareit.user.service.UserService;
import java.util.List;
diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/UserDto.java
similarity index 72%
rename from src/main/java/ru/practicum/shareit/user/dto/UserDto.java
rename to src/main/java/ru/practicum/shareit/user/UserDto.java
index e956bd0..5a67957 100644
--- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java
+++ b/src/main/java/ru/practicum/shareit/user/UserDto.java
@@ -1,7 +1,6 @@
-package ru.practicum.shareit.user.dto;
+package ru.practicum.shareit.user;
import jakarta.validation.constraints.Email;
-import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
@@ -11,7 +10,6 @@ public class UserDto {
private Long id;
@Email(message = "Некорректный email")
- @NotNull
private String email;
private String name;
}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java
similarity index 82%
rename from src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java
rename to src/main/java/ru/practicum/shareit/user/UserMapper.java
index b673bc4..7cb4381 100644
--- a/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java
+++ b/src/main/java/ru/practicum/shareit/user/UserMapper.java
@@ -1,7 +1,4 @@
-package ru.practicum.shareit.user.mapper;
-
-import ru.practicum.shareit.user.model.User;
-import ru.practicum.shareit.user.dto.UserDto;
+package ru.practicum.shareit.user;
public class UserMapper {
diff --git a/src/main/java/ru/practicum/shareit/user/UserRepository.java b/src/main/java/ru/practicum/shareit/user/UserRepository.java
new file mode 100644
index 0000000..6f3696f
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/UserRepository.java
@@ -0,0 +1,8 @@
+package ru.practicum.shareit.user;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserRepository extends JpaRepository {
+
+ boolean existsByEmail(String email);
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java
similarity index 60%
rename from src/main/java/ru/practicum/shareit/user/service/UserService.java
rename to src/main/java/ru/practicum/shareit/user/UserService.java
index 658346a..4098213 100644
--- a/src/main/java/ru/practicum/shareit/user/service/UserService.java
+++ b/src/main/java/ru/practicum/shareit/user/UserService.java
@@ -1,7 +1,4 @@
-package ru.practicum.shareit.user.service;
-
-import ru.practicum.shareit.user.model.User;
-import ru.practicum.shareit.user.dto.UserDto;
+package ru.practicum.shareit.user;
import java.util.List;
diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java
new file mode 100644
index 0000000..0ed417f
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java
@@ -0,0 +1,81 @@
+package ru.practicum.shareit.user;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.shareit.exception.ConflictException;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.exception.ValidationException;
+
+import java.util.*;
+
+@Service
+@Slf4j
+@Transactional
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+ private final UserRepository userRepository;
+
+ @Override
+ public User create(UserDto userDto) {
+ if (userRepository.existsByEmail(userDto.getEmail())) {
+ log.error("Ошибка создания пользователя: email {} уже используется", userDto.getEmail());
+ throw new ConflictException("Данный email уже используется");
+ }
+
+ User user = UserMapper.toUser(userDto);
+ User createdUser = userRepository.save(user);
+
+ log.info("Пользователь успешно создан с id: {}, email: {}",
+ user.getId(), user.getEmail());
+ return createdUser;
+ }
+
+ @Override
+ public User update(UserDto userDto) {
+ if (userDto.getId() == null || userDto.getId() == 0) {
+ throw new ValidationException("id должен быть указан");
+ }
+
+ User existingUser = userRepository.findById(userDto.getId())
+ .orElseThrow(() -> new NotFoundException("Пользователь не найден"));
+
+ if (userDto.getEmail() != null && !Objects.equals(userDto.getEmail(), existingUser.getEmail())) {
+ if (userRepository.existsByEmail(userDto.getEmail())) {
+ log.error("Ошибка обновления: email {} уже используется", userDto.getEmail());
+ throw new ConflictException("Этот e-mail уже используется");
+ }
+ existingUser.setEmail(userDto.getEmail());
+ }
+
+ UserMapper.updateUserFields(existingUser, userDto);
+ User updatedUser = userRepository.save(existingUser);
+
+ log.info("Пользователь с id {} успешно обновлён", updatedUser.getId());
+ return updatedUser;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public User getUserById(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> {
+ log.error("Пользователь с id {} не найден", userId);
+ return new NotFoundException("Пользователь с id " + userId + " не найден.");
+ });
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getAll() {
+ return userRepository.findAll();
+ }
+
+ @Override
+ public void delete(Long userId) {
+ User user = getUserById(userId);
+ userRepository.deleteById(userId);
+ log.info("Пользователь с id {} удалён", userId);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java
deleted file mode 100644
index 9a04c58..0000000
--- a/src/main/java/ru/practicum/shareit/user/model/User.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package ru.practicum.shareit.user.model;
-
-import lombok.Builder;
-import lombok.Data;
-
-@Data
-@Builder
-public class User {
- private Long id;
- private String email;
- private String name;
-}
\ No newline at end of file
diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java
deleted file mode 100644
index 19d63c8..0000000
--- a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package ru.practicum.shareit.user.service;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import ru.practicum.shareit.exception.ConflictException;
-import ru.practicum.shareit.exception.NotFoundException;
-import ru.practicum.shareit.exception.ValidationException;
-import ru.practicum.shareit.user.model.User;
-import ru.practicum.shareit.user.dto.UserDto;
-import ru.practicum.shareit.user.mapper.UserMapper;
-
-import java.util.*;
-import java.util.concurrent.atomic.AtomicLong;
-
-@Service
-@Slf4j
-public class UserServiceImpl implements UserService {
- private final Map users = new HashMap<>();
- private final Map usersByEmail = new HashMap<>();
- private final AtomicLong userIdCounter = new AtomicLong(0);
-
- @Override
- public User create(UserDto userDto) {
- if (usersByEmail.containsKey(userDto.getEmail())) {
- log.error("Ошибка создания пользователя: email {} уже используется", userDto.getEmail());
- throw new ConflictException("Данный email уже используется");
- }
-
- User user = UserMapper.toUser(userDto);
- user.setId(getNextId());
- users.put(user.getId(), user);
- usersByEmail.put(user.getEmail(), user);
-
- log.info("Пользователь успешно создан с id: {}, email: {}",
- user.getId(), user.getEmail());
- return user;
- }
-
- @Override
- public User update(UserDto userDto) {
- if (userDto.getId() == null || userDto.getId() == 0) {
- throw new ValidationException("id должен быть указан");
- }
- User existingUser = users.get(userDto.getId());
- if (existingUser == null) {
- log.error("Пользователь не найден");
- throw new NotFoundException("Пользователь не найден");
- }
- // проверяем доступность email
- if (!Objects.equals(userDto.getEmail(), existingUser.getEmail())) {
- if (usersByEmail.containsKey(userDto.getEmail())) {
- User existingUserByEmail = usersByEmail.get(userDto.getEmail());
- if (!Objects.equals(existingUserByEmail.getId(), userDto.getId())) {
- log.error("Ошибка обновления: email {} уже используется", userDto.getEmail());
- throw new ConflictException("Этот e-mail уже используется");
- }
- }
- usersByEmail.remove(existingUser.getEmail());
- existingUser.setEmail(userDto.getEmail());
- usersByEmail.put(existingUser.getEmail(), existingUser);
- }
-
- // Обновляем только изменяемые поля
- UserMapper.updateUserFields(existingUser, userDto);
-
- log.info("Пользователь с id {} успешно обновлён", existingUser.getId());
- return existingUser;
- }
-
- @Override
- public User getUserById(Long userId) {
- User user = users.get(userId);
- if (user == null) {
- log.error("Пользователь с id {} не найден.", userId);
- throw new NotFoundException("Пользователь с id " + userId + " не найден.");
- }
-
- log.info("Пользователь с id {} получен.", userId);
- return user;
- }
-
- @Override
- public List getAll() {
- return users.values().stream()
- .toList();
- }
-
- @Override
- public void delete(Long userId) {
- User user = getUserById(userId); // один вызов
- users.remove(userId);
- usersByEmail.remove(user.getEmail());
- log.info("Пользователь с id {} удален", userId);
- }
-
- public long getNextId() {
- return userIdCounter.incrementAndGet();
- }
-}
\ No newline at end of file
diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties
new file mode 100644
index 0000000..16ae096
--- /dev/null
+++ b/src/main/resources/application-test.properties
@@ -0,0 +1,10 @@
+spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+
+spring.jpa.hibernate.ddl-auto=none
+spring.sql.init.mode=always
+
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b9e5d4b..aef7a50 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,14 +1,15 @@
-spring.jpa.hibernate.ddl-auto=none
-spring.jpa.properties.hibernate.format_sql=true
-spring.sql.init.mode=always
-
logging.level.org.springframework.orm.jpa=INFO
logging.level.org.springframework.transaction=INFO
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
# TODO Append connection to DB
-#spring.datasource.driverClassName
-#spring.datasource.url
-#spring.datasource.username
-#spring.datasource.password
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.url=jdbc:postgresql://localhost:5432/shareIt
+spring.datasource.username=dbuser
+spring.datasource.password=12345
+
+spring.sql.init.mode=always
+spring.jpa.hibernate.ddl-auto=none
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
\ 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..a7a038d
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,41 @@
+CREATE TABLE IF NOT EXISTS users (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(512) NOT NULL,
+ CONSTRAINT pk_user PRIMARY KEY (id),
+ CONSTRAINT UQ_USER_EMAIL UNIQUE (email)
+);
+
+CREATE TABLE IF NOT EXISTS items (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ description TEXT NOT NULL,
+ is_available BOOLEAN NOT NULL,
+ owner_id BIGINT NOT NULL,
+ request_id BIGINT,
+ CONSTRAINT pk_item PRIMARY KEY (id),
+ CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS bookings (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ item_id BIGINT NOT NULL,
+ booker_id BIGINT NOT NULL,
+ status VARCHAR(20) NOT NULL,
+ CONSTRAINT pk_booking PRIMARY KEY (id),
+ CONSTRAINT fk_booking_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+ CONSTRAINT fk_booking_booker FOREIGN KEY (booker_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS comments (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ message TEXT NOT NULL,
+ item_id BIGINT NOT NULL,
+ author_id BIGINT NOT NULL,
+ created TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ CONSTRAINT pk_comment PRIMARY KEY (id),
+ CONSTRAINT fk_comment_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
+ CONSTRAINT fk_comment_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE
+);
\ No newline at end of file