diff --git a/backend/src/main/java/edu/zut/bookrider/model/enums/RentalStatus.java b/backend/src/main/java/edu/zut/bookrider/model/enums/RentalStatus.java index 54b8f72a..ebec17f3 100644 --- a/backend/src/main/java/edu/zut/bookrider/model/enums/RentalStatus.java +++ b/backend/src/main/java/edu/zut/bookrider/model/enums/RentalStatus.java @@ -3,6 +3,5 @@ public enum RentalStatus { RENTED, RETURNED, - RETURN_IN_PROGRESS, PARTIALLY_RETURNED } diff --git a/backend/src/main/java/edu/zut/bookrider/repository/RentalReturnItemRepository.java b/backend/src/main/java/edu/zut/bookrider/repository/RentalReturnItemRepository.java index 40a85179..5ae2b85c 100644 --- a/backend/src/main/java/edu/zut/bookrider/repository/RentalReturnItemRepository.java +++ b/backend/src/main/java/edu/zut/bookrider/repository/RentalReturnItemRepository.java @@ -1,9 +1,25 @@ package edu.zut.bookrider.repository; -import edu.zut.bookrider.model.Rental; import edu.zut.bookrider.model.RentalReturnItem; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface RentalReturnItemRepository extends JpaRepository { - boolean existsByRental(Rental rental); + @Query(""" + SELECT COALESCE(SUM(rri.returnedQuantity), 0) + FROM RentalReturnItem rri + WHERE rri.rental.id = :rentalId + """) + int sumReturnedQuantityByRentalId(@Param("rentalId") Integer rentalId); + + @Query(""" + SELECT rri.rental.id, SUM(rri.returnedQuantity) + FROM RentalReturnItem rri + WHERE rri.rental.id IN :rentalIds + GROUP BY rri.rental.id + """) + List sumReturnedQuantitiesByRentalIds(@Param("rentalIds") List rentalIds); } diff --git a/backend/src/main/java/edu/zut/bookrider/service/RentalReturnItemService.java b/backend/src/main/java/edu/zut/bookrider/service/RentalReturnItemService.java index 03aa59df..7ca65a65 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/RentalReturnItemService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/RentalReturnItemService.java @@ -1,6 +1,7 @@ package edu.zut.bookrider.service; import edu.zut.bookrider.dto.RentalWithQuantityDTO; +import edu.zut.bookrider.exception.RentalAlreadyReturnedException; import edu.zut.bookrider.model.Rental; import edu.zut.bookrider.model.RentalReturn; import edu.zut.bookrider.model.RentalReturnItem; @@ -8,7 +9,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @RequiredArgsConstructor @@ -18,20 +21,45 @@ public class RentalReturnItemService { private final RentalReturnItemRepository rentalReturnItemRepository; public List createRentalReturnItems(RentalReturn rentalReturn, List rentals) { - List returnItems = rentals.stream().map(rentalDto -> { + List rentalIds = rentals.stream() + .map(r -> r.getRental().getId()) + .toList(); + + List results = rentalReturnItemRepository.sumReturnedQuantitiesByRentalIds(rentalIds); + + Map returnedQuantities = results.stream() + .collect(Collectors.toMap( + row -> (Integer) row[0], + row -> ((Number) row[1]).intValue() + )); + + List returnItems = new ArrayList<>(); + + for (RentalWithQuantityDTO dto : rentals) { + Rental rental = dto.getRental(); + int alreadyReturned = returnedQuantities.getOrDefault(rental.getId(), 0); + int remaining = rental.getQuantity() - alreadyReturned; + int toReturn = dto.getQuantityToReturn(); + + if (toReturn < 1 || toReturn > remaining) { + throw new RentalAlreadyReturnedException("Invalid return quantity for rental ID " + rental.getId() + + ". Requested: " + toReturn + ", Available to return: " + remaining); + } + RentalReturnItem returnItem = new RentalReturnItem(); returnItem.setRentalReturn(rentalReturn); - returnItem.setRental(rentalDto.getRental()); - returnItem.setBook(rentalDto.getRental().getBook()); - returnItem.setReturnedQuantity(rentalDto.getQuantityToReturn()); - return returnItem; - }).collect(Collectors.toList()); + returnItem.setRental(rental); + returnItem.setBook(rental.getBook()); + returnItem.setReturnedQuantity(toReturn); + + returnItems.add(returnItem); + } rentalReturnItemRepository.saveAll(returnItems); return returnItems; } - public boolean isAlreadyReturned(Rental rental) { - return rentalReturnItemRepository.existsByRental(rental); + int sumReturnedQuantityByRentalId(Integer rentalId) { + return rentalReturnItemRepository.sumReturnedQuantityByRentalId(rentalId); } } diff --git a/backend/src/main/java/edu/zut/bookrider/service/RentalReturnService.java b/backend/src/main/java/edu/zut/bookrider/service/RentalReturnService.java index ff6dc039..60daf548 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/RentalReturnService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/RentalReturnService.java @@ -210,12 +210,9 @@ public void checkRentals(List rentalReturnRe Integer rentalId = requestDTO.getRentalId(); Rental rental = rentalService.getRentalById(rentalId); - boolean isAlreadyReturned = rentalReturnItemService.isAlreadyReturned(rental); - if (isAlreadyReturned) { + if (rental.getStatus() == RentalStatus.RETURNED) { throw new RentalAlreadyReturnedException("Rental " + rental.getId() + " has already been returned."); } - - rentalService.updateRentalStatus(rentalId, RentalStatus.RETURN_IN_PROGRESS); } } @@ -229,7 +226,7 @@ public RentalReturn createRentalReturnForOrder(Order returnOrder, List rentalReturnItems = rentalReturnItemService.createRentalReturnItems(savedRentalReturn, rentals); - savedRentalReturn.setRentalReturnItems(rentalReturnItems); + savedRentalReturn.getRentalReturnItems().addAll(rentalReturnItems); return rentalReturnRepository.save(rentalReturn); } @@ -262,7 +259,7 @@ public List createInPersonRentalReturn(InPersonRentalReturnRequ RentalReturn savedRentalReturn = rentalReturnRepository.save(rentalReturn); List rentalReturnItems = rentalReturnItemService.createRentalReturnItems(savedRentalReturn, rentalsForLibrary); - savedRentalReturn.setRentalReturnItems(rentalReturnItems); + savedRentalReturn.getRentalReturnItems().addAll(rentalReturnItems); BigDecimal totalLateFees = BigDecimal.ZERO; for (RentalWithQuantityDTO rentalWithQuantityDTO : rentalsForLibrary) { diff --git a/backend/src/main/java/edu/zut/bookrider/service/RentalService.java b/backend/src/main/java/edu/zut/bookrider/service/RentalService.java index f99724c5..17121bd3 100644 --- a/backend/src/main/java/edu/zut/bookrider/service/RentalService.java +++ b/backend/src/main/java/edu/zut/bookrider/service/RentalService.java @@ -32,6 +32,7 @@ public class RentalService { private final RentalRepository rentalRepository; private final RentalMapper rentalMapper; + private final RentalReturnItemService rentalReturnItemService; @Transactional public void createRental(OrderItem orderItem) { @@ -100,9 +101,16 @@ public PageResponseDTO getUserRentals(Pageable pageable) { User user = userService.getUser(); Page rentals = rentalRepository.findAllByUser(user, pageable); - List rentalDTOs = rentals.getContent() - .stream() - .map(rentalMapper::map) + List rentalDTOs = rentals.getContent().stream() + .map(rental -> { + int remainingQuantity = rental.getQuantity(); + if (rental.getStatus() == RentalStatus.PARTIALLY_RETURNED) { + int returned = rentalReturnItemService.sumReturnedQuantityByRentalId(rental.getId()); + remainingQuantity -= returned; + } + rental.setQuantity(remainingQuantity); + return rentalMapper.map(rental); + }) .toList(); return new PageResponseDTO<>( @@ -113,11 +121,4 @@ public PageResponseDTO getUserRentals(Pageable pageable) { rentals.getTotalPages() ); } - - public Rental updateRentalStatus(Integer rentalId, RentalStatus status) { - Rental rental = getRentalById(rentalId); - rental.setStatus(status); - - return rentalRepository.save(rental); - } } diff --git a/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalControllerIT.java b/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalControllerIT.java index 6a6f0b64..cf59de21 100644 --- a/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalControllerIT.java +++ b/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalControllerIT.java @@ -3,6 +3,7 @@ import edu.zut.bookrider.model.*; import edu.zut.bookrider.model.enums.OrderStatus; import edu.zut.bookrider.model.enums.PaymentStatus; +import edu.zut.bookrider.model.enums.RentalReturnStatus; import edu.zut.bookrider.model.enums.RentalStatus; import edu.zut.bookrider.repository.*; import edu.zut.bookrider.service.UserIdGeneratorService; @@ -57,6 +58,8 @@ public class RentalControllerIT { private RoleRepository roleRepository; @Autowired private OrderRepository orderRepository; + @Autowired + private RentalReturnRepository rentalReturnRepository; private User user; private User driver; @@ -203,4 +206,45 @@ void whenUserRequestsRentalsWithDifferentStatuses_thenReturnRentalsPage() throws .andExpect(status().isOk()) .andExpect(jsonPath("$.totalElements").value(2)); } + + @Test + @WithMockUser(username = "testuser@rcit.com", roles = {"user"}) + void whenUserRequestsRentalsWithPartialReturns_thenReturnRentalsPageWithCorrectQuantities() throws Exception { + + Author author = createAuthor("Author"); + Language language = createLanguage("language"); + + Book book1 = createBook("Book1", "113109022025", language, author); + Book book2 = createBook("Book2", "113209022025", language, author); + + Order order1 = createOrder(user, driver, library1, address, book1, 2, OrderStatus.PENDING, false); + Order order2 = createOrder(user, driver, library1, address, book2, 1, OrderStatus.PENDING, false); + + createRental(order1, 2, RentalStatus.RENTED); + Rental rental1 = createRental(order2, 3, RentalStatus.PARTIALLY_RETURNED); + + Order returnOrder = createOrder(user, driver, library1, address, book2, 2, OrderStatus.AWAITING_LIBRARY_CONFIRMATION, true); + + RentalReturn rentalReturn = new RentalReturn(); + rentalReturn.setReturnOrder(returnOrder); + rentalReturn.setStatus(RentalReturnStatus.IN_PROGRESS); + + RentalReturnItem rentalReturnItem = new RentalReturnItem(); + rentalReturnItem.setRentalReturn(rentalReturn); + rentalReturnItem.setRental(rental1); + rentalReturnItem.setBook(rental1.getBook()); + rentalReturnItem.setReturnedQuantity(2); + + List rentalReturnItems = new ArrayList<>(); + rentalReturnItems.add(rentalReturnItem); + rentalReturn.setRentalReturnItems(rentalReturnItems); + + rentalReturn = rentalReturnRepository.save(rentalReturn); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/rentals") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.content[1].quantity").value(1)); + } } diff --git a/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalReturnControllerIT.java b/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalReturnControllerIT.java index 16e771aa..9eacec3d 100644 --- a/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalReturnControllerIT.java +++ b/backend/src/test/java/edu/zut/bookrider/integration/controller/RentalReturnControllerIT.java @@ -167,6 +167,23 @@ private Rental createRental(Order order, int quantity, boolean addDays) { return rentalRepository.save(rental); } + private GeneralRentalReturnRequestDTO createSingleRentalReturnRequest(Address address, Rental rental, int quantityToReturn) { + RentalReturnWithQuantityRequestDTO rentalReturnRequestDTO = new RentalReturnWithQuantityRequestDTO(); + rentalReturnRequestDTO.setRentalId(rental.getId()); + rentalReturnRequestDTO.setQuantityToReturn(quantityToReturn); + + CreateAddressDTO createAddressDTO = new CreateAddressDTO(); + createAddressDTO.setStreet(address.getStreet()); + createAddressDTO.setCity(address.getCity()); + createAddressDTO.setPostalCode(address.getPostalCode()); + + GeneralRentalReturnRequestDTO request = new GeneralRentalReturnRequestDTO(); + request.setCreateAddressDTO(createAddressDTO); + request.setRentalReturnRequests(List.of(rentalReturnRequestDTO)); + + return request; + } + @BeforeEach void setUp() { user = createUser("testuser@rrcit.com", "user", 9999); @@ -998,6 +1015,53 @@ void whenUserRequestsReturnOrderForAlreadyReturnedReturn_thenReturnConflict() th .andExpect(status().isConflict()); } + @Test + @WithMockUser(username = "testuser@rrcit.com:user", roles = {"user"}) + void whenUserRequestsReturnOrderPartiallyReturnedReturn_thenReturnOk() throws Exception { + + Author author = createAuthor("Author"); + Language language = createLanguage("language"); + + Book book = createBook("Book1", "113109022025", language, author); + + Order deliveryOrder = createOrder(user, driver, library1, address, book, 3, OrderStatus.DELIVERED, false); + + Rental rental = new Rental(); + rental.setUser(user); + rental.setBook(deliveryOrder.getOrderItems().get(0).getBook()); + rental.setLibrary(deliveryOrder.getLibrary()); + rental.setOrder(deliveryOrder); + rental.setQuantity(3); + rental.setReturnDeadline(LocalDateTime.now().plusDays(30)); + rental.setStatus(RentalStatus.RENTED); + rental = rentalRepository.save(rental); + + Order returnOrder = createOrder(user, driver, library1, address, book, 1, OrderStatus.DRIVER_ACCEPTED, true); + + RentalReturn rentalReturn = new RentalReturn(); + rentalReturn.setReturnOrder(returnOrder); + rentalReturn.setStatus(RentalReturnStatus.IN_PROGRESS); + + RentalReturnItem rentalReturnItem = new RentalReturnItem(); + rentalReturnItem.setRentalReturn(rentalReturn); + rentalReturnItem.setRental(rental); + rentalReturnItem.setBook(rental.getBook()); + rentalReturnItem.setReturnedQuantity(1); + + List rentalReturnItems = new ArrayList<>(); + rentalReturnItems.add(rentalReturnItem); + rentalReturn.setRentalReturnItems(rentalReturnItems); + + rentalReturnRepository.save(rentalReturn); + + GeneralRentalReturnRequestDTO requestDTO = createSingleRentalReturnRequest(address, rental, 2); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/rental-returns") + .content(new ObjectMapper().writeValueAsString(requestDTO)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + @Test @WithMockUser(username = "librarianRrcit:1", roles = {"librarian"}) void whenLibrarianRequestsDeliveryReturnCompletionFromWrongLibrary_thenReturnConflict() throws Exception {