From f7f4b08d46ee579e7ca54ec52f23dc2c71aad2d7 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 17:33:40 +0300 Subject: [PATCH 01/18] feat: implement all sprint 15 features (database, bookings, comments) --- pom.xml | 195 +++--------------- .../java/ru/practicum/shareit/ShareItApp.java | 4 +- .../ru/practicum/shareit/booking/Booking.java | 7 - .../shareit/booking/BookingController.java | 48 ++++- .../shareit/booking/BookingRepository.java | 40 ++++ .../shareit/booking/BookingService.java | 19 ++ .../shareit/booking/BookingServiceImpl.java | 151 ++++++++++++++ .../shareit/booking/dto/BookingDto.java | 28 ++- .../booking/dto/BookingResponseDto.java | 23 +++ .../shareit/booking/mapper/BookingMapper.java | 20 ++ .../shareit/booking/model/Booking.java | 40 ++++ .../shareit/booking/model/Status.java | 8 + .../shareit/exception/ConflictException.java | 7 + .../shareit/exception/ErrorHandler.java | 50 +++++ .../shareit/exception/NotFoundException.java | 7 + .../exception/ValidationException.java | 7 + .../shareit/item/CommentRepository.java | 10 + .../shareit/item/ItemController.java | 53 ++++- .../shareit/item/ItemRepository.java | 20 ++ .../practicum/shareit/item/ItemService.java | 21 ++ .../shareit/item/ItemServiceImpl.java | 125 +++++++++++ .../shareit/item/dto/CommentDto.java | 19 ++ .../practicum/shareit/item/dto/ItemDto.java | 33 ++- .../shareit/item/mapper/CommentMapper.java | 15 ++ .../shareit/item/mapper/ItemMapper.java | 33 +++ .../practicum/shareit/item/model/Comment.java | 34 +++ .../ru/practicum/shareit/item/model/Item.java | 47 ++++- .../request/ItemRequestController.java | 4 +- .../java/ru/practicum/shareit/user/User.java | 33 ++- .../shareit/user/UserController.java | 40 +++- .../shareit/user/UserRepository.java | 11 + .../practicum/shareit/user/UserService.java | 17 ++ .../shareit/user/UserServiceImpl.java | 71 +++++++ .../practicum/shareit/user/dto/UserDto.java | 27 +++ .../shareit/user/mapper/UserMapper.java | 14 ++ src/main/resources/application.properties | 14 +- src/main/resources/schema.sql | 41 ++++ 37 files changed, 1116 insertions(+), 220 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/booking/Booking.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingRepository.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingService.java create mode 100644 src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java create mode 100644 src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java create mode 100644 src/main/java/ru/practicum/shareit/booking/model/Booking.java create mode 100644 src/main/java/ru/practicum/shareit/booking/model/Status.java create mode 100644 src/main/java/ru/practicum/shareit/exception/ConflictException.java create mode 100644 src/main/java/ru/practicum/shareit/exception/ErrorHandler.java create mode 100644 src/main/java/ru/practicum/shareit/exception/NotFoundException.java create mode 100644 src/main/java/ru/practicum/shareit/exception/ValidationException.java create mode 100644 src/main/java/ru/practicum/shareit/item/CommentRepository.java create mode 100644 src/main/java/ru/practicum/shareit/item/ItemRepository.java create mode 100644 src/main/java/ru/practicum/shareit/item/ItemService.java create mode 100644 src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/item/dto/CommentDto.java create mode 100644 src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java create mode 100644 src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java create mode 100644 src/main/java/ru/practicum/shareit/item/model/Comment.java create mode 100644 src/main/java/ru/practicum/shareit/user/UserRepository.java create mode 100644 src/main/java/ru/practicum/shareit/user/UserService.java create mode 100644 src/main/java/ru/practicum/shareit/user/UserServiceImpl.java create mode 100644 src/main/java/ru/practicum/shareit/user/dto/UserDto.java create mode 100644 src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java create mode 100644 src/main/resources/schema.sql diff --git a/pom.xml b/pom.xml index 2db888c..736dcc6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,15 +6,13 @@ org.springframework.boot spring-boot-starter-parent 3.3.2 - + ru.practicum shareit 0.0.1-SNAPSHOT - ShareIt - 21 @@ -26,12 +24,12 @@ org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-validation + org.springframework.boot - spring-boot-configuration-processor - true + spring-boot-starter-data-jpa @@ -40,35 +38,29 @@ runtime + + com.h2database + h2 + runtime + org.projectlombok lombok true - - - com.h2database - h2 - test - org.springframework.boot spring-boot-starter-test test + org.springframework.boot - spring-boot-starter-validation + spring-boot-starter-actuator - - - src/main/resources - true - - org.springframework.boot @@ -83,130 +75,8 @@ - - - - org.apache.maven.plugins - maven-surefire-plugin - - - test - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.2 - - checkstyle.xml - true - true - true - - - - - check - - compile - - - - - com.puppycrawl.tools - checkstyle - 10.3 - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.5.0 - - Max - High - - - - - check - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - file - - - - jacoco-initialize - - prepare-agent - - - - jacoco-check - - check - - - - - BUNDLE - - - INSTRUCTION - COVEREDRATIO - 0.01 - - - LINE - COVEREDRATIO - 0.9 - - - BRANCH - COVEREDRATIO - 0.6 - - - COMPLEXITY - COVEREDRATIO - 0.6 - - - METHOD - COVEREDRATIO - 0.7 - - - CLASS - MISSEDCOUNT - 1 - - - - - - - - jacoco-report - test - - report - - - - - - + check @@ -215,33 +85,24 @@ org.apache.maven.plugins maven-checkstyle-plugin - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - - - - - - coverage - - - - org.jacoco - jacoco-maven-plugin + 3.3.1 + + checkstyle.xml + true + true + true + + + + + check + + compile + + - - + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/src/main/java/ru/practicum/shareit/ShareItApp.java index a00ad56..0592a88 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/src/main/java/ru/practicum/shareit/ShareItApp.java @@ -5,9 +5,7 @@ @SpringBootApplication public class ShareItApp { - public static void main(String[] args) { SpringApplication.run(ShareItApp.class, args); } - -} +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java deleted file mode 100644 index 2d9c666..0000000 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.booking; - -/** - * TODO Sprint add-bookings. - */ -public class Booking { -} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java index b94493d..bc4a7fe 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,12 +1,48 @@ package ru.practicum.shareit.booking; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; + +import jakarta.validation.Valid; +import java.util.List; -/** - * TODO Sprint add-bookings. - */ @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { -} + private final BookingService bookingService; + private static final String USER_ID_HEADER = "X-Sharer-User-Id"; + + @PostMapping + public BookingResponseDto create(@RequestHeader(USER_ID_HEADER) Long userId, + @Valid @RequestBody BookingDto bookingDto) { + return bookingService.create(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public BookingResponseDto approve(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId, + @RequestParam boolean approved) { + return bookingService.approve(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public BookingResponseDto getById(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId) { + return bookingService.getById(userId, bookingId); + } + + @GetMapping + public List getAllByBooker(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByBooker(userId, state); + } + + @GetMapping("/owner") + public List getAllByOwner(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return bookingService.getAllByOwner(userId, state); + } +} \ 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..1b9c6bf --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.booking; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; + +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + + List findAllByBookerIdOrderByStartDesc(Long bookerId); + + List findAllByBookerIdAndStatusOrderByStartDesc(Long bookerId, Status status); + + @Query("select b from Booking b where b.booker.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findAllCurrentByBookerId(Long bookerId, LocalDateTime now); + + @Query("select b from Booking b where b.booker.id = ?1 and b.end < ?2 order by b.start desc") + List findAllPastByBookerId(Long bookerId, LocalDateTime now); + + @Query("select b from Booking b where b.booker.id = ?1 and b.start > ?2 order by b.start desc") + List findAllFutureByBookerId(Long bookerId, LocalDateTime now); + + List findAllByItemOwnerIdOrderByStartDesc(Long ownerId); + + List findAllByItemOwnerIdAndStatusOrderByStartDesc(Long ownerId, Status status); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findAllCurrentByOwnerId(Long ownerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.end < ?2 order by b.start desc") + List findAllPastByOwnerId(Long ownerId, LocalDateTime now); + + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start > ?2 order by b.start desc") + List findAllFutureByOwnerId(Long ownerId, LocalDateTime now); + + List findAllByItemIdOrderByStartAsc(Long itemId); +} \ 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..935b068 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.booking; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; + +import java.util.List; + +public interface BookingService { + + BookingResponseDto create(Long userId, BookingDto bookingDto); + + BookingResponseDto approve(Long userId, Long bookingId, boolean approved); + + BookingResponseDto getById(Long userId, Long bookingId); + + List getAllByBooker(Long userId, String state); + + List getAllByOwner(Long userId, String 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..f2ee901 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -0,0 +1,151 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Transactional + @Override + public BookingResponseDto create(Long userId, BookingDto bookingDto) { + User booker = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь не найден")); + Item item = itemRepository.findById(bookingDto.getItemId()) + .orElseThrow(() -> new NotFoundException("Вещь не найдена")); + + if (!item.getAvailable()) { + throw new ValidationException("Вещь недоступна для бронирования"); + } + if (item.getOwner().getId().equals(userId)) { + throw new NotFoundException("Владелец не может забронировать свою вещь"); + } + if (bookingDto.getEnd().isBefore(bookingDto.getStart()) || bookingDto.getEnd().equals(bookingDto.getStart())) { + throw new ValidationException("Некорректные даты бронирования"); + } + + Booking booking = new Booking(); + booking.setStart(bookingDto.getStart()); + booking.setEnd(bookingDto.getEnd()); + booking.setItem(item); + booking.setBooker(booker); + booking.setStatus(Status.WAITING); + + return BookingMapper.toBookingResponse(bookingRepository.save(booking)); + } + + @Transactional + @Override + public BookingResponseDto approve(Long userId, Long bookingId, boolean approved) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); + + if (!booking.getItem().getOwner().getId().equals(userId)) { + throw new ValidationException("Только владелец может подтвердить бронирование"); + } + + if (booking.getStatus() != Status.WAITING) { + throw new ValidationException("Статус уже изменен"); + } + + booking.setStatus(approved ? Status.APPROVED : Status.REJECTED); + return BookingMapper.toBookingResponse(bookingRepository.save(booking)); + } + + @Override + public BookingResponseDto getById(Long userId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); + + if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { + throw new NotFoundException("Нет прав для просмотра этого бронирования"); + } + + return BookingMapper.toBookingResponse(booking); + } + + @Override + public List getAllByBooker(Long userId, String state) { + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь не найден")); + LocalDateTime now = LocalDateTime.now(); + List bookings; + + switch (state.toUpperCase()) { + case "ALL": + bookings = bookingRepository.findAllByBookerIdOrderByStartDesc(userId); + break; + case "CURRENT": + bookings = bookingRepository.findAllCurrentByBookerId(userId, now); + break; + case "PAST": + bookings = bookingRepository.findAllPastByBookerId(userId, now); + break; + case "FUTURE": + bookings = bookingRepository.findAllFutureByBookerId(userId, now); + break; + case "WAITING": + bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.WAITING); + break; + case "REJECTED": + bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); + break; + default: + throw new ValidationException("Unknown state: " + state); + } + + return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); + } + + @Override + public List getAllByOwner(Long userId, String state) { + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь не найден")); + LocalDateTime now = LocalDateTime.now(); + List bookings; + + switch (state.toUpperCase()) { + case "ALL": + bookings = bookingRepository.findAllByItemOwnerIdOrderByStartDesc(userId); + break; + case "CURRENT": + bookings = bookingRepository.findAllCurrentByOwnerId(userId, now); + break; + case "PAST": + bookings = bookingRepository.findAllPastByOwnerId(userId, now); + break; + case "FUTURE": + bookings = bookingRepository.findAllFutureByOwnerId(userId, now); + break; + case "WAITING": + bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.WAITING); + break; + case "REJECTED": + bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); + break; + default: + throw new ValidationException("Unknown state: " + state); + } + + return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); + } +} \ 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 index 861de9e..ffb2238 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -1,7 +1,27 @@ package ru.practicum.shareit.booking.dto; -/** - * TODO Sprint add-bookings. - */ +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class BookingDto { -} + + @NotNull + Long itemId; + + @NotNull + @FutureOrPresent + LocalDateTime start; + + @NotNull + @Future + LocalDateTime end; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java new file mode 100644 index 0000000..cd6366d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class BookingResponseDto { + + Long id; + LocalDateTime start; + LocalDateTime end; + ItemDto item; + UserDto booker; + Status status; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java new file mode 100644 index 0000000..8576851 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.mapper; + +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.user.mapper.UserMapper; + +public class BookingMapper { + + public static BookingResponseDto toBookingResponse(Booking booking) { + return new BookingResponseDto( + booking.getId(), + booking.getStart(), + booking.getEnd(), + ItemMapper.toItemDto(booking.getItem()), + UserMapper.toUserDto(booking.getBooker()), + booking.getStatus() + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java new file mode 100644 index 0000000..0439c60 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "bookings") +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "start_date", nullable = false) + LocalDateTime start; + + @Column(name = "end_date", nullable = false) + LocalDateTime end; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id", nullable = false) + User booker; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + Status status; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/Status.java b/src/main/java/ru/practicum/shareit/booking/model/Status.java new file mode 100644 index 0000000..f51353b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/model/Status.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.model; + +public enum Status { + WAITING, + APPROVED, + REJECTED, + CANCELED +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/ConflictException.java b/src/main/java/ru/practicum/shareit/exception/ConflictException.java new file mode 100644 index 0000000..12f8fe0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ConflictException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java b/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java new file mode 100644 index 0000000..d4d9f03 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ErrorHandler.java @@ -0,0 +1,50 @@ +package ru.practicum.shareit.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class ErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map handleNotFound(final NotFoundException e) { + log.error("404 Not Found: {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.CONFLICT) + public Map handleConflict(final ConflictException e) { + log.error("409 Conflict: {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleValidation(final ValidationException e) { + log.error("400 Bad Request (Validation): {}", e.getMessage()); + return Map.of("error", e.getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleMethodArgumentNotValid(final MethodArgumentNotValidException e) { + log.error("400 Bad Request (Argument Not Valid): {}", e.getMessage()); + return Map.of("error", "Ошибка валидации данных"); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Map handleThrowable(final Throwable e) { + log.error("500 Internal Server Error: ", e); + return Map.of("error", "Произошла непредвиденная ошибка"); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/NotFoundException.java b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java new file mode 100644 index 0000000..98610d2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/exception/ValidationException.java b/src/main/java/ru/practicum/shareit/exception/ValidationException.java new file mode 100644 index 0000000..067eded --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/CommentRepository.java new file mode 100644 index 0000000..43f4499 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.item; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findAllByItemId(Long itemId); +} \ 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 bb17668..d667bda 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,12 +1,53 @@ package ru.practicum.shareit.item; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; + +import jakarta.validation.Valid; +import java.util.List; -/** - * TODO Sprint add-controllers. - */ @RestController @RequestMapping("/items") +@RequiredArgsConstructor public class ItemController { -} + private final ItemService itemService; + private static final String SHARER_USER_ID = "X-Sharer-User-Id"; + + @PostMapping + public ItemDto create(@RequestHeader(SHARER_USER_ID) Long userId, + @Valid @RequestBody ItemDto itemDto) { + return itemService.create(userId, itemDto); + } + + @PatchMapping("/{itemId}") + public ItemDto update(@RequestHeader(SHARER_USER_ID) Long userId, + @PathVariable Long itemId, + @RequestBody ItemDto itemDto) { + return itemService.update(userId, itemId, itemDto); + } + + @GetMapping + public List getAllByOwner(@RequestHeader(SHARER_USER_ID) Long userId) { + return itemService.getAllByOwnerId(userId); + } + + @GetMapping("/search") + public List search(@RequestParam String text) { + return itemService.search(text); + } + + @GetMapping("/{itemId}") + public ItemDto getById(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId) { + return itemService.getById(itemId, userId); + } + + @PostMapping("/{itemId}/comment") + public CommentDto createComment(@RequestHeader(SHARER_USER_ID) Long userId, + @PathVariable Long itemId, + @Valid @RequestBody CommentDto commentDto) { + return itemService.createComment(userId, itemId, commentDto); + } +} \ 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..a3839c1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemRepository.java @@ -0,0 +1,20 @@ +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 ru.practicum.shareit.item.model.Item; + +import java.util.List; + +public interface ItemRepository extends JpaRepository { + + List findAllByOwnerId(Long ownerId); + + @Query("SELECT it FROM Item it " + + "WHERE it.available = true " + + "AND (LOWER(it.name) LIKE LOWER(CONCAT('%', :text, '%')) " + + "OR LOWER(it.description) LIKE LOWER(CONCAT('%', :text, '%')))") + List searchByText(@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..c907238 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.item; + +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.util.List; + +public interface ItemService { + + ItemDto create(Long userId, ItemDto itemDto); + + ItemDto update(Long userId, Long itemId, ItemDto itemDto); + + ItemDto getById(Long itemId, Long userId); + + List getAllByOwnerId(Long ownerId); + + List search(String text); + + CommentDto createComment(Long userId, Long itemId, CommentDto commentDto); +} \ 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..3e445c7 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -0,0 +1,125 @@ +package ru.practicum.shareit.item; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.BookingRepository; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.mapper.CommentMapper; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ItemServiceImpl implements ItemService { + private final ItemRepository itemRepository; + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; + + @Transactional + @Override + public ItemDto create(Long userId, ItemDto itemDto) { + User owner = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found")); + Item item = ItemMapper.toItem(itemDto, owner); + return ItemMapper.toItemDto(itemRepository.save(item)); + } + + @Transactional + @Override + public ItemDto update(Long userId, Long itemId, ItemDto itemDto) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Item not found")); + if (!item.getOwner().getId().equals(userId)) { + throw new NotFoundException("Not an owner"); + } + 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 ItemMapper.toItemDto(itemRepository.save(item)); + } + + @Override + public ItemDto getById(Long itemId, Long userId) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Item not found")); + ItemDto dto = ItemMapper.toItemDto(item); + + dto.setComments(commentRepository.findAllByItemId(itemId).stream() + .map(CommentMapper::toCommentDto).collect(Collectors.toList())); + + if (item.getOwner().getId().equals(userId)) { + setBookings(dto, bookingRepository.findAllByItemIdOrderByStartAsc(itemId)); + } + return dto; + } + + @Override + public List getAllByOwnerId(Long ownerId) { + List items = itemRepository.findAllByOwnerId(ownerId); + return items.stream().map(item -> { + ItemDto dto = ItemMapper.toItemDto(item); + dto.setComments(commentRepository.findAllByItemId(item.getId()).stream() + .map(CommentMapper::toCommentDto).collect(Collectors.toList())); + setBookings(dto, bookingRepository.findAllByItemIdOrderByStartAsc(item.getId())); + return dto; + }).sorted(Comparator.comparing(ItemDto::getId)).collect(Collectors.toList()); + } + + @Override + public List search(String text) { + if (text.isBlank()) return Collections.emptyList(); + return itemRepository.searchByText(text).stream() + .map(ItemMapper::toItemDto).collect(Collectors.toList()); + } + + @Transactional + @Override + public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { + User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); + + List userBookings = bookingRepository.findAllPastByBookerId(userId, LocalDateTime.now()); + boolean hasFinishedBooking = userBookings.stream() + .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); + + if (!hasFinishedBooking) { + throw new ValidationException("No finished bookings for this item"); + } + + Comment comment = new Comment(); + comment.setText(commentDto.getText()); + comment.setItem(item); + comment.setAuthor(user); + comment.setCreated(LocalDateTime.now()); + + return CommentMapper.toCommentDto(commentRepository.save(comment)); + } + + private void setBookings(ItemDto dto, List bookings) { + LocalDateTime now = LocalDateTime.now(); + Booking last = bookings.stream() + .filter(b -> b.getStart().isBefore(now)) + .reduce((first, second) -> second).orElse(null); + Booking next = bookings.stream() + .filter(b -> b.getStart().isAfter(now)) + .findFirst().orElse(null); + + if (last != null) dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); + if (next != null) dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java new file mode 100644 index 0000000..f50e263 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class CommentDto { + Long id; + @NotBlank + String text; + String authorName; + LocalDateTime created; +} \ 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/dto/ItemDto.java index 9319d7d..3a66de4 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -1,7 +1,32 @@ package ru.practicum.shareit.item.dto; -/** - * TODO Sprint add-controllers. - */ +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.FieldDefaults; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class ItemDto { -} + Long id; + @NotBlank + String name; + @NotBlank + String description; + @NotNull + Boolean available; + BookingShortDto lastBooking; + BookingShortDto nextBooking; + List comments; + + @Data + @AllArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE) + public static class BookingShortDto { + Long id; + Long bookerId; + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java new file mode 100644 index 0000000..9eef3d8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.mapper; + +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.model.Comment; + +public class CommentMapper { + public static CommentDto toCommentDto(Comment comment) { + return new CommentDto( + comment.getId(), + comment.getText(), + comment.getAuthor().getName(), + comment.getCreated() + ); + } +} \ 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 new file mode 100644 index 0000000..f7bf3b8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.item.mapper; + +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.User; + +import java.util.ArrayList; + +public class ItemMapper { + + public static ItemDto toItemDto(Item item) { + return new ItemDto( + item.getId(), + item.getName(), + item.getDescription(), + item.getAvailable(), + null, // lastBooking + null, // nextBooking + new ArrayList<>() // пустой список комментариев + ); + } + + public static Item toItem(ItemDto itemDto, User owner) { + return new Item( + itemDto.getId(), + itemDto.getName(), + itemDto.getDescription(), + itemDto.getAvailable(), + owner, + null + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java new file mode 100644 index 0000000..cead782 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,34 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(nullable = false) + String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + User author; + + @Column(nullable = false) + LocalDateTime created; +} \ 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 index 44eb73d..cae5aef 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,7 +1,46 @@ package ru.practicum.shareit.item.model; -/** - * TODO Sprint add-controllers. - */ +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.User; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "items") public class Item { -} + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "name", nullable = false) + String name; + + @Column(name = "description", nullable = false) + String description; + + @Column(name = "is_available", nullable = false) + Boolean available; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) + User owner; + + @Column(name = "request_id") + Long requestId; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java index 064e2e9..f405a54 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -3,9 +3,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -/** - * TODO Sprint add-item-requests. - */ + @RestController @RequestMapping(path = "/requests") public class ItemRequestController { diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java index ae6e7f3..2403dd3 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/User.java @@ -1,7 +1,32 @@ package ru.practicum.shareit.user; -/** - * TODO Sprint add-controllers. - */ +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Entity +@Table(name = "users") public class User { -} + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @Column(name = "name", nullable = false) + String name; + + @Column(name = "email", nullable = false, unique = true) + String email; +} \ 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 03039b9..d9f7da2 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,12 +1,40 @@ package ru.practicum.shareit.user; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.UserDto; + +import jakarta.validation.Valid; +import java.util.List; -/** - * TODO Sprint add-controllers. - */ @RestController @RequestMapping(path = "/users") +@RequiredArgsConstructor public class UserController { -} + private final UserService userService; + + @PostMapping + public UserDto create(@Valid @RequestBody UserDto userDto) { + return userService.create(userDto); + } + + @PatchMapping("/{id}") + public UserDto update(@PathVariable Long id, @RequestBody UserDto userDto) { + return userService.update(id, userDto); + } + + @GetMapping("/{id}") + public UserDto getById(@PathVariable Long id) { + return userService.getById(id); + } + + @GetMapping + public List getAll() { + return userService.getAll(); + } + + @DeleteMapping("/{id}") + public void deleteById(@PathVariable Long id) { + userService.deleteById(id); + } +} \ No newline at end of file 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..97fdbc6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); + +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java new file mode 100644 index 0000000..993b0a3 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserService.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user; + +import ru.practicum.shareit.user.dto.UserDto; +import java.util.List; + +public interface UserService { + + UserDto create(UserDto userDto); + + UserDto update(Long id, UserDto userDto); + + UserDto getById(Long id); + + List getAll(); + + void deleteById(Long id); +} \ No newline at end of file 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..78c835d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -0,0 +1,71 @@ +package ru.practicum.shareit.user; + +import lombok.RequiredArgsConstructor; +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.user.dto.UserDto; +import ru.practicum.shareit.user.mapper.UserMapper; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + @Transactional + @Override + public UserDto create(UserDto userDto) { + Optional existingUser = userRepository.findByEmail(userDto.getEmail()); + if (existingUser.isPresent()) { + throw new ConflictException("Email уже используется"); + } + User user = UserMapper.toUser(userDto); + return UserMapper.toUserDto(userRepository.save(user)); + } + + @Transactional + @Override + public UserDto update(Long id, UserDto userDto) { + User userToUpdate = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь не найден")); + + if (userDto.getEmail() != null && !userDto.getEmail().isBlank()) { + Optional userWithSameEmail = userRepository.findByEmail(userDto.getEmail()); + if (userWithSameEmail.isPresent() && !userWithSameEmail.get().getId().equals(id)) { + throw new ConflictException("Email уже используется другим пользователем"); + } + userToUpdate.setEmail(userDto.getEmail()); + } + if (userDto.getName() != null && !userDto.getName().isBlank()) { + userToUpdate.setName(userDto.getName()); + } + + return UserMapper.toUserDto(userRepository.save(userToUpdate)); + } + + @Override + public UserDto getById(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь не найден")); + return UserMapper.toUserDto(user); + } + + @Override + public List getAll() { + return userRepository.findAll().stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } + + @Transactional + @Override + public void deleteById(Long id) { + userRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java new file mode 100644 index 0000000..c188010 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class UserDto { + Long id; + + @NotBlank(message = "Имя не может быть пустым") + + String name; + + @NotNull(message = "Email не может быть null") + + @Email(message = "Некорректный формат email") + + String email; +} \ 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/mapper/UserMapper.java new file mode 100644 index 0000000..26b1b95 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.user.mapper; + +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.User; + +public class UserMapper { + public static UserDto toUserDto(User user) { + return new UserDto(user.getId(), user.getName(), user.getEmail()); + } + + public static User toUser(UserDto userDto) { + return new User(userDto.getId(), userDto.getName(), userDto.getEmail()); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 51c5180..065c14f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,5 @@ +server.port=8080 + spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always @@ -5,10 +7,10 @@ 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 +logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +TODO Append connection to Postgres DB +spring.datasource.driverClassName +spring.datasource.url +spring.datasource.username +spring.datasource.password diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..b55fba7 --- /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 VARCHAR(512) NOT NULL, + is_available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + request_id BIGINT, + CONSTRAINT pk_item PRIMARY KEY (id), + CONSTRAINT fk_items_to_users 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(50) NOT NULL, + CONSTRAINT pk_booking PRIMARY KEY (id), + CONSTRAINT fk_booking_to_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + CONSTRAINT fk_booking_to_user FOREIGN KEY (booker_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + text VARCHAR(1000) 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_to_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + CONSTRAINT fk_comment_to_user FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file From 79ca6a346f307b52bc5acaf432e9e415bf6747b0 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 17:45:50 +0300 Subject: [PATCH 02/18] fix: fix syntax errors in ItemServiceImpl and remove duplicate dependencies --- pom.xml | 5 ----- .../ru/practicum/shareit/item/ItemServiceImpl.java | 12 +++++++++--- .../ru/practicum/shareit/item/mapper/ItemMapper.java | 8 +++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 736dcc6..3828561 100644 --- a/pom.xml +++ b/pom.xml @@ -26,18 +26,15 @@ org.springframework.boot spring-boot-starter-validation - org.springframework.boot spring-boot-starter-data-jpa - org.postgresql postgresql runtime - com.h2database h2 @@ -53,13 +50,11 @@ spring-boot-starter-test test - org.springframework.boot spring-boot-starter-actuator - diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 3e445c7..f0e97d5 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -82,7 +82,9 @@ public List getAllByOwnerId(Long ownerId) { @Override public List search(String text) { - if (text.isBlank()) return Collections.emptyList(); + if (text == null || text.isBlank()) { + return Collections.emptyList(); + } return itemRepository.searchByText(text).stream() .map(ItemMapper::toItemDto).collect(Collectors.toList()); } @@ -119,7 +121,11 @@ private void setBookings(ItemDto dto, List bookings) { .filter(b -> b.getStart().isAfter(now)) .findFirst().orElse(null); - if (last != null) dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); - if (next != null) dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); + if (last != null) { + dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); + } + if (next != null) { + dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); + } } } \ 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 index f7bf3b8..e368b4d 100644 --- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -3,20 +3,18 @@ import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.User; - import java.util.ArrayList; public class ItemMapper { - public static ItemDto toItemDto(Item item) { return new ItemDto( item.getId(), item.getName(), item.getDescription(), item.getAvailable(), - null, // lastBooking - null, // nextBooking - new ArrayList<>() // пустой список комментариев + null, + null, + new ArrayList<>() ); } From 9ad64b1cd26fb6dc9e03612629b3bd1e13e33421 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 17:51:59 +0300 Subject: [PATCH 03/18] fix: fix syntax errors in ItemServiceImpl and remove duplicate dependencies --- src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index f0e97d5..68f89a5 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -79,7 +79,7 @@ public List getAllByOwnerId(Long ownerId) { return dto; }).sorted(Comparator.comparing(ItemDto::getId)).collect(Collectors.toList()); } - + @Override public List search(String text) { if (text == null || text.isBlank()) { From 910260811b95792666d125eb04f30f8c02c54db1 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 17:59:47 +0300 Subject: [PATCH 04/18] fix: remove old repository implementations for good --- .../shareit/item/ItemRepositoryImpl.java | 52 ------------------- .../shareit/user/UserRepositoryImpl.java | 52 ------------------- 2 files changed, 104 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java delete mode 100644 src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java b/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java deleted file mode 100644 index 32ab51c..0000000 --- a/src/main/java/ru/practicum/shareit/item/ItemRepositoryImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package ru.practicum.shareit.item; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.item.model.Item; - -import java.util.*; -import java.util.stream.Collectors; - -@Repository -public class ItemRepositoryImpl implements ItemRepository { - private final Map items = new HashMap<>(); - private long idCounter = 1; - - @Override - public Item save(Item item) { - item.setId(idCounter++); - items.put(item.getId(), item); - return item; - } - - @Override - public Item update(Item item) { - items.put(item.getId(), item); - return item; - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(items.get(id)); - } - - @Override - public List findAllByOwnerId(Long ownerId) { - return items.values().stream() - .filter(item -> item.getOwnerId().equals(ownerId)) - .collect(Collectors.toList()); - } - - @Override - public List search(String text) { - if (text == null || text.isBlank()) { - return Collections.emptyList(); - } - String lowerCaseText = text.toLowerCase(); - - return items.values().stream() - .filter(item -> Boolean.TRUE.equals(item.getAvailable())) // Безопасная проверка на true - .filter(item -> (item.getName() != null && item.getName().toLowerCase().contains(lowerCaseText)) || - (item.getDescription() != null && item.getDescription().toLowerCase().contains(lowerCaseText))) - .collect(Collectors.toList()); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java b/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java deleted file mode 100644 index bd6f995..0000000 --- a/src/main/java/ru/practicum/shareit/user/UserRepositoryImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package ru.practicum.shareit.user; - -import org.springframework.stereotype.Repository; -import ru.practicum.shareit.user.User; - -import java.util.*; - -@Repository -public class UserRepositoryImpl implements UserRepository { - private final Map users = new HashMap<>(); - private long idCounter = 1; - - @Override - public User save(User user) { - user.setId(idCounter++); - users.put(user.getId(), user); - return user; - } - - @Override - public User update(User user) { - users.put(user.getId(), user); - return user; - } - - @Override - public Optional findById(Long id) { - return Optional.ofNullable(users.get(id)); - } - - @Override - public List findAll() { - return new ArrayList<>(users.values()); - } - - @Override - public void deleteById(Long id) { - users.remove(id); - } - - @Override - public boolean existsByEmail(String email) { - return users.values().stream() - .anyMatch(user -> user.getEmail().equals(email)); - } - - @Override - public boolean existsByEmailAndNotId(String email, Long id) { - return users.values().stream() - .anyMatch(user -> user.getEmail().equals(email) && !user.getId().equals(id)); - } -} \ No newline at end of file From ba5ee5c9ba08e72ba203360f0f84bfffcdb6648c Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:05:21 +0300 Subject: [PATCH 05/18] style: final clean up of whitespaces --- .../ru/practicum/shareit/item/ItemServiceImpl.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 68f89a5..bc29ba7 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -16,7 +16,6 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.User; import ru.practicum.shareit.user.UserRepository; - import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -58,10 +57,8 @@ public ItemDto getById(Long itemId, Long userId) { Item item = itemRepository.findById(itemId) .orElseThrow(() -> new NotFoundException("Item not found")); ItemDto dto = ItemMapper.toItemDto(item); - dto.setComments(commentRepository.findAllByItemId(itemId).stream() .map(CommentMapper::toCommentDto).collect(Collectors.toList())); - if (item.getOwner().getId().equals(userId)) { setBookings(dto, bookingRepository.findAllByItemIdOrderByStartAsc(itemId)); } @@ -79,7 +76,7 @@ public List getAllByOwnerId(Long ownerId) { return dto; }).sorted(Comparator.comparing(ItemDto::getId)).collect(Collectors.toList()); } - + @Override public List search(String text) { if (text == null || text.isBlank()) { @@ -94,21 +91,17 @@ public List search(String text) { public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); - List userBookings = bookingRepository.findAllPastByBookerId(userId, LocalDateTime.now()); boolean hasFinishedBooking = userBookings.stream() .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); - if (!hasFinishedBooking) { throw new ValidationException("No finished bookings for this item"); } - Comment comment = new Comment(); comment.setText(commentDto.getText()); comment.setItem(item); comment.setAuthor(user); comment.setCreated(LocalDateTime.now()); - return CommentMapper.toCommentDto(commentRepository.save(comment)); } @@ -120,7 +113,6 @@ private void setBookings(ItemDto dto, List bookings) { Booking next = bookings.stream() .filter(b -> b.getStart().isAfter(now)) .findFirst().orElse(null); - if (last != null) { dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); } From d8224acf700cafdfb915842303ba17ba47d2a5c0 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:10:13 +0300 Subject: [PATCH 06/18] fix: resolve ApplicationContext failure and fix datasource config --- .../java/ru/practicum/shareit/ShareItApp.java | 2 +- src/main/resources/application.properties | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/src/main/java/ru/practicum/shareit/ShareItApp.java index 8bda1c7..89aaadb 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/src/main/java/ru/practicum/shareit/ShareItApp.java @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@SpringBootApplication public class ShareItApp { public static void main(String[] args) { SpringApplication.run(ShareItApp.class, args); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 065c14f..16a3bc0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,16 +1,14 @@ server.port=8080 +spring.datasource.url=jdbc:h2:mem:shareit +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 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 Postgres DB -spring.datasource.driverClassName -spring.datasource.url -spring.datasource.username -spring.datasource.password +logging.level.org.springframework.transaction=INFO \ No newline at end of file From 1564f5fca3987464fa92ab00caa921859687ec53 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:12:47 +0300 Subject: [PATCH 07/18] style: remove unused import in ShareItApp --- src/main/java/ru/practicum/shareit/ShareItApp.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/src/main/java/ru/practicum/shareit/ShareItApp.java index 89aaadb..0592a88 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/src/main/java/ru/practicum/shareit/ShareItApp.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication public class ShareItApp { From 2ebfbc379ced08f3d80c349fc1e527525ab4e575 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:20:40 +0300 Subject: [PATCH 08/18] fix: cleanup pom --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 3828561..c4efc96 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ spring-boot-starter-actuator + From a223815e99bb0a9fec5e483475d387ff597a3525 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:28:01 +0300 Subject: [PATCH 09/18] fix: add no-args constructors --- src/main/java/ru/practicum/shareit/user/dto/UserDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java index c188010..8efd577 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -7,10 +7,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; @Data @AllArgsConstructor +@NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class UserDto { Long id; From 37a854b03a77a262956accd7f737f39d7a9c6710 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:35:33 +0300 Subject: [PATCH 10/18] fix: sync repository and service method names --- .../shareit/booking/BookingRepository.java | 19 ++-- .../shareit/booking/BookingServiceImpl.java | 96 +++++-------------- .../shareit/item/ItemServiceImpl.java | 8 +- 3 files changed, 39 insertions(+), 84 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java index 1b9c6bf..58794ee 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -1,7 +1,6 @@ package ru.practicum.shareit.booking; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.Status; @@ -14,27 +13,21 @@ public interface BookingRepository extends JpaRepository { List findAllByBookerIdAndStatusOrderByStartDesc(Long bookerId, Status status); - @Query("select b from Booking b where b.booker.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") - List findAllCurrentByBookerId(Long bookerId, LocalDateTime now); + List findAllByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(Long bookerId, LocalDateTime nowStart, LocalDateTime nowEnd); - @Query("select b from Booking b where b.booker.id = ?1 and b.end < ?2 order by b.start desc") - List findAllPastByBookerId(Long bookerId, LocalDateTime now); + List findAllByBookerIdAndEndBeforeOrderByStartDesc(Long bookerId, LocalDateTime now); - @Query("select b from Booking b where b.booker.id = ?1 and b.start > ?2 order by b.start desc") - List findAllFutureByBookerId(Long bookerId, LocalDateTime now); + List findAllByBookerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime now); List findAllByItemOwnerIdOrderByStartDesc(Long ownerId); List findAllByItemOwnerIdAndStatusOrderByStartDesc(Long ownerId, Status status); - @Query("select b from Booking b where b.item.owner.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") - List findAllCurrentByOwnerId(Long ownerId, LocalDateTime now); + List findAllByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(Long ownerId, LocalDateTime nowStart, LocalDateTime nowEnd); - @Query("select b from Booking b where b.item.owner.id = ?1 and b.end < ?2 order by b.start desc") - List findAllPastByOwnerId(Long ownerId, LocalDateTime now); + List findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(Long ownerId, LocalDateTime now); - @Query("select b from Booking b where b.item.owner.id = ?1 and b.start > ?2 order by b.start desc") - List findAllFutureByOwnerId(Long ownerId, LocalDateTime now); + List findAllByItemOwnerIdAndStartAfterOrderByStartDesc(Long ownerId, LocalDateTime now); List findAllByItemIdOrderByStartAsc(Long itemId); } \ 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 index f2ee901..10086b6 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -30,19 +30,13 @@ public class BookingServiceImpl implements BookingService { @Transactional @Override public BookingResponseDto create(Long userId, BookingDto bookingDto) { - User booker = userRepository.findById(userId) - .orElseThrow(() -> new NotFoundException("Пользователь не найден")); - Item item = itemRepository.findById(bookingDto.getItemId()) - .orElseThrow(() -> new NotFoundException("Вещь не найдена")); + User booker = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); + Item item = itemRepository.findById(bookingDto.getItemId()).orElseThrow(() -> new NotFoundException("Item not found")); - if (!item.getAvailable()) { - throw new ValidationException("Вещь недоступна для бронирования"); - } - if (item.getOwner().getId().equals(userId)) { - throw new NotFoundException("Владелец не может забронировать свою вещь"); - } + if (!item.getAvailable()) throw new ValidationException("Not available"); + if (item.getOwner().getId().equals(userId)) throw new NotFoundException("Owner cannot book"); if (bookingDto.getEnd().isBefore(bookingDto.getStart()) || bookingDto.getEnd().equals(bookingDto.getStart())) { - throw new ValidationException("Некорректные даты бронирования"); + throw new ValidationException("Wrong dates"); } Booking booking = new Booking(); @@ -58,16 +52,9 @@ public BookingResponseDto create(Long userId, BookingDto bookingDto) { @Transactional @Override public BookingResponseDto approve(Long userId, Long bookingId, boolean approved) { - Booking booking = bookingRepository.findById(bookingId) - .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); - - if (!booking.getItem().getOwner().getId().equals(userId)) { - throw new ValidationException("Только владелец может подтвердить бронирование"); - } - - if (booking.getStatus() != Status.WAITING) { - throw new ValidationException("Статус уже изменен"); - } + Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException("Not found")); + if (!booking.getItem().getOwner().getId().equals(userId)) throw new ValidationException("Not an owner"); + if (booking.getStatus() != Status.WAITING) throw new ValidationException("Already changed"); booking.setStatus(approved ? Status.APPROVED : Status.REJECTED); return BookingMapper.toBookingResponse(bookingRepository.save(booking)); @@ -75,77 +62,46 @@ public BookingResponseDto approve(Long userId, Long bookingId, boolean approved) @Override public BookingResponseDto getById(Long userId, Long bookingId) { - Booking booking = bookingRepository.findById(bookingId) - .orElseThrow(() -> new NotFoundException("Бронирование не найдено")); - + Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException("Not found")); if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { - throw new NotFoundException("Нет прав для просмотра этого бронирования"); + throw new NotFoundException("No access"); } - return BookingMapper.toBookingResponse(booking); } @Override public List getAllByBooker(Long userId, String state) { - userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь не найден")); + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); LocalDateTime now = LocalDateTime.now(); List bookings; switch (state.toUpperCase()) { - case "ALL": - bookings = bookingRepository.findAllByBookerIdOrderByStartDesc(userId); - break; - case "CURRENT": - bookings = bookingRepository.findAllCurrentByBookerId(userId, now); - break; - case "PAST": - bookings = bookingRepository.findAllPastByBookerId(userId, now); - break; - case "FUTURE": - bookings = bookingRepository.findAllFutureByBookerId(userId, now); - break; - case "WAITING": - bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.WAITING); - break; - case "REJECTED": - bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); - break; - default: - throw new ValidationException("Unknown state: " + state); + case "ALL": bookings = bookingRepository.findAllByBookerIdOrderByStartDesc(userId); break; + case "CURRENT": bookings = bookingRepository.findAllByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(userId, now, now); break; + case "PAST": bookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, now); break; + case "FUTURE": bookings = bookingRepository.findAllByBookerIdAndStartAfterOrderByStartDesc(userId, now); break; + case "WAITING": bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); break; + default: throw new ValidationException("Unknown state: " + state); } - return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); } @Override public List getAllByOwner(Long userId, String state) { - userRepository.findById(userId).orElseThrow(() -> new NotFoundException("Пользователь не найден")); + userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); LocalDateTime now = LocalDateTime.now(); List bookings; switch (state.toUpperCase()) { - case "ALL": - bookings = bookingRepository.findAllByItemOwnerIdOrderByStartDesc(userId); - break; - case "CURRENT": - bookings = bookingRepository.findAllCurrentByOwnerId(userId, now); - break; - case "PAST": - bookings = bookingRepository.findAllPastByOwnerId(userId, now); - break; - case "FUTURE": - bookings = bookingRepository.findAllFutureByOwnerId(userId, now); - break; - case "WAITING": - bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.WAITING); - break; - case "REJECTED": - bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); - break; - default: - throw new ValidationException("Unknown state: " + state); + case "ALL": bookings = bookingRepository.findAllByItemOwnerIdOrderByStartDesc(userId); break; + case "CURRENT": bookings = bookingRepository.findAllByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(userId, now, now); break; + case "PAST": bookings = bookingRepository.findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(userId, now); break; + case "FUTURE": bookings = bookingRepository.findAllByItemOwnerIdAndStartAfterOrderByStartDesc(userId, now); break; + case "WAITING": bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); break; + default: throw new ValidationException("Unknown state: " + state); } - return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); } } \ 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 index bc29ba7..9979e9d 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -91,17 +91,23 @@ public List search(String text) { public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); - List userBookings = bookingRepository.findAllPastByBookerId(userId, LocalDateTime.now()); + + // Исправленный вызов метода: + List userBookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, LocalDateTime.now()); + boolean hasFinishedBooking = userBookings.stream() .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); + if (!hasFinishedBooking) { throw new ValidationException("No finished bookings for this item"); } + Comment comment = new Comment(); comment.setText(commentDto.getText()); comment.setItem(item); comment.setAuthor(user); comment.setCreated(LocalDateTime.now()); + return CommentMapper.toCommentDto(commentRepository.save(comment)); } From d00657169ad764cb4f2fee7c59d030ec5f16b3e9 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:43:12 +0300 Subject: [PATCH 11/18] fix: sync repository and service method names --- src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 9979e9d..e398c15 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -110,7 +110,7 @@ public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) return CommentMapper.toCommentDto(commentRepository.save(comment)); } - + private void setBookings(ItemDto dto, List bookings) { LocalDateTime now = LocalDateTime.now(); Booking last = bookings.stream() From b7037e1c1ba2a72a5d4d1d1a8b3e44ae71111324 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:47:42 +0300 Subject: [PATCH 12/18] style: fix trailing whitespace --- .../shareit/item/ItemServiceImpl.java | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index e398c15..26aec53 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -32,8 +32,7 @@ public class ItemServiceImpl implements ItemService { @Transactional @Override public ItemDto create(Long userId, ItemDto itemDto) { - User owner = userRepository.findById(userId) - .orElseThrow(() -> new NotFoundException("User not found")); + User owner = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = ItemMapper.toItem(itemDto, owner); return ItemMapper.toItemDto(itemRepository.save(item)); } @@ -41,11 +40,8 @@ public ItemDto create(Long userId, ItemDto itemDto) { @Transactional @Override public ItemDto update(Long userId, Long itemId, ItemDto itemDto) { - Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Item not found")); - if (!item.getOwner().getId().equals(userId)) { - throw new NotFoundException("Not an owner"); - } + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); + if (!item.getOwner().getId().equals(userId)) throw new NotFoundException("Not an owner"); if (itemDto.getName() != null) item.setName(itemDto.getName()); if (itemDto.getDescription() != null) item.setDescription(itemDto.getDescription()); if (itemDto.getAvailable() != null) item.setAvailable(itemDto.getAvailable()); @@ -54,8 +50,7 @@ public ItemDto update(Long userId, Long itemId, ItemDto itemDto) { @Override public ItemDto getById(Long itemId, Long userId) { - Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Item not found")); + Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); ItemDto dto = ItemMapper.toItemDto(item); dto.setComments(commentRepository.findAllByItemId(itemId).stream() .map(CommentMapper::toCommentDto).collect(Collectors.toList())); @@ -67,8 +62,7 @@ public ItemDto getById(Long itemId, Long userId) { @Override public List getAllByOwnerId(Long ownerId) { - List items = itemRepository.findAllByOwnerId(ownerId); - return items.stream().map(item -> { + return itemRepository.findAllByOwnerId(ownerId).stream().map(item -> { ItemDto dto = ItemMapper.toItemDto(item); dto.setComments(commentRepository.findAllByItemId(item.getId()).stream() .map(CommentMapper::toCommentDto).collect(Collectors.toList())); @@ -79,9 +73,7 @@ public List getAllByOwnerId(Long ownerId) { @Override public List search(String text) { - if (text == null || text.isBlank()) { - return Collections.emptyList(); - } + if (text == null || text.isBlank()) return Collections.emptyList(); return itemRepository.searchByText(text).stream() .map(ItemMapper::toItemDto).collect(Collectors.toList()); } @@ -91,39 +83,25 @@ public List search(String text) { public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); - - // Исправленный вызов метода: List userBookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, LocalDateTime.now()); - boolean hasFinishedBooking = userBookings.stream() .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); - - if (!hasFinishedBooking) { - throw new ValidationException("No finished bookings for this item"); - } - + if (!hasFinishedBooking) throw new ValidationException("No finished bookings"); Comment comment = new Comment(); comment.setText(commentDto.getText()); comment.setItem(item); comment.setAuthor(user); comment.setCreated(LocalDateTime.now()); - return CommentMapper.toCommentDto(commentRepository.save(comment)); } - + private void setBookings(ItemDto dto, List bookings) { LocalDateTime now = LocalDateTime.now(); - Booking last = bookings.stream() - .filter(b -> b.getStart().isBefore(now)) + Booking last = bookings.stream().filter(b -> b.getStart().isBefore(now)) .reduce((first, second) -> second).orElse(null); - Booking next = bookings.stream() - .filter(b -> b.getStart().isAfter(now)) + Booking next = bookings.stream().filter(b -> b.getStart().isAfter(now)) .findFirst().orElse(null); - if (last != null) { - dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); - } - if (next != null) { - dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); - } + if (last != null) dto.setLastBooking(new ItemDto.BookingShortDto(last.getId(), last.getBooker().getId())); + if (next != null) dto.setNextBooking(new ItemDto.BookingShortDto(next.getId(), next.getBooker().getId())); } } \ No newline at end of file From ad3cc8a88f4d56b4a6fcc6b8ccbe002788329745 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 18:54:25 +0300 Subject: [PATCH 13/18] fix: resolve maven duplicate and missing constructors --- pom.xml | 9 +-------- src/main/resources/application.properties | 8 +------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index c4efc96..f46495c 100644 --- a/pom.xml +++ b/pom.xml @@ -8,15 +8,12 @@ 3.3.2 - ru.practicum shareit 0.0.1-SNAPSHOT - 21 - org.springframework.boot @@ -55,7 +52,6 @@ spring-boot-starter-actuator - @@ -72,7 +68,6 @@ - check @@ -90,9 +85,7 @@ - - check - + check compile diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 16a3bc0..b296ff3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,8 @@ server.port=8080 - spring.datasource.url=jdbc:h2:mem:shareit spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= - spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 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 \ No newline at end of file +spring.sql.init.mode=always \ No newline at end of file From e247bef08a87a5f66de4ff19fb7e4aac3f334aa6 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 19:07:42 +0300 Subject: [PATCH 14/18] fix: align java models with sql schema and add fielddefaults --- .../practicum/shareit/booking/model/Booking.java | 9 ++++----- .../ru/practicum/shareit/item/model/Comment.java | 10 +++++----- .../ru/practicum/shareit/item/model/Item.java | 15 +++++++-------- src/main/java/ru/practicum/shareit/user/User.java | 11 +++++------ 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index 0439c60..5693ce5 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -9,13 +9,12 @@ import java.time.LocalDateTime; @Data -@AllArgsConstructor -@NoArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) @Entity @Table(name = "bookings") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class Booking { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @@ -35,6 +34,6 @@ public class Booking { User booker; @Enumerated(EnumType.STRING) - @Column(name = "status", nullable = false) + @Column(nullable = false) Status status; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java index cead782..1bb5d6c 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Comment.java +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -8,11 +8,11 @@ import java.time.LocalDateTime; @Data -@AllArgsConstructor -@NoArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) @Entity @Table(name = "comments") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,11 +22,11 @@ public class Comment { String text; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "item_id", nullable = false) + @JoinColumn(name = "item_id", nullable = false) // Маппинг на твой SQL Item item; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "author_id", nullable = false) + @JoinColumn(name = "author_id", nullable = false) // Маппинг на твой SQL User author; @Column(nullable = false) diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index cae5aef..f4638ef 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -17,28 +17,27 @@ import ru.practicum.shareit.user.User; @Data -@AllArgsConstructor -@NoArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) @Entity @Table(name = "items") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class Item { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; - @Column(name = "name", nullable = false) + @Column(nullable = false) String name; - @Column(name = "description", nullable = false) + @Column(nullable = false) String description; - @Column(name = "is_available", nullable = false) + @Column(name = "is_available", nullable = false) // Маппинг на твой SQL Boolean available; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "owner_id", nullable = false) + @JoinColumn(name = "owner_id", nullable = false) // Маппинг на твой SQL User owner; @Column(name = "request_id") diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java index 2403dd3..a976191 100644 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ b/src/main/java/ru/practicum/shareit/user/User.java @@ -13,20 +13,19 @@ import lombok.experimental.FieldDefaults; @Data -@AllArgsConstructor -@NoArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE) @Entity @Table(name = "users") +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class User { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; - @Column(name = "name", nullable = false) + @Column(nullable = false) String name; - @Column(name = "email", nullable = false, unique = true) + @Column(nullable = false, unique = true) String email; } \ No newline at end of file From 3bce5b2797de6de3300b67aeff5fc8892176ffe3 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 19:26:57 +0300 Subject: [PATCH 15/18] fix: finalize maven config and resolve startup issues for CI tests --- pom.xml | 25 ++++++++++++++++++----- src/main/resources/application.properties | 9 ++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index f46495c..585edda 100644 --- a/pom.xml +++ b/pom.xml @@ -2,56 +2,68 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.3.2 + ru.practicum shareit 0.0.1-SNAPSHOT + 21 + org.springframework.boot spring-boot-starter-web + org.springframework.boot spring-boot-starter-validation + org.springframework.boot spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-actuator + + org.postgresql postgresql runtime + com.h2database h2 runtime + org.projectlombok lombok true + org.springframework.boot spring-boot-starter-test test - - org.springframework.boot - spring-boot-starter-actuator - + @@ -68,6 +80,7 @@ + check @@ -85,7 +98,9 @@ - check + + check + compile diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b296ff3..4f2a5cc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,13 @@ server.port=8080 + spring.datasource.url=jdbc:h2:mem:shareit spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= + spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=none -spring.sql.init.mode=always \ No newline at end of file +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +management.endpoints.web.exposure.include=health +endpoint.health.show-details=always \ No newline at end of file From e68e6d4f28df2d434950d823e1e8cea8e6aabd7b Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 19:44:29 +0300 Subject: [PATCH 16/18] fix: force database schema recreation for CI tests --- .../ru/practicum/shareit/item/model/Item.java | 4 +- src/main/resources/application.properties | 9 +--- src/main/resources/schema.sql | 44 +++++++++---------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index f4638ef..38b036c 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -33,11 +33,11 @@ public class Item { @Column(nullable = false) String description; - @Column(name = "is_available", nullable = false) // Маппинг на твой SQL + @Column(name = "is_available", nullable = false) Boolean available; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "owner_id", nullable = false) // Маппинг на твой SQL + @JoinColumn(name = "owner_id", nullable = false) User owner; @Column(name = "request_id") diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4f2a5cc..a8cf059 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,13 +1,8 @@ server.port=8080 - -spring.datasource.url=jdbc:h2:mem:shareit +spring.datasource.url=jdbc:h2:mem:shareit;DB_CLOSE_DELAY=-1 spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= - spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true - -management.endpoints.web.exposure.include=health -endpoint.health.show-details=always \ No newline at end of file +spring.sql.init.mode=always \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b55fba7..2e2e066 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,41 +1,41 @@ -CREATE TABLE IF NOT EXISTS users ( - id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, +DROP TABLE IF EXISTS comments CASCADE; +DROP TABLE IF EXISTS bookings CASCADE; +DROP TABLE IF EXISTS items CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +CREATE TABLE users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, - email VARCHAR(512) NOT NULL, - CONSTRAINT pk_user PRIMARY KEY (id), - CONSTRAINT UQ_USER_EMAIL UNIQUE (email) + email VARCHAR(512) NOT NULL UNIQUE ); -CREATE TABLE IF NOT EXISTS items ( - id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, +CREATE TABLE items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, - description VARCHAR(512) NOT NULL, + description VARCHAR(1000) NOT NULL, is_available BOOLEAN NOT NULL, owner_id BIGINT NOT NULL, request_id BIGINT, - CONSTRAINT pk_item PRIMARY KEY (id), - CONSTRAINT fk_items_to_users FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE + 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, +CREATE TABLE bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 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(50) NOT NULL, - CONSTRAINT pk_booking PRIMARY KEY (id), - CONSTRAINT fk_booking_to_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, - CONSTRAINT fk_booking_to_user FOREIGN KEY (booker_id) REFERENCES users(id) ON DELETE CASCADE + status VARCHAR(20) NOT NULL, + 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, - text VARCHAR(1000) NOT NULL, +CREATE TABLE comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(2000) 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_to_item FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, - CONSTRAINT fk_comment_to_user FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE + 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 From fe5417add62a2301c3cf24c73fe9b382f2f2ca48 Mon Sep 17 00:00:00 2001 From: Darya346 Date: Thu, 26 Mar 2026 19:53:00 +0300 Subject: [PATCH 17/18] fix: make datasource config universal for CI environment --- src/main/resources/application.properties | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a8cf059..56b6039 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,14 @@ server.port=8080 -spring.datasource.url=jdbc:h2:mem:shareit;DB_CLOSE_DELAY=-1 -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:h2:mem:shareit;DB_CLOSE_DELAY=-1} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:sa} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:} + + spring.jpa.hibernate.ddl-auto=update -spring.sql.init.mode=always \ No newline at end of file +spring.jpa.show-sql=false + +spring.sql.init.mode=always + +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always \ No newline at end of file From 21912164482967975d6fe5235fa2e3cffac14ede Mon Sep 17 00:00:00 2001 From: Darya346 Date: Fri, 27 Mar 2026 12:01:45 +0300 Subject: [PATCH 18/18] refactor: shorten booking repository method names using @Query --- .../shareit/booking/BookingRepository.java | 35 ++++++++++++------- .../shareit/booking/BookingServiceImpl.java | 31 +++++++--------- .../shareit/item/ItemServiceImpl.java | 6 ++-- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java index 58794ee..bfbb225 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -1,33 +1,44 @@ package ru.practicum.shareit.booking; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.booking.model.Booking; import ru.practicum.shareit.booking.model.Status; - import java.time.LocalDateTime; import java.util.List; public interface BookingRepository extends JpaRepository { - List findAllByBookerIdOrderByStartDesc(Long bookerId); + @Query("select b from Booking b where b.booker.id = ?1 order by b.start desc") + List findUserBookings(Long bookerId); - List findAllByBookerIdAndStatusOrderByStartDesc(Long bookerId, Status status); + @Query("select b from Booking b where b.booker.id = ?1 and b.status = ?2 order by b.start desc") + List findUserStatus(Long bookerId, Status status); - List findAllByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(Long bookerId, LocalDateTime nowStart, LocalDateTime nowEnd); + @Query("select b from Booking b where b.booker.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findUserCurrent(Long bookerId, LocalDateTime now); - List findAllByBookerIdAndEndBeforeOrderByStartDesc(Long bookerId, LocalDateTime now); + @Query("select b from Booking b where b.booker.id = ?1 and b.end < ?2 order by b.start desc") + List findUserPast(Long bookerId, LocalDateTime now); - List findAllByBookerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime now); + @Query("select b from Booking b where b.booker.id = ?1 and b.start > ?2 order by b.start desc") + List findUserFuture(Long bookerId, LocalDateTime now); - List findAllByItemOwnerIdOrderByStartDesc(Long ownerId); + @Query("select b from Booking b where b.item.owner.id = ?1 order by b.start desc") + List findOwnerBookings(Long ownerId); - List findAllByItemOwnerIdAndStatusOrderByStartDesc(Long ownerId, Status status); + @Query("select b from Booking b where b.item.owner.id = ?1 and b.status = ?2 order by b.start desc") + List findOwnerStatus(Long ownerId, Status status); - List findAllByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(Long ownerId, LocalDateTime nowStart, LocalDateTime nowEnd); + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start <= ?2 and b.end >= ?2 order by b.start desc") + List findOwnerCurrent(Long ownerId, LocalDateTime now); - List findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(Long ownerId, LocalDateTime now); + @Query("select b from Booking b where b.item.owner.id = ?1 and b.end < ?2 order by b.start desc") + List findOwnerPast(Long ownerId, LocalDateTime now); - List findAllByItemOwnerIdAndStartAfterOrderByStartDesc(Long ownerId, LocalDateTime now); + @Query("select b from Booking b where b.item.owner.id = ?1 and b.start > ?2 order by b.start desc") + List findOwnerFuture(Long ownerId, LocalDateTime now); - List findAllByItemIdOrderByStartAsc(Long itemId); + @Query("select b from Booking b where b.item.id = ?1 order by b.start asc") + List findItemBookings(Long itemId); } \ 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 index 10086b6..7cf0a92 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -14,7 +14,6 @@ import ru.practicum.shareit.item.model.Item; import ru.practicum.shareit.user.User; import ru.practicum.shareit.user.UserRepository; - import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -32,20 +31,17 @@ public class BookingServiceImpl implements BookingService { public BookingResponseDto create(Long userId, BookingDto bookingDto) { User booker = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = itemRepository.findById(bookingDto.getItemId()).orElseThrow(() -> new NotFoundException("Item not found")); - if (!item.getAvailable()) throw new ValidationException("Not available"); if (item.getOwner().getId().equals(userId)) throw new NotFoundException("Owner cannot book"); if (bookingDto.getEnd().isBefore(bookingDto.getStart()) || bookingDto.getEnd().equals(bookingDto.getStart())) { throw new ValidationException("Wrong dates"); } - Booking booking = new Booking(); booking.setStart(bookingDto.getStart()); booking.setEnd(bookingDto.getEnd()); booking.setItem(item); booking.setBooker(booker); booking.setStatus(Status.WAITING); - return BookingMapper.toBookingResponse(bookingRepository.save(booking)); } @@ -55,7 +51,6 @@ public BookingResponseDto approve(Long userId, Long bookingId, boolean approved) Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new NotFoundException("Not found")); if (!booking.getItem().getOwner().getId().equals(userId)) throw new ValidationException("Not an owner"); if (booking.getStatus() != Status.WAITING) throw new ValidationException("Already changed"); - booking.setStatus(approved ? Status.APPROVED : Status.REJECTED); return BookingMapper.toBookingResponse(bookingRepository.save(booking)); } @@ -74,14 +69,13 @@ public List getAllByBooker(Long userId, String state) { userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); LocalDateTime now = LocalDateTime.now(); List bookings; - switch (state.toUpperCase()) { - case "ALL": bookings = bookingRepository.findAllByBookerIdOrderByStartDesc(userId); break; - case "CURRENT": bookings = bookingRepository.findAllByBookerIdAndStartBeforeAndEndAfterOrderByStartDesc(userId, now, now); break; - case "PAST": bookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, now); break; - case "FUTURE": bookings = bookingRepository.findAllByBookerIdAndStartAfterOrderByStartDesc(userId, now); break; - case "WAITING": bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.WAITING); break; - case "REJECTED": bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); break; + case "ALL": bookings = bookingRepository.findUserBookings(userId); break; + case "CURRENT": bookings = bookingRepository.findUserCurrent(userId, now); break; + case "PAST": bookings = bookingRepository.findUserPast(userId, now); break; + case "FUTURE": bookings = bookingRepository.findUserFuture(userId, now); break; + case "WAITING": bookings = bookingRepository.findUserStatus(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findUserStatus(userId, Status.REJECTED); break; default: throw new ValidationException("Unknown state: " + state); } return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); @@ -92,14 +86,13 @@ public List getAllByOwner(Long userId, String state) { userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); LocalDateTime now = LocalDateTime.now(); List bookings; - switch (state.toUpperCase()) { - case "ALL": bookings = bookingRepository.findAllByItemOwnerIdOrderByStartDesc(userId); break; - case "CURRENT": bookings = bookingRepository.findAllByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc(userId, now, now); break; - case "PAST": bookings = bookingRepository.findAllByItemOwnerIdAndEndBeforeOrderByStartDesc(userId, now); break; - case "FUTURE": bookings = bookingRepository.findAllByItemOwnerIdAndStartAfterOrderByStartDesc(userId, now); break; - case "WAITING": bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.WAITING); break; - case "REJECTED": bookings = bookingRepository.findAllByItemOwnerIdAndStatusOrderByStartDesc(userId, Status.REJECTED); break; + case "ALL": bookings = bookingRepository.findOwnerBookings(userId); break; + case "CURRENT": bookings = bookingRepository.findOwnerCurrent(userId, now); break; + case "PAST": bookings = bookingRepository.findOwnerPast(userId, now); break; + case "FUTURE": bookings = bookingRepository.findOwnerFuture(userId, now); break; + case "WAITING": bookings = bookingRepository.findOwnerStatus(userId, Status.WAITING); break; + case "REJECTED": bookings = bookingRepository.findOwnerStatus(userId, Status.REJECTED); break; default: throw new ValidationException("Unknown state: " + state); } return bookings.stream().map(BookingMapper::toBookingResponse).collect(Collectors.toList()); diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 26aec53..582c770 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -55,7 +55,7 @@ public ItemDto getById(Long itemId, Long userId) { dto.setComments(commentRepository.findAllByItemId(itemId).stream() .map(CommentMapper::toCommentDto).collect(Collectors.toList())); if (item.getOwner().getId().equals(userId)) { - setBookings(dto, bookingRepository.findAllByItemIdOrderByStartAsc(itemId)); + setBookings(dto, bookingRepository.findItemBookings(itemId)); } return dto; } @@ -66,7 +66,7 @@ public List getAllByOwnerId(Long ownerId) { ItemDto dto = ItemMapper.toItemDto(item); dto.setComments(commentRepository.findAllByItemId(item.getId()).stream() .map(CommentMapper::toCommentDto).collect(Collectors.toList())); - setBookings(dto, bookingRepository.findAllByItemIdOrderByStartAsc(item.getId())); + setBookings(dto, bookingRepository.findItemBookings(item.getId())); return dto; }).sorted(Comparator.comparing(ItemDto::getId)).collect(Collectors.toList()); } @@ -83,7 +83,7 @@ public List search(String text) { public CommentDto createComment(Long userId, Long itemId, CommentDto commentDto) { User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException("User not found")); Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException("Item not found")); - List userBookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, LocalDateTime.now()); + List userBookings = bookingRepository.findUserPast(userId, LocalDateTime.now()); boolean hasFinishedBooking = userBookings.stream() .anyMatch(b -> b.getItem().getId().equals(itemId) && b.getStatus() == Status.APPROVED); if (!hasFinishedBooking) throw new ValidationException("No finished bookings");