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