From 90147b24548a11120026d2632047808ecc81e621 Mon Sep 17 00:00:00 2001 From: yeju Date: Sun, 22 Feb 2026 22:15:54 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat]=20=EC=85=80=EB=9F=AC=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=B0=98=ED=92=88=20=EC=83=9D=EC=84=B1=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seller/service/SellerReturnService.java | 98 ++++++ .../service/model/ReturnCreateCommand.java | 13 + .../bbangle/exception/BbangleErrorCode.java | 3 +- .../bbangle/order/domain/OrderItem.java | 8 + .../seller/controller/SellerOrderApi.java | 8 + .../controller/SellerOrderController.java | 15 + .../dto/request/SellerOrderRequest.java | 23 ++ .../dto/response/SellerOrderResponse.java | 48 +++ .../SellerReturnServiceCreateTest.java | 322 ++++++++++++++++++ 9 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java create mode 100644 src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java index 7fc0cee9a..e6f2a4d5a 100644 --- a/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java @@ -2,13 +2,25 @@ import com.bbangle.bbangle.claim.domain.ReturnRequest; import com.bbangle.bbangle.claim.domain.constant.DecisionType; +import com.bbangle.bbangle.claim.domain.constant.ReturnRequestRequestStatus; import com.bbangle.bbangle.claim.repository.ReturnRequestRepository; +import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand; import com.bbangle.bbangle.exception.BbangleErrorCode; import com.bbangle.bbangle.exception.BbangleException; +import com.bbangle.bbangle.order.domain.Order; import com.bbangle.bbangle.order.domain.OrderItem; import com.bbangle.bbangle.order.domain.OrderItemHistory; import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository; +import com.bbangle.bbangle.order.repository.OrderItemRepository; +import com.bbangle.bbangle.order.repository.OrderRepository; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnContent; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; +import com.bbangle.bbangle.seller.repository.SellerRepository; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +31,92 @@ public class SellerReturnService { private final ReturnRequestRepository returnRequestRepository; private final OrderItemHistoryRepository orderItemHistoryRepository; + private final OrderRepository orderRepository; + private final OrderItemRepository orderItemRepository; + private final SellerRepository sellerRepository; + + @Transactional + public ReturnCreateResponse createReturn(ReturnCreateCommand command) { + if (command.orderItemIds() == null || command.orderItemIds().isEmpty()) { + throw new BbangleException(BbangleErrorCode.ORDER_ITEM_NOT_FOUND); + } + + List uniqueOrderItemIds = command.orderItemIds().stream() + .distinct() + .toList(); + + int requestedCount = uniqueOrderItemIds.size(); + + Order order = orderRepository.findById(command.orderId()) + .orElseThrow(() -> new BbangleException(BbangleErrorCode.ORDER_NOT_FOUND)); + + Long storeId = getStoreIdOrThrow(command.sellerId()); + + List orderItems = orderItemRepository.findByOrderIdAndIdIn( + order.getId(), + uniqueOrderItemIds + ); + + Set foundIds = orderItems.stream() + .map(OrderItem::getId) + .collect(Collectors.toSet()); + + if (!foundIds.isEmpty()) { + assertOwnedOrderItems(order.getId(), new ArrayList<>(foundIds), storeId); + } + + List notFoundIds = uniqueOrderItemIds.stream() + .filter(id -> !foundIds.contains(id)) + .toList(); + + List successOrderItemIds = new ArrayList<>(); + List failedOrderItemIds = new ArrayList<>(notFoundIds); + + for (OrderItem orderItem : orderItems) { + if (orderItem.requestReturn()) { + ReturnRequest returnRequest = ReturnRequest.builder() + .orderItem(orderItem) + .detailReason(command.reason()) + .status(ReturnRequestRequestStatus.REQUESTED) + .build(); + returnRequestRepository.save(returnRequest); + orderItemHistoryRepository.save(OrderItemHistory.create(orderItem)); + successOrderItemIds.add(orderItem.getId()); + } else { + failedOrderItemIds.add(orderItem.getId()); + } + } + + int successCount = successOrderItemIds.size(); + int failCount = failedOrderItemIds.size(); + + SellerOrderResponse.Summary summary = + SellerOrderResponse.Summary.of(requestedCount, successCount, failCount); + + ReturnContent content = ReturnContent.of( + order.getId(), + summary, + successOrderItemIds, + failedOrderItemIds + ); + + return ReturnCreateResponse.of(content); + } + + private Long getStoreIdOrThrow(Long sellerId) { + Long storeId = sellerRepository.findStoreIdBySellerId(sellerId); + if (storeId == null) { + throw new BbangleException(BbangleErrorCode.SELLER_NOT_FOUND); + } + return storeId; + } + + private void assertOwnedOrderItems(Long orderId, List orderItemIds, Long storeId) { + long ownedCount = orderItemRepository.countOwnedOrderItems(orderId, orderItemIds, storeId); + if (ownedCount != orderItemIds.size()) { + throw new BbangleException(BbangleErrorCode.ORDER_ACCESS_DENIED); + } + } @Transactional public void decision(List returnIds, Long sellerId, DecisionType decisionType, String reason) { diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java new file mode 100644 index 000000000..b7666328e --- /dev/null +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java @@ -0,0 +1,13 @@ +package com.bbangle.bbangle.claim.seller.service.model; + +import java.util.List; +import lombok.Builder; + +@Builder +public record ReturnCreateCommand( + Long orderId, + List orderItemIds, + String reason, + Long sellerId +) { +} diff --git a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java index 0eda97686..a2cd5fc87 100644 --- a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java +++ b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java @@ -155,7 +155,8 @@ public enum BbangleErrorCode { CLAIM_INVALID_STATUS(-773, "이미 처리된 Claim 입니다", BAD_REQUEST), // Order Error(781 ~ 800) - ORDER_INVALID_STATUS(-781, "요청하신 order의 상태로 변경할 수 없습니다", BAD_REQUEST); + ORDER_INVALID_STATUS(-781, "요청하신 order의 상태로 변경할 수 없습니다", BAD_REQUEST), + RETURN_NOT_ALLOWED(-782, "반품 요청이 불가능한 상태입니다.", BAD_REQUEST); private final int code; private final String message; diff --git a/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java b/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java index 52e81f3ba..5ee36d67b 100644 --- a/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java +++ b/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java @@ -128,6 +128,14 @@ public void shipOrder() { this.orderStatus = OrderStatus.SHIPPED; } + public boolean requestReturn() { + if (this.orderStatus != OrderStatus.SHIPPED && this.orderStatus != OrderStatus.PURCHASE_CONFIRMED) { + return false; + } + this.orderStatus = RETURN_REQUESTED; + return true; + } + public void returnApprove() { if (orderStatus != RETURN_REQUESTED) { diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java index 224188910..6461b7516 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java @@ -11,6 +11,7 @@ import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderItemDetailResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderSearchResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.OrderConfirmResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ShipmentRegisterResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -71,4 +72,11 @@ SingleResult registerShipment( @PathVariable Long orderId, @Valid @RequestBody SellerOrderRequest.ShipmentRegisterRequest request ); + + @Operation(summary = "(판매자) 반품 요청 생성") + SingleResult createReturn( + @AuthenticationPrincipal Long sellerId, + @PathVariable Long orderId, + @Valid @RequestBody SellerOrderRequest.ReturnCreateRequest request + ); } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java index f9cde7355..7eb1c73ca 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java @@ -13,7 +13,9 @@ import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderItemDetailResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderSearchResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.OrderConfirmResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ShipmentRegisterResponse; +import com.bbangle.bbangle.claim.seller.service.SellerReturnService; import com.bbangle.bbangle.order.seller.service.SellerOrderService; import jakarta.validation.Valid; import java.util.List; @@ -40,6 +42,8 @@ public class SellerOrderController implements SellerOrderApi { private final SellerOrderService sellerOrderService; + private final SellerReturnService sellerReturnService; + @Override @GetMapping("/completed") public SingleResult> getCompletedOrders( @@ -144,4 +148,15 @@ public SingleResult registerShipment( return responseService.getSingleResult(result); } + @PostMapping("/{orderId}/returns") + @Override + public SingleResult createReturn( + @AuthenticationPrincipal Long sellerId, + @PathVariable Long orderId, + @Valid @RequestBody SellerOrderRequest.ReturnCreateRequest request + ) { + var result = sellerReturnService.createReturn(request.toCommand(sellerId, orderId)); + return responseService.getSingleResult(result); + } + } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java index 6836fc8e5..c0bd67205 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java @@ -1,5 +1,6 @@ package com.bbangle.bbangle.order.seller.controller.dto.request; +import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand; import com.bbangle.bbangle.order.seller.service.model.SellerOrderCommand.OrderConfirmCommand; import com.bbangle.bbangle.order.seller.service.model.SellerOrderCommand.ShipmentRegisterCommand; import io.swagger.v3.oas.annotations.media.Schema; @@ -54,4 +55,26 @@ public ShipmentRegisterCommand toCommand(Long sellerId, Long orderId) { .build(); } } + + @Schema(description = "판매자 반품 요청 생성 DTO") + public record ReturnCreateRequest( + + @Schema(description = "반품 대상 주문상품 ID 목록") + @NotEmpty(message = "orderItemIds는 필수입니다.") + List orderItemIds, + + @Schema(description = "반품 사유") + String reason + + ) { + + public ReturnCreateCommand toCommand(Long sellerId, Long orderId) { + return ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(orderItemIds) + .reason(reason) + .build(); + } + } } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java index 58af31282..2c8c7aa3f 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java @@ -82,6 +82,54 @@ public static Summary of( } } + @Builder + @Schema(description = "판매자 반품 요청 생성 응답 DTO") + public record ReturnCreateResponse( + + @Schema(description = "반품 요청 결과 컨텐츠") + ReturnContent content + + ) { + + public static ReturnCreateResponse of(ReturnContent content) { + return ReturnCreateResponse.builder() + .content(content) + .build(); + } + } + + @Builder + @Schema(description = "반품 요청 응답 컨텐츠") + public record ReturnContent( + + @Schema(description = "주문 ID") + Long orderId, + + @Schema(description = "반품 요청 요약 정보") + Summary summary, + + @Schema(description = "반품 요청 성공한 주문상품 ID 목록") + List successOrderItemIds, + + @Schema(description = "반품 요청 실패한 주문상품 ID 목록") + List failedOrderItemIds + + ) { + public static ReturnContent of( + Long orderId, + Summary summary, + List successOrderItemIds, + List failedOrderItemIds + ) { + return ReturnContent.builder() + .orderId(orderId) + .summary(summary) + .successOrderItemIds(successOrderItemIds) + .failedOrderItemIds(failedOrderItemIds) + .build(); + } + } + @Builder @Schema(description = "판매자 운송장 정보 등록 응답 DTO") public record ShipmentRegisterResponse( diff --git a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java new file mode 100644 index 000000000..14a534cdb --- /dev/null +++ b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java @@ -0,0 +1,322 @@ +package com.bbangle.bbangle.claim.seller.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import com.bbangle.bbangle.claim.domain.ReturnRequest; +import com.bbangle.bbangle.claim.repository.ReturnRequestRepository; +import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand; +import com.bbangle.bbangle.exception.BbangleErrorCode; +import com.bbangle.bbangle.exception.BbangleException; +import com.bbangle.bbangle.fixture.order.OrderItemFixture; +import com.bbangle.bbangle.order.domain.Order; +import com.bbangle.bbangle.order.domain.OrderItem; +import com.bbangle.bbangle.order.domain.OrderItemHistory; +import com.bbangle.bbangle.order.domain.model.OrderStatus; +import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository; +import com.bbangle.bbangle.order.repository.OrderItemRepository; +import com.bbangle.bbangle.order.repository.OrderRepository; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; +import com.bbangle.bbangle.seller.repository.SellerRepository; +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@DisplayName("[단위 테스트] SellerReturnService - createReturn") +@ExtendWith(MockitoExtension.class) +class SellerReturnServiceCreateTest { + + @InjectMocks + private SellerReturnService sut; + + @Mock + private ReturnRequestRepository returnRequestRepository; + + @Mock + private OrderItemHistoryRepository orderItemHistoryRepository; + + @Mock + private OrderRepository orderRepository; + + @Mock + private OrderItemRepository orderItemRepository; + + @Mock + private SellerRepository sellerRepository; + + @DisplayName("SHIPPED 상태의 주문상품에 대해 반품 요청이 성공한다.") + @Test + void givenShippedOrderItem_whenCreateReturn_thenSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("상품 불량") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ReturnCreateResponse result = sut.createReturn(command); + + // then + assertThat(result).isNotNull(); + assertThat(result.content().orderId()).isEqualTo(orderId); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).isEmpty(); + assertThat(result.content().summary().requestedCount()).isEqualTo(1); + assertThat(result.content().summary().successCount()).isEqualTo(1); + assertThat(result.content().summary().failCount()).isEqualTo(0); + assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); + + then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); + then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + } + + @DisplayName("PURCHASE_CONFIRMED 상태의 주문상품에 대해 반품 요청이 성공한다.") + @Test + void givenPurchaseConfirmedOrderItem_whenCreateReturn_thenSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.PURCHASE_CONFIRMED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("단순 변심") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ReturnCreateResponse result = sut.createReturn(command); + + // then + assertThat(result).isNotNull(); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).isEmpty(); + assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); + + then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); + then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + } + + @DisplayName("혼합 상태(SHIPPED + ORDER_CONFIRMED)에서 SHIPPED만 반품 성공하고, ORDER_CONFIRMED는 실패한다.") + @Test + void givenMixedStatusOrderItems_whenCreateReturn_thenPartialSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem shippedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(shippedItem, "id", 10L); + + OrderItem confirmedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.ORDER_CONFIRMED); + ReflectionTestUtils.setField(confirmedItem, "id", 11L); + + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L, 11L)) + .reason("혼합 테스트") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L, 11L))) + .willReturn(List.of(shippedItem, confirmedItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L, 11L), storeId)) + .willReturn(2L); + + // when + ReturnCreateResponse result = sut.createReturn(command); + + // then + assertThat(result.content().summary().requestedCount()).isEqualTo(2); + assertThat(result.content().summary().successCount()).isEqualTo(1); + assertThat(result.content().summary().failCount()).isEqualTo(1); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).containsExactly(11L); + + assertThat(shippedItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); + assertThat(confirmedItem.getOrderStatus()).isEqualTo(OrderStatus.ORDER_CONFIRMED); + + then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); + then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + } + + @DisplayName("허용되지 않는 상태(PAYMENT_COMPLETED)의 주문상품은 모두 실패한다.") + @Test + void givenInvalidStatusOrderItems_whenCreateReturn_thenAllFail() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem paymentCompletedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.PAYMENT_COMPLETED); + ReflectionTestUtils.setField(paymentCompletedItem, "id", 10L); + + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("반품 테스트") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(paymentCompletedItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ReturnCreateResponse result = sut.createReturn(command); + + // then + assertThat(result.content().summary().successCount()).isEqualTo(0); + assertThat(result.content().summary().failCount()).isEqualTo(1); + assertThat(result.content().successOrderItemIds()).isEmpty(); + assertThat(result.content().failedOrderItemIds()).containsExactly(10L); + + assertThat(paymentCompletedItem.getOrderStatus()).isEqualTo(OrderStatus.PAYMENT_COMPLETED); + + then(returnRequestRepository).should(never()).save(any(ReturnRequest.class)); + then(orderItemHistoryRepository).should(never()).save(any(OrderItemHistory.class)); + } + + @DisplayName("존재하지 않는 orderId이면 ORDER_NOT_FOUND 예외가 발생한다.") + @Test + void givenNonExistingOrderId_whenCreateReturn_thenThrowsException() { + // given + Long orderId = 999L; + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(1L) + .orderId(orderId) + .orderItemIds(List.of(1L)) + .reason("반품 사유") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.empty()); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createReturn(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_NOT_FOUND); + } + + @DisplayName("소유권이 없는 주문상품이면 ORDER_ACCESS_DENIED 예외가 발생한다.") + @Test + void givenNonOwnerSeller_whenCreateReturn_thenThrowsException() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("반품 사유") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(0L); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createReturn(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_ACCESS_DENIED); + } + + @DisplayName("orderItemIds가 비어있으면 ORDER_ITEM_NOT_FOUND 예외가 발생한다.") + @Test + void givenEmptyOrderItemIds_whenCreateReturn_thenThrowsException() { + // given + ReturnCreateCommand command = ReturnCreateCommand.builder() + .sellerId(1L) + .orderId(1L) + .orderItemIds(List.of()) + .reason("반품 사유") + .build(); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createReturn(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_ITEM_NOT_FOUND); + } + + private T newEntity(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 01272f79be383453dcc5e42d4b5604da53123c4b Mon Sep 17 00:00:00 2001 From: yeju Date: Mon, 23 Feb 2026 21:04:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat]=20=ED=8C=90=EB=A7=A4=EC=9E=90=20?= =?UTF-8?q?=EA=B5=90=ED=99=98=20=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bbangle/claim/domain/ExchangeRequest.java | 41 +++ .../bbangle/claim/domain/ReturnRequest.java | 4 +- .../repository/ExchangeRequestRepository.java | 7 + .../seller/service/SellerExchangeService.java | 125 +++++++ .../seller/service/SellerReturnService.java | 27 +- .../service/model/ExchangeCreateCommand.java | 14 + .../service/model/ReturnCreateCommand.java | 1 + .../bbangle/exception/BbangleErrorCode.java | 3 +- .../bbangle/order/domain/OrderItem.java | 7 + .../seller/controller/SellerOrderApi.java | 8 + .../controller/SellerOrderController.java | 15 + .../dto/request/SellerOrderRequest.java | 33 +- .../dto/response/SellerOrderResponse.java | 48 +++ .../SellerExchangeServiceCreateTest.java | 319 ++++++++++++++++++ .../SellerReturnServiceCreateTest.java | 21 +- 15 files changed, 647 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/bbangle/bbangle/claim/repository/ExchangeRequestRepository.java create mode 100644 src/main/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeService.java create mode 100644 src/main/java/com/bbangle/bbangle/claim/seller/service/model/ExchangeCreateCommand.java create mode 100644 src/test/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeServiceCreateTest.java diff --git a/src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java b/src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java index 503e2a52e..6e7d1db01 100644 --- a/src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java +++ b/src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java @@ -1,13 +1,22 @@ package com.bbangle.bbangle.claim.domain; +import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.APPROVED; +import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.REJECTED; +import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.REQUESTED; + import com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus; +import com.bbangle.bbangle.exception.BbangleErrorCode; +import com.bbangle.bbangle.exception.BbangleException; +import com.bbangle.bbangle.order.domain.OrderItem; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -22,4 +31,36 @@ public class ExchangeRequest extends Claim { @Enumerated(EnumType.STRING) private ExchangeRequestStatus status; + private String sellerComment; + + @Builder + public ExchangeRequest( + OrderItem orderItem, + String detailReason, + LocalDateTime decidedAt, + ExchangeRequestStatus status, + String sellerComment + ) { + super(orderItem, detailReason, decidedAt); + this.status = status; + this.sellerComment = sellerComment; + } + + public void approve(String reason) { + if (status != REQUESTED) { + throw new BbangleException(BbangleErrorCode.CLAIM_INVALID_STATUS); + } + this.status = APPROVED; + this.sellerComment = reason; + super.decide(); + } + + public void reject(String reason) { + if (status != REQUESTED) { + throw new BbangleException(BbangleErrorCode.CLAIM_INVALID_STATUS); + } + this.status = REJECTED; + this.sellerComment = reason; + super.decide(); + } } diff --git a/src/main/java/com/bbangle/bbangle/claim/domain/ReturnRequest.java b/src/main/java/com/bbangle/bbangle/claim/domain/ReturnRequest.java index 774e8a0b8..d15da829d 100644 --- a/src/main/java/com/bbangle/bbangle/claim/domain/ReturnRequest.java +++ b/src/main/java/com/bbangle/bbangle/claim/domain/ReturnRequest.java @@ -38,10 +38,12 @@ public ReturnRequest( OrderItem orderItem, String detailReason, LocalDateTime decidedAt, - ReturnRequestRequestStatus status + ReturnRequestRequestStatus status, + String sellerComment ) { super(orderItem, detailReason, decidedAt); this.status = status; + this.sellerComment = sellerComment; } public void approve(String reason) { diff --git a/src/main/java/com/bbangle/bbangle/claim/repository/ExchangeRequestRepository.java b/src/main/java/com/bbangle/bbangle/claim/repository/ExchangeRequestRepository.java new file mode 100644 index 000000000..1cde8e083 --- /dev/null +++ b/src/main/java/com/bbangle/bbangle/claim/repository/ExchangeRequestRepository.java @@ -0,0 +1,7 @@ +package com.bbangle.bbangle.claim.repository; + +import com.bbangle.bbangle.claim.domain.ExchangeRequest; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ExchangeRequestRepository extends JpaRepository { +} diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeService.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeService.java new file mode 100644 index 000000000..75d063b8f --- /dev/null +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeService.java @@ -0,0 +1,125 @@ +package com.bbangle.bbangle.claim.seller.service; + +import com.bbangle.bbangle.claim.domain.ExchangeRequest; +import com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus; +import com.bbangle.bbangle.claim.repository.ExchangeRequestRepository; +import com.bbangle.bbangle.claim.seller.service.model.ExchangeCreateCommand; +import com.bbangle.bbangle.exception.BbangleErrorCode; +import com.bbangle.bbangle.exception.BbangleException; +import com.bbangle.bbangle.order.domain.Order; +import com.bbangle.bbangle.order.domain.OrderItem; +import com.bbangle.bbangle.order.domain.OrderItemHistory; +import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository; +import com.bbangle.bbangle.order.repository.OrderItemRepository; +import com.bbangle.bbangle.order.repository.OrderRepository; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeContent; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeCreateResponse; +import com.bbangle.bbangle.seller.repository.SellerRepository; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class SellerExchangeService { + + private final ExchangeRequestRepository exchangeRequestRepository; + private final OrderItemHistoryRepository orderItemHistoryRepository; + private final OrderRepository orderRepository; + private final OrderItemRepository orderItemRepository; + private final SellerRepository sellerRepository; + + @Transactional + public ExchangeCreateResponse createExchange(ExchangeCreateCommand command) { + if (command.orderItemIds() == null || command.orderItemIds().isEmpty()) { + throw new BbangleException(BbangleErrorCode.ORDER_ITEM_NOT_FOUND); + } + + List uniqueOrderItemIds = command.orderItemIds().stream() + .distinct() + .toList(); + + int requestedCount = uniqueOrderItemIds.size(); + + Order order = orderRepository.findById(command.orderId()) + .orElseThrow(() -> new BbangleException(BbangleErrorCode.ORDER_NOT_FOUND)); + + Long storeId = getStoreIdOrThrow(command.sellerId()); + + List orderItems = orderItemRepository.findByOrderIdAndIdIn( + order.getId(), + uniqueOrderItemIds + ); + + Set foundIds = orderItems.stream() + .map(OrderItem::getId) + .collect(Collectors.toSet()); + + if (!foundIds.isEmpty()) { + assertOwnedOrderItems(order.getId(), new ArrayList<>(foundIds), storeId); + } + + List notFoundIds = uniqueOrderItemIds.stream() + .filter(id -> !foundIds.contains(id)) + .toList(); + + List successOrderItemIds = new ArrayList<>(); + List failedOrderItemIds = new ArrayList<>(notFoundIds); + List exchangeRequestsToSave = new ArrayList<>(); + List historiesToSave = new ArrayList<>(); + + for (OrderItem orderItem : orderItems) { + if (orderItem.requestExchange()) { + ExchangeRequest exchangeRequest = ExchangeRequest.builder() + .orderItem(orderItem) + .detailReason(command.reason()) + .sellerComment(command.sellerComment()) + .status(ExchangeRequestStatus.REQUESTED) + .build(); + exchangeRequestsToSave.add(exchangeRequest); + historiesToSave.add(OrderItemHistory.create(orderItem)); + successOrderItemIds.add(orderItem.getId()); + } else { + failedOrderItemIds.add(orderItem.getId()); + } + } + + exchangeRequestRepository.saveAll(exchangeRequestsToSave); + orderItemHistoryRepository.saveAll(historiesToSave); + + int successCount = successOrderItemIds.size(); + int failCount = failedOrderItemIds.size(); + + SellerOrderResponse.Summary summary = + SellerOrderResponse.Summary.of(requestedCount, successCount, failCount); + + ExchangeContent content = ExchangeContent.of( + order.getId(), + summary, + successOrderItemIds, + failedOrderItemIds + ); + + return ExchangeCreateResponse.of(content); + } + + private Long getStoreIdOrThrow(Long sellerId) { + Long storeId = sellerRepository.findStoreIdBySellerId(sellerId); + if (storeId == null) { + throw new BbangleException(BbangleErrorCode.SELLER_NOT_FOUND); + } + return storeId; + } + + private void assertOwnedOrderItems(Long orderId, List orderItemIds, Long storeId) { + long ownedCount = orderItemRepository.countOwnedOrderItems(orderId, orderItemIds, storeId); + if (ownedCount != orderItemIds.size()) { + throw new BbangleException(BbangleErrorCode.ORDER_ACCESS_DENIED); + } + } +} diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java index e6f2a4d5a..df57c0fa7 100644 --- a/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java @@ -71,22 +71,28 @@ public ReturnCreateResponse createReturn(ReturnCreateCommand command) { List successOrderItemIds = new ArrayList<>(); List failedOrderItemIds = new ArrayList<>(notFoundIds); + List returnRequestsToSave = new ArrayList<>(); + List historiesToSave = new ArrayList<>(); for (OrderItem orderItem : orderItems) { if (orderItem.requestReturn()) { ReturnRequest returnRequest = ReturnRequest.builder() .orderItem(orderItem) .detailReason(command.reason()) + .sellerComment(command.sellerComment()) .status(ReturnRequestRequestStatus.REQUESTED) .build(); - returnRequestRepository.save(returnRequest); - orderItemHistoryRepository.save(OrderItemHistory.create(orderItem)); + returnRequestsToSave.add(returnRequest); + historiesToSave.add(OrderItemHistory.create(orderItem)); successOrderItemIds.add(orderItem.getId()); } else { failedOrderItemIds.add(orderItem.getId()); } } + returnRequestRepository.saveAll(returnRequestsToSave); + orderItemHistoryRepository.saveAll(historiesToSave); + int successCount = successOrderItemIds.size(); int failCount = failedOrderItemIds.size(); @@ -126,28 +132,27 @@ public void decision(List returnIds, Long sellerId, DecisionType decisionT } List returnRequests = returnRequestRepository.findAllById(returnIds); + List historiesToSave = new ArrayList<>(); + for (ReturnRequest returnRequest : returnRequests) { - processDecision(returnRequest, decisionType, reason); + OrderItemHistory history = processDecision(returnRequest, decisionType, reason); + historiesToSave.add(history); } + orderItemHistoryRepository.saveAll(historiesToSave); } - private void processDecision(ReturnRequest returnRequest, DecisionType decisionType, String reason) { + private OrderItemHistory processDecision(ReturnRequest returnRequest, DecisionType decisionType, String reason) { + OrderItem orderItem = returnRequest.getOrderItem(); switch (decisionType) { case APPROVE -> { returnRequest.approve(reason); - OrderItem orderItem = returnRequest.getOrderItem(); orderItem.returnApprove(); - OrderItemHistory history = OrderItemHistory.create(orderItem); - orderItemHistoryRepository.save(history); } case REJECT -> { returnRequest.reject(reason); - OrderItem orderItem = returnRequest.getOrderItem(); orderItem.returnReject(); - OrderItemHistory history = OrderItemHistory.create(orderItem); - orderItemHistoryRepository.save(history); } } + return OrderItemHistory.create(orderItem); } - } diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ExchangeCreateCommand.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ExchangeCreateCommand.java new file mode 100644 index 000000000..3e1c3c09b --- /dev/null +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ExchangeCreateCommand.java @@ -0,0 +1,14 @@ +package com.bbangle.bbangle.claim.seller.service.model; + +import java.util.List; +import lombok.Builder; + +@Builder +public record ExchangeCreateCommand( + Long orderId, + List orderItemIds, + String reason, + String sellerComment, + Long sellerId +) { +} diff --git a/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java index b7666328e..4cae547ca 100644 --- a/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java +++ b/src/main/java/com/bbangle/bbangle/claim/seller/service/model/ReturnCreateCommand.java @@ -8,6 +8,7 @@ public record ReturnCreateCommand( Long orderId, List orderItemIds, String reason, + String sellerComment, Long sellerId ) { } diff --git a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java index a2cd5fc87..15978c464 100644 --- a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java +++ b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java @@ -156,7 +156,8 @@ public enum BbangleErrorCode { // Order Error(781 ~ 800) ORDER_INVALID_STATUS(-781, "요청하신 order의 상태로 변경할 수 없습니다", BAD_REQUEST), - RETURN_NOT_ALLOWED(-782, "반품 요청이 불가능한 상태입니다.", BAD_REQUEST); + RETURN_NOT_ALLOWED(-782, "반품 요청이 불가능한 상태입니다.", BAD_REQUEST), + EXCHANGE_NOT_ALLOWED(-784, "교환 요청이 불가능한 상태입니다.", BAD_REQUEST); private final int code; private final String message; diff --git a/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java b/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java index 5ee36d67b..2fcbc13c7 100644 --- a/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java +++ b/src/main/java/com/bbangle/bbangle/order/domain/OrderItem.java @@ -136,6 +136,13 @@ public boolean requestReturn() { return true; } + public boolean requestExchange() { + if (this.orderStatus != OrderStatus.SHIPPED && this.orderStatus != OrderStatus.PURCHASE_CONFIRMED) { + return false; + } + this.orderStatus = OrderStatus.EXCHANGE_REQUEST; + return true; + } public void returnApprove() { if (orderStatus != RETURN_REQUESTED) { diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java index 6461b7516..b0ed4ed7a 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java @@ -10,6 +10,7 @@ import com.bbangle.bbangle.order.seller.controller.dto.response.OrderDetailResponse.OrderDetail; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderItemDetailResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderSearchResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.OrderConfirmResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ShipmentRegisterResponse; @@ -79,4 +80,11 @@ SingleResult createReturn( @PathVariable Long orderId, @Valid @RequestBody SellerOrderRequest.ReturnCreateRequest request ); + + @Operation(summary = "(판매자) 교환 요청 생성") + SingleResult createExchange( + @AuthenticationPrincipal Long sellerId, + @PathVariable Long orderId, + @Valid @RequestBody SellerOrderRequest.ExchangeCreateRequest request + ); } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java index 7eb1c73ca..9f2ff7aef 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java @@ -12,9 +12,11 @@ import com.bbangle.bbangle.order.seller.controller.dto.response.OrderDetailResponse.OrderDetail; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderItemDetailResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.OrderResponse.OrderSearchResponse; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.OrderConfirmResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse; import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ShipmentRegisterResponse; +import com.bbangle.bbangle.claim.seller.service.SellerExchangeService; import com.bbangle.bbangle.claim.seller.service.SellerReturnService; import com.bbangle.bbangle.order.seller.service.SellerOrderService; import jakarta.validation.Valid; @@ -44,6 +46,8 @@ public class SellerOrderController implements SellerOrderApi { private final SellerReturnService sellerReturnService; + private final SellerExchangeService sellerExchangeService; + @Override @GetMapping("/completed") public SingleResult> getCompletedOrders( @@ -159,4 +163,15 @@ public SingleResult createReturn( return responseService.getSingleResult(result); } + @PostMapping("/{orderId}/exchanges") + @Override + public SingleResult createExchange( + @AuthenticationPrincipal Long sellerId, + @PathVariable Long orderId, + @Valid @RequestBody SellerOrderRequest.ExchangeCreateRequest request + ) { + var result = sellerExchangeService.createExchange(request.toCommand(sellerId, orderId)); + return responseService.getSingleResult(result); + } + } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java index c0bd67205..c0807dca5 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java @@ -1,5 +1,6 @@ package com.bbangle.bbangle.order.seller.controller.dto.request; +import com.bbangle.bbangle.claim.seller.service.model.ExchangeCreateCommand; import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand; import com.bbangle.bbangle.order.seller.service.model.SellerOrderCommand.OrderConfirmCommand; import com.bbangle.bbangle.order.seller.service.model.SellerOrderCommand.ShipmentRegisterCommand; @@ -64,7 +65,10 @@ public record ReturnCreateRequest( List orderItemIds, @Schema(description = "반품 사유") - String reason + String reason, + + @Schema(description = "판매자 코멘트") + String sellerComment ) { @@ -74,6 +78,33 @@ public ReturnCreateCommand toCommand(Long sellerId, Long orderId) { .orderId(orderId) .orderItemIds(orderItemIds) .reason(reason) + .sellerComment(sellerComment) + .build(); + } + } + + @Schema(description = "판매자 교환 요청 생성 DTO") + public record ExchangeCreateRequest( + + @Schema(description = "교환 대상 주문상품 ID 목록") + @NotEmpty(message = "orderItemIds는 필수입니다.") + List orderItemIds, + + @Schema(description = "교환 사유") + String reason, + + @Schema(description = "판매자 코멘트") + String sellerComment + + ) { + + public ExchangeCreateCommand toCommand(Long sellerId, Long orderId) { + return ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(orderItemIds) + .reason(reason) + .sellerComment(sellerComment) .build(); } } diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java index 2c8c7aa3f..3ff38b900 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/response/SellerOrderResponse.java @@ -130,6 +130,54 @@ public static ReturnContent of( } } + @Builder + @Schema(description = "판매자 교환 요청 생성 응답 DTO") + public record ExchangeCreateResponse( + + @Schema(description = "교환 요청 결과 컨텐츠") + ExchangeContent content + + ) { + + public static ExchangeCreateResponse of(ExchangeContent content) { + return ExchangeCreateResponse.builder() + .content(content) + .build(); + } + } + + @Builder + @Schema(description = "교환 요청 응답 컨텐츠") + public record ExchangeContent( + + @Schema(description = "주문 ID") + Long orderId, + + @Schema(description = "교환 요청 요약 정보") + Summary summary, + + @Schema(description = "교환 요청 성공한 주문상품 ID 목록") + List successOrderItemIds, + + @Schema(description = "교환 요청 실패한 주문상품 ID 목록") + List failedOrderItemIds + + ) { + public static ExchangeContent of( + Long orderId, + Summary summary, + List successOrderItemIds, + List failedOrderItemIds + ) { + return ExchangeContent.builder() + .orderId(orderId) + .summary(summary) + .successOrderItemIds(successOrderItemIds) + .failedOrderItemIds(failedOrderItemIds) + .build(); + } + } + @Builder @Schema(description = "판매자 운송장 정보 등록 응답 DTO") public record ShipmentRegisterResponse( diff --git a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeServiceCreateTest.java b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeServiceCreateTest.java new file mode 100644 index 000000000..f6933ce5f --- /dev/null +++ b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerExchangeServiceCreateTest.java @@ -0,0 +1,319 @@ +package com.bbangle.bbangle.claim.seller.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import com.bbangle.bbangle.claim.repository.ExchangeRequestRepository; +import com.bbangle.bbangle.claim.seller.service.model.ExchangeCreateCommand; +import com.bbangle.bbangle.exception.BbangleErrorCode; +import com.bbangle.bbangle.exception.BbangleException; +import com.bbangle.bbangle.fixture.order.OrderItemFixture; +import com.bbangle.bbangle.order.domain.Order; +import com.bbangle.bbangle.order.domain.OrderItem; +import com.bbangle.bbangle.order.domain.model.OrderStatus; +import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository; +import com.bbangle.bbangle.order.repository.OrderItemRepository; +import com.bbangle.bbangle.order.repository.OrderRepository; +import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeCreateResponse; +import com.bbangle.bbangle.seller.repository.SellerRepository; +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@DisplayName("[단위 테스트] SellerExchangeService - createExchange") +@ExtendWith(MockitoExtension.class) +class SellerExchangeServiceCreateTest { + + @InjectMocks + private SellerExchangeService sut; + + @Mock + private ExchangeRequestRepository exchangeRequestRepository; + + @Mock + private OrderItemHistoryRepository orderItemHistoryRepository; + + @Mock + private OrderRepository orderRepository; + + @Mock + private OrderItemRepository orderItemRepository; + + @Mock + private SellerRepository sellerRepository; + + @DisplayName("SHIPPED 상태의 주문상품에 대해 교환 요청이 성공한다.") + @Test + void givenShippedOrderItem_whenCreateExchange_thenSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("상품 불량") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ExchangeCreateResponse result = sut.createExchange(command); + + // then + assertThat(result).isNotNull(); + assertThat(result.content().orderId()).isEqualTo(orderId); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).isEmpty(); + assertThat(result.content().summary().requestedCount()).isEqualTo(1); + assertThat(result.content().summary().successCount()).isEqualTo(1); + assertThat(result.content().summary().failCount()).isEqualTo(0); + assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.EXCHANGE_REQUEST); + + then(exchangeRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); + } + + @DisplayName("PURCHASE_CONFIRMED 상태의 주문상품에 대해 교환 요청이 성공한다.") + @Test + void givenPurchaseConfirmedOrderItem_whenCreateExchange_thenSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.PURCHASE_CONFIRMED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("사이즈 변경") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ExchangeCreateResponse result = sut.createExchange(command); + + // then + assertThat(result).isNotNull(); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).isEmpty(); + assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.EXCHANGE_REQUEST); + + then(exchangeRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); + } + + @DisplayName("혼합 상태(SHIPPED + ORDER_CONFIRMED)에서 SHIPPED만 교환 성공하고, ORDER_CONFIRMED는 실패한다.") + @Test + void givenMixedStatusOrderItems_whenCreateExchange_thenPartialSuccess() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem shippedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(shippedItem, "id", 10L); + + OrderItem confirmedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.ORDER_CONFIRMED); + ReflectionTestUtils.setField(confirmedItem, "id", 11L); + + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L, 11L)) + .reason("혼합 테스트") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L, 11L))) + .willReturn(List.of(shippedItem, confirmedItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L, 11L), storeId)) + .willReturn(2L); + + // when + ExchangeCreateResponse result = sut.createExchange(command); + + // then + assertThat(result.content().summary().requestedCount()).isEqualTo(2); + assertThat(result.content().summary().successCount()).isEqualTo(1); + assertThat(result.content().summary().failCount()).isEqualTo(1); + assertThat(result.content().successOrderItemIds()).containsExactly(10L); + assertThat(result.content().failedOrderItemIds()).containsExactly(11L); + + assertThat(shippedItem.getOrderStatus()).isEqualTo(OrderStatus.EXCHANGE_REQUEST); + assertThat(confirmedItem.getOrderStatus()).isEqualTo(OrderStatus.ORDER_CONFIRMED); + + then(exchangeRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); + } + + @DisplayName("허용되지 않는 상태(PAYMENT_COMPLETED)의 주문상품은 모두 실패한다.") + @Test + void givenInvalidStatusOrderItems_whenCreateExchange_thenAllFail() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem paymentCompletedItem = OrderItemFixture.orderItemWithStatus(OrderStatus.PAYMENT_COMPLETED); + ReflectionTestUtils.setField(paymentCompletedItem, "id", 10L); + + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("교환 테스트") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(paymentCompletedItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(1L); + + // when + ExchangeCreateResponse result = sut.createExchange(command); + + // then + assertThat(result.content().summary().successCount()).isEqualTo(0); + assertThat(result.content().summary().failCount()).isEqualTo(1); + assertThat(result.content().successOrderItemIds()).isEmpty(); + assertThat(result.content().failedOrderItemIds()).containsExactly(10L); + + assertThat(paymentCompletedItem.getOrderStatus()).isEqualTo(OrderStatus.PAYMENT_COMPLETED); + + then(exchangeRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); + } + + @DisplayName("존재하지 않는 orderId이면 ORDER_NOT_FOUND 예외가 발생한다.") + @Test + void givenNonExistingOrderId_whenCreateExchange_thenThrowsException() { + // given + Long orderId = 999L; + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(1L) + .orderId(orderId) + .orderItemIds(List.of(1L)) + .reason("교환 사유") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.empty()); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createExchange(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_NOT_FOUND); + } + + @DisplayName("소유권이 없는 주문상품이면 ORDER_ACCESS_DENIED 예외가 발생한다.") + @Test + void givenNonOwnerSeller_whenCreateExchange_thenThrowsException() { + // given + Long orderId = 1L; + Long sellerId = 1L; + Long storeId = 100L; + + Order order = newEntity(Order.class); + ReflectionTestUtils.setField(order, "id", orderId); + + OrderItem orderItem = OrderItemFixture.orderItemWithStatus(OrderStatus.SHIPPED); + ReflectionTestUtils.setField(orderItem, "id", 10L); + + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(sellerId) + .orderId(orderId) + .orderItemIds(List.of(10L)) + .reason("교환 사유") + .build(); + + given(orderRepository.findById(orderId)).willReturn(Optional.of(order)); + given(sellerRepository.findStoreIdBySellerId(sellerId)).willReturn(storeId); + given(orderItemRepository.findByOrderIdAndIdIn(orderId, List.of(10L))) + .willReturn(List.of(orderItem)); + given(orderItemRepository.countOwnedOrderItems(orderId, List.of(10L), storeId)) + .willReturn(0L); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createExchange(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_ACCESS_DENIED); + } + + @DisplayName("orderItemIds가 비어있으면 ORDER_ITEM_NOT_FOUND 예외가 발생한다.") + @Test + void givenEmptyOrderItemIds_whenCreateExchange_thenThrowsException() { + // given + ExchangeCreateCommand command = ExchangeCreateCommand.builder() + .sellerId(1L) + .orderId(1L) + .orderItemIds(List.of()) + .reason("교환 사유") + .build(); + + // when + BbangleException result = assertThrows(BbangleException.class, + () -> sut.createExchange(command)); + + // then + assertThat(result.getBbangleErrorCode()).isEqualTo(BbangleErrorCode.ORDER_ITEM_NOT_FOUND); + } + + private T newEntity(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java index 14a534cdb..212802f81 100644 --- a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java +++ b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceCreateTest.java @@ -2,13 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import com.bbangle.bbangle.claim.domain.ReturnRequest; import com.bbangle.bbangle.claim.repository.ReturnRequestRepository; import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand; import com.bbangle.bbangle.exception.BbangleErrorCode; @@ -16,7 +14,6 @@ import com.bbangle.bbangle.fixture.order.OrderItemFixture; import com.bbangle.bbangle.order.domain.Order; import com.bbangle.bbangle.order.domain.OrderItem; -import com.bbangle.bbangle.order.domain.OrderItemHistory; import com.bbangle.bbangle.order.domain.model.OrderStatus; import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository; import com.bbangle.bbangle.order.repository.OrderItemRepository; @@ -97,8 +94,8 @@ void givenShippedOrderItem_whenCreateReturn_thenSuccess() { assertThat(result.content().summary().failCount()).isEqualTo(0); assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); - then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); - then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + then(returnRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @DisplayName("PURCHASE_CONFIRMED 상태의 주문상품에 대해 반품 요청이 성공한다.") @@ -138,8 +135,8 @@ void givenPurchaseConfirmedOrderItem_whenCreateReturn_thenSuccess() { assertThat(result.content().failedOrderItemIds()).isEmpty(); assertThat(orderItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); - then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); - then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + then(returnRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @DisplayName("혼합 상태(SHIPPED + ORDER_CONFIRMED)에서 SHIPPED만 반품 성공하고, ORDER_CONFIRMED는 실패한다.") @@ -186,8 +183,8 @@ void givenMixedStatusOrderItems_whenCreateReturn_thenPartialSuccess() { assertThat(shippedItem.getOrderStatus()).isEqualTo(OrderStatus.RETURN_REQUESTED); assertThat(confirmedItem.getOrderStatus()).isEqualTo(OrderStatus.ORDER_CONFIRMED); - then(returnRequestRepository).should(times(1)).save(any(ReturnRequest.class)); - then(orderItemHistoryRepository).should(times(1)).save(any(OrderItemHistory.class)); + then(returnRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @DisplayName("허용되지 않는 상태(PAYMENT_COMPLETED)의 주문상품은 모두 실패한다.") @@ -229,8 +226,8 @@ void givenInvalidStatusOrderItems_whenCreateReturn_thenAllFail() { assertThat(paymentCompletedItem.getOrderStatus()).isEqualTo(OrderStatus.PAYMENT_COMPLETED); - then(returnRequestRepository).should(never()).save(any(ReturnRequest.class)); - then(orderItemHistoryRepository).should(never()).save(any(OrderItemHistory.class)); + then(returnRequestRepository).should(times(1)).saveAll(anyList()); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @DisplayName("존재하지 않는 orderId이면 ORDER_NOT_FOUND 예외가 발생한다.") From 289b6d5cfc82cdb7b7eef159554a60cdb085faac Mon Sep 17 00:00:00 2001 From: yeju Date: Mon, 23 Feb 2026 22:34:32 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A4=91=EB=B3=B5=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/bbangle/bbangle/exception/BbangleErrorCode.java | 6 +++--- .../bbangle/order/seller/controller/SellerOrderApi.java | 2 ++ .../order/seller/controller/SellerOrderController.java | 3 +++ .../seller/controller/dto/request/SellerOrderRequest.java | 4 ++++ .../claim/seller/service/SellerReturnServiceUnitTest.java | 4 ++-- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java index 0ba3a3146..90290640a 100644 --- a/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java +++ b/src/main/java/com/bbangle/bbangle/exception/BbangleErrorCode.java @@ -159,9 +159,9 @@ public enum BbangleErrorCode { // Order Error(781 ~ 800) ORDER_INVALID_STATUS(-781, "요청하신 order의 상태로 변경할 수 없습니다", BAD_REQUEST), RETURN_NOT_ALLOWED(-782, "반품 요청이 불가능한 상태입니다.", BAD_REQUEST), - EXCHANGE_NOT_ALLOWED(-784, "교환 요청이 불가능한 상태입니다.", BAD_REQUEST); - DELIVERY_NOT_FOUND(-782, "해당 주문상품의 배송 정보를 찾을 수 없습니다.", NOT_FOUND), - DELIVERY_MODIFY_NOT_ALLOWED(-783, "현재 배송 상태에서는 운송장을 수정할 수 없습니다.", BAD_REQUEST); + EXCHANGE_NOT_ALLOWED(-783, "교환 요청이 불가능한 상태입니다.", BAD_REQUEST), + DELIVERY_NOT_FOUND(-784, "해당 주문상품의 배송 정보를 찾을 수 없습니다.", NOT_FOUND), + DELIVERY_MODIFY_NOT_ALLOWED(-785, "현재 배송 상태에서는 운송장을 수정할 수 없습니다.", BAD_REQUEST); private final int code; private final String message; diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java index 5c1245bc0..288d3b9ef 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderApi.java @@ -87,6 +87,8 @@ SingleResult createExchange( @AuthenticationPrincipal Long sellerId, @PathVariable Long orderId, @Valid @RequestBody SellerOrderRequest.ExchangeCreateRequest request + ); + @Operation(summary = "(판매자) 운송장 수정") SingleResult modifyShipment( @AuthenticationPrincipal Long sellerId, diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java index 0e263589a..3386e1e84 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/SellerOrderController.java @@ -173,6 +173,9 @@ public SingleResult createExchange( @Valid @RequestBody SellerOrderRequest.ExchangeCreateRequest request ) { var result = sellerExchangeService.createExchange(request.toCommand(sellerId, orderId)); + return responseService.getSingleResult(result); + } + @PatchMapping("/{orderId}/shipment") @Override public SingleResult modifyShipment( diff --git a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java index b0ec5ccde..d487e6626 100644 --- a/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java +++ b/src/main/java/com/bbangle/bbangle/order/seller/controller/dto/request/SellerOrderRequest.java @@ -106,6 +106,10 @@ public ExchangeCreateCommand toCommand(Long sellerId, Long orderId) { .orderItemIds(orderItemIds) .reason(reason) .sellerComment(sellerComment) + .build(); + } + } + @Schema(description = "판매자 운송장 수정 요청 DTO") public record ShipmentModifyRequest( diff --git a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceUnitTest.java b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceUnitTest.java index 0d2758fba..7972ef117 100644 --- a/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceUnitTest.java +++ b/src/test/java/com/bbangle/bbangle/claim/seller/service/SellerReturnServiceUnitTest.java @@ -68,7 +68,7 @@ void decision_approve_success() { // then then(returnRequestRepository).should(times(1)).countReturnsBySeller(returnIds, sellerId); then(returnRequestRepository).should(times(1)).findAllById(returnIds); - then(orderItemHistoryRepository).should(times(3)).save(any(OrderItemHistory.class)); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @Test @@ -93,7 +93,7 @@ void decision_reject_success() { // then then(returnRequestRepository).should(times(1)).countReturnsBySeller(returnIds, sellerId); then(returnRequestRepository).should(times(1)).findAllById(returnIds); - then(orderItemHistoryRepository).should(times(2)).save(any(OrderItemHistory.class)); + then(orderItemHistoryRepository).should(times(1)).saveAll(anyList()); } @Test