diff --git a/src/main/java/com/almang/inventory/global/api/SuccessMessage.java b/src/main/java/com/almang/inventory/global/api/SuccessMessage.java index 68a13fb0..5bc1d52a 100644 --- a/src/main/java/com/almang/inventory/global/api/SuccessMessage.java +++ b/src/main/java/com/almang/inventory/global/api/SuccessMessage.java @@ -55,6 +55,7 @@ public enum SuccessMessage { UPDATE_RECEIPT_SUCCESS("입고 수정 성공"), DELETE_RECEIPT_SUCCESS("입고 삭제 성공"), GET_RECEIPT_ITEM_SUCCESS("입고 아이템 조회 성공"), + UPDATE_RECEIPT_ITEM_SUCCESS("입고 아이템 수정 성공"), ; private final String message; diff --git a/src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java b/src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java index 7abf85b3..cb279f1b 100644 --- a/src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java +++ b/src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java @@ -5,6 +5,7 @@ import com.almang.inventory.global.api.SuccessMessage; import com.almang.inventory.global.security.principal.CustomUserPrincipal; import com.almang.inventory.receipt.domain.ReceiptStatus; +import com.almang.inventory.receipt.dto.request.UpdateReceiptItemRequest; import com.almang.inventory.receipt.dto.request.UpdateReceiptRequest; import com.almang.inventory.receipt.dto.response.DeleteReceiptResponse; import com.almang.inventory.receipt.dto.response.ReceiptItemResponse; @@ -149,4 +150,20 @@ public ResponseEntity> getReceiptItem( ApiResponse.success(SuccessMessage.GET_RECEIPT_ITEM_SUCCESS.getMessage(), response) ); } + + @PatchMapping("/receipt/{receiptItemId}") + @Operation(summary = "입고 아이템 수정", description = "입고 아이템을 수정합니다.") + public ResponseEntity> updateReceiptItem( + @PathVariable Long receiptItemId, + @Valid @RequestBody UpdateReceiptItemRequest request, + @AuthenticationPrincipal CustomUserPrincipal userPrincipal + ) { + Long userId = userPrincipal.getId(); + log.info("[ReceiptController] 입고 아이템 수정 요청 - userId: {}, receiptItemId: {}", userId, receiptItemId); + ReceiptItemResponse response = receiptService.updateReceiptItem(receiptItemId, request, userId); + + return ResponseEntity.ok( + ApiResponse.success(SuccessMessage.UPDATE_RECEIPT_ITEM_SUCCESS.getMessage(), response) + ); + } } diff --git a/src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java b/src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java index 414a7804..72054c0a 100644 --- a/src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java +++ b/src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java @@ -11,6 +11,7 @@ public record ReceiptItemResponse( BigDecimal measuredWeight, BigDecimal expectedQuantity, Integer actualQuantity, + Integer unitPrice, Integer amount, BigDecimal errorRate, String note @@ -24,6 +25,7 @@ public static ReceiptItemResponse from(ReceiptItem receiptItem) { receiptItem.getMeasuredWeight(), receiptItem.getExpectedQuantity(), receiptItem.getActualQuantity(), + receiptItem.getUnitPrice(), receiptItem.getAmount(), receiptItem.getErrorRate(), receiptItem.getNote() diff --git a/src/main/java/com/almang/inventory/receipt/service/ReceiptService.java b/src/main/java/com/almang/inventory/receipt/service/ReceiptService.java index eaff04e1..3a2099e1 100644 --- a/src/main/java/com/almang/inventory/receipt/service/ReceiptService.java +++ b/src/main/java/com/almang/inventory/receipt/service/ReceiptService.java @@ -157,6 +157,26 @@ public ReceiptItemResponse getReceiptItem(Long receiptItemId, Long userId) { return ReceiptItemResponse.from(receiptItem); } + @Transactional + public ReceiptItemResponse updateReceiptItem(Long receiptItemId, UpdateReceiptItemRequest request, Long userId) { + User user = findUserById(userId); + Store store = user.getStore(); + + log.info("[ReceiptService] 입고 아이템 수정 요청 - userId: {}, storeId: {}", userId, store.getId()); + Receipt receipt = findReceiptByIdAndValidateAccess(request.receiptId(), store); + ReceiptItem receiptItem = findReceiptItemByIdAndValidateAccess(receiptItemId, receipt); + + receiptItem.update( + request.boxCount(), request.measuredWeight(), + request.expectedQuantity(), request.actualQuantity(), + request.unitPrice(), request.note() + ); + receipt.updateTotalBoxCount(calculateTotalBoxCount(receipt.getItems())); + + log.info("[ReceiptService] 입고 아이템 수정 성공 - receiptItemId: {}", receiptItem.getId()); + return ReceiptItemResponse.from(receiptItem); + } + private List createReceiptItemsFromOrder(Order order) { List items = new ArrayList<>(); diff --git a/src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java b/src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java index 864c1a79..34ca3624 100644 --- a/src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java +++ b/src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java @@ -63,7 +63,8 @@ private UsernamePasswordAuthenticationToken auth() { // given Long orderId = 100L; - ReceiptItemResponse item = new ReceiptItemResponse( + ReceiptItemResponse item = + new ReceiptItemResponse( 1000L, 1L, 10L, @@ -73,6 +74,7 @@ private UsernamePasswordAuthenticationToken auth() { null, 5000, null, + null, "비고입니다." ); @@ -104,8 +106,7 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.data.status").value(ReceiptStatus.PENDING.name())) .andExpect(jsonPath("$.data.activated").value(true)) .andExpect(jsonPath("$.data.receiptItems[0].receiptItemId").value(1000L)) - .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)) - .andExpect(jsonPath("$.data.receiptItems[0].amount").value(5000)); + .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)); } @Test @@ -195,6 +196,7 @@ private UsernamePasswordAuthenticationToken auth() { null, 5000, null, + null, "비고입니다." ); @@ -226,8 +228,7 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.data.status").value(ReceiptStatus.PENDING.name())) .andExpect(jsonPath("$.data.activated").value(true)) .andExpect(jsonPath("$.data.receiptItems[0].receiptItemId").value(1000L)) - .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)) - .andExpect(jsonPath("$.data.receiptItems[0].amount").value(5000)); + .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)); } @Test @@ -317,6 +318,7 @@ private UsernamePasswordAuthenticationToken auth() { null, 5000, null, + null, "비고입니다." ); @@ -348,8 +350,7 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.data.status").value(ReceiptStatus.PENDING.name())) .andExpect(jsonPath("$.data.activated").value(true)) .andExpect(jsonPath("$.data.receiptItems[0].receiptItemId").value(1000L)) - .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)) - .andExpect(jsonPath("$.data.receiptItems[0].amount").value(5000)); + .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)); } @Test @@ -414,7 +415,7 @@ private UsernamePasswordAuthenticationToken auth() { null, null, BigDecimal.valueOf(5), null, 5000, null, - "비고1" + null, "비고1" ); ReceiptItemResponse item2 = new ReceiptItemResponse( @@ -422,7 +423,7 @@ private UsernamePasswordAuthenticationToken auth() { null, null, BigDecimal.valueOf(3), null, 3000, null, - "비고2" + null, "비고2" ); ReceiptResponse r1 = new ReceiptResponse( @@ -539,6 +540,7 @@ private UsernamePasswordAuthenticationToken auth() { BigDecimal.valueOf(5), 10, 11000, + 110000, BigDecimal.valueOf(1.000), "수정 비고입니다." ); @@ -577,7 +579,7 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.data.receiptItems[0].receiptItemId").value(1000L)) .andExpect(jsonPath("$.data.receiptItems[0].productId").value(10L)) .andExpect(jsonPath("$.data.receiptItems[0].boxCount").value(2)) - .andExpect(jsonPath("$.data.receiptItems[0].amount").value(11000)); + .andExpect(jsonPath("$.data.receiptItems[0].amount").value(110000)); } @Test @@ -793,6 +795,7 @@ private UsernamePasswordAuthenticationToken auth() { BigDecimal.valueOf(5), 10, 5000, + 50000, BigDecimal.valueOf(1.000), "비고입니다." ); @@ -811,7 +814,7 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.data.receiptId").value(1L)) .andExpect(jsonPath("$.data.productId").value(10L)) .andExpect(jsonPath("$.data.boxCount").value(2)) - .andExpect(jsonPath("$.data.amount").value(5000)); + .andExpect(jsonPath("$.data.amount").value(50000)); } @Test @@ -867,4 +870,217 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.message").value(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED.getMessage())) .andExpect(jsonPath("$.data").doesNotExist()); } + + @Test + void 입고_아이템_수정에_성공한다() throws Exception { + // given + Long receiptItemId = 1000L; + Long receiptId = 1L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + receiptId, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + ReceiptItemResponse response = new ReceiptItemResponse( + receiptItemId, + receiptId, + 10L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + 15000, + BigDecimal.valueOf(0.0), + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenReturn(response); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message").value(SuccessMessage.UPDATE_RECEIPT_ITEM_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.receiptItemId").value(receiptItemId)) + .andExpect(jsonPath("$.data.receiptId").value(receiptId)) + .andExpect(jsonPath("$.data.productId").value(10L)) + .andExpect(jsonPath("$.data.boxCount").value(2)) + .andExpect(jsonPath("$.data.actualQuantity").value(10)) + .andExpect(jsonPath("$.data.unitPrice").value(1500)) + .andExpect(jsonPath("$.data.amount").value(15000)); + } + + @Test + void 입고_아이템_수정시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long receiptItemId = 1000L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + 1L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 입고_아이템_수정시_입고가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long receiptItemId = 1000L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + 9999L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.RECEIPT_NOT_FOUND)); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.RECEIPT_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.RECEIPT_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 입고_아이템_수정시_입고_접근_권한이_없으면_예외가_발생한다() throws Exception { + // given + Long receiptItemId = 1000L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + 1L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.RECEIPT_ACCESS_DENIED)); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value(ErrorCode.RECEIPT_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.RECEIPT_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 입고_아이템_수정시_아이템이_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long receiptItemId = 9999L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + 1L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.RECEIPT_ITEM_NOT_FOUND)); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.RECEIPT_ITEM_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.RECEIPT_ITEM_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 입고_아이템_수정시_다른_상점의_아이템이면_접근_거부_예외가_발생한다() throws Exception { + // given + Long receiptItemId = 1000L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + receiptItemId, + 1L, + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정 비고입니다." + ); + + when(receiptService.updateReceiptItem(anyLong(), any(UpdateReceiptItemRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED)); + + String body = objectMapper.writeValueAsString(request); + + // when & then + mockMvc.perform(patch("/api/v1/receipt/receipt/{receiptItemId}", receiptItemId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } } diff --git a/src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java b/src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java index e14f1805..da912ce8 100644 --- a/src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java +++ b/src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java @@ -20,6 +20,7 @@ import com.almang.inventory.receipt.dto.request.UpdateReceiptRequest; import com.almang.inventory.receipt.dto.response.ReceiptItemResponse; import com.almang.inventory.receipt.dto.response.ReceiptResponse; +import com.almang.inventory.receipt.repository.ReceiptItemRepository; import com.almang.inventory.receipt.repository.ReceiptRepository; import com.almang.inventory.store.domain.Store; import com.almang.inventory.store.repository.StoreRepository; @@ -43,14 +44,15 @@ class ReceiptServiceTest { @Autowired private ReceiptService receiptService; - @Autowired private ReceiptRepository receiptRepository; + @Autowired private ReceiptItemRepository receiptItemRepository; @Autowired private OrderRepository orderRepository; @Autowired private UserRepository userRepository; @Autowired private StoreRepository storeRepository; @Autowired private VendorRepository vendorRepository; @Autowired private ProductRepository productRepository; + private Store newStore(String name) { return storeRepository.save( Store.builder() @@ -1087,4 +1089,193 @@ private Order newOrderWithItems(Store store, Vendor vendor) { .isInstanceOf(BaseException.class) .hasMessageContaining(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED.getMessage()); } + + @Test + void 입고_아이템_수정에_성공한다() { + // given + Store store = newStore("아이템수정상점"); + User user = newUser(store, "modifyItemUser"); + Vendor vendor = newVendor(store, "발주처"); + + Order order = newOrderWithItems(store, vendor); + + Receipt receipt = Receipt.builder() + .store(store) + .order(order) + .receiptDate(LocalDate.now()) + .totalBoxCount(0) + .status(ReceiptStatus.PENDING) + .activated(true) + .build(); + + for (OrderItem orderItem : order.getItems()) { + ReceiptItem item = ReceiptItem.builder() + .product(orderItem.getProduct()) + .expectedQuantity(BigDecimal.valueOf(orderItem.getQuantity())) + .amount(orderItem.getAmount()) + .unitPrice(orderItem.getUnitPrice()) + .build(); + receipt.addItem(item); + } + + Receipt saved = receiptRepository.save(receipt); + ReceiptItem targetItem = saved.getItems().get(0); + + UpdateReceiptItemRequest req = new UpdateReceiptItemRequest( + targetItem.getId(), + saved.getId(), + 2, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(5), + 10, + 1500, + "수정된 비고" + ); + + // when + ReceiptItemResponse response = receiptService.updateReceiptItem( + targetItem.getId(), req, user.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.boxCount()).isEqualTo(2); + assertThat(response.actualQuantity()).isEqualTo(10); + assertThat(response.unitPrice()).isEqualTo(1500); + assertThat(response.amount()).isEqualTo(10 * 1500); + + ReceiptItem updated = receiptItemRepository.findById(targetItem.getId()).orElseThrow(); + assertThat(updated.getBoxCount()).isEqualTo(2); + assertThat(updated.getActualQuantity()).isEqualTo(10); + assertThat(updated.getUnitPrice()).isEqualTo(1500); + assertThat(updated.getErrorRate()).isNotNull(); + } + + @Test + void 입고_아이템_수정시_사용자가_존재하지_않으면_예외가_발생한다() { + // given + Long notExistUserId = 9999L; + Long anyItemId = 1L; + + UpdateReceiptItemRequest request = new UpdateReceiptItemRequest( + anyItemId, 1L, 1, null, null, 5, 1000, "비고" + ); + + // when & then + assertThatThrownBy(() -> receiptService.updateReceiptItem(anyItemId, request, notExistUserId)) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 입고_아이템_수정시_아이템이_해당_입고에_속하지_않으면_접근_거부_예외가_발생한다() { + // given + Store store = newStore("아이템불일치상점"); + User user = newUser(store, "wrongItemUser"); + Vendor vendor = newVendor(store, "발주처"); + + Order order1 = newOrderWithItems(store, vendor); + Order order2 = newOrderWithItems(store, vendor); + + Receipt receipt1 = Receipt.builder() + .store(store) + .order(order1) + .receiptDate(LocalDate.now()) + .totalBoxCount(0) + .status(ReceiptStatus.PENDING) + .activated(true) + .build(); + + receipt1.addItem( + ReceiptItem.builder() + .product(order1.getItems().get(0).getProduct()) + .expectedQuantity(BigDecimal.valueOf(5)) + .amount(5000) + .unitPrice(1000) + .build() + ); + Receipt savedReceipt1 = receiptRepository.save(receipt1); + + Receipt receipt2 = Receipt.builder() + .store(store) + .order(order2) + .receiptDate(LocalDate.now()) + .totalBoxCount(0) + .status(ReceiptStatus.PENDING) + .activated(true) + .build(); + receipt2.addItem( + ReceiptItem.builder() + .product(order2.getItems().get(0).getProduct()) + .expectedQuantity(BigDecimal.valueOf(3)) + .amount(6000) + .unitPrice(2000) + .build() + ); + Receipt savedReceipt2 = receiptRepository.save(receipt2); + ReceiptItem otherReceiptItem = savedReceipt2.getItems().get(0); + + UpdateReceiptItemRequest wrongRequest = new UpdateReceiptItemRequest( + otherReceiptItem.getId(), + savedReceipt1.getId(), + 1, + null, + null, + 10, + 2000, + "잘못 수정" + ); + + // when & then + assertThatThrownBy(() -> + receiptService.updateReceiptItem(otherReceiptItem.getId(), wrongRequest, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.RECEIPT_ITEM_ACCESS_DENIED.getMessage()); + } + + @Test + void 입고_아이템_수정시_입고ID가_다르면_예외가_발생한다() { + // given + Store store = newStore("입고Id불일치상점"); + User user = newUser(store, "receiptMismatchUser"); + Vendor vendor = newVendor(store, "발주처"); + + Order order = newOrderWithItems(store, vendor); + + Receipt receipt = Receipt.builder() + .store(store) + .order(order) + .receiptDate(LocalDate.now()) + .totalBoxCount(0) + .status(ReceiptStatus.PENDING) + .activated(true) + .build(); + receipt.addItem( + ReceiptItem.builder() + .product(order.getItems().get(0).getProduct()) + .expectedQuantity(BigDecimal.valueOf(5)) + .amount(5000) + .unitPrice(1000) + .build() + ); + + Receipt saved = receiptRepository.save(receipt); + ReceiptItem targetItem = saved.getItems().get(0); + + UpdateReceiptItemRequest wrongReq = new UpdateReceiptItemRequest( + targetItem.getId(), + saved.getId() + 999, + 1, + null, + null, + 5, + 1000, + "비고" + ); + + // when & then + assertThatThrownBy(() -> + receiptService.updateReceiptItem(targetItem.getId(), wrongReq, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.RECEIPT_NOT_FOUND.getMessage()); + } }