diff --git a/http/shipping.http b/http/shipping.http new file mode 100644 index 00000000..b53a1e2e --- /dev/null +++ b/http/shipping.http @@ -0,0 +1,2 @@ +### 대여 Id로 운송장 번호와 발송일 조회 +GET http://localhost:8080/api/v1/shipping/1 \ No newline at end of file diff --git a/src/main/java/ok/cherry/global/swagger/shipping/ShippingControllerDoc.java b/src/main/java/ok/cherry/global/swagger/shipping/ShippingControllerDoc.java new file mode 100644 index 00000000..ac0b06d9 --- /dev/null +++ b/src/main/java/ok/cherry/global/swagger/shipping/ShippingControllerDoc.java @@ -0,0 +1,28 @@ +package ok.cherry.global.swagger.shipping; + +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import ok.cherry.shipping.application.response.TrackingNumberResponse; + +@Tag(name = "Shipping") +public interface ShippingControllerDoc { + + @Operation(method = "GET", summary = "운송장 번호 및 발송일 조회", description = "대여 Id로 배송 정보의 운송장 번호와 발송일을 조회합니다") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "운송장 번호 및 발송일 조회 성공", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = TrackingNumberResponse.class))), + @ApiResponse(responseCode = "404", description = "운송장 번호 및 발송일 조회 실패 - 배송 정보를 찾을 수 없음", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetail.class))) + }) + ResponseEntity getTrackingNumber( + @Parameter(description = "조회할 대여 Id") Long rentalId + ); +} diff --git a/src/main/java/ok/cherry/shipping/application/ShippingService.java b/src/main/java/ok/cherry/shipping/application/ShippingService.java index ee3832f4..7bbd9647 100644 --- a/src/main/java/ok/cherry/shipping/application/ShippingService.java +++ b/src/main/java/ok/cherry/shipping/application/ShippingService.java @@ -9,6 +9,7 @@ import ok.cherry.member.domain.Member; import ok.cherry.rental.domain.Rental; import ok.cherry.shipping.application.command.CreateShippingCommand; +import ok.cherry.shipping.application.response.TrackingNumberResponse; import ok.cherry.shipping.domain.Shipping; import ok.cherry.shipping.domain.status.ShippingStatus; import ok.cherry.shipping.domain.type.Direction; @@ -44,6 +45,12 @@ public Shipping createShipping( return shippingRepository.save(shipping); } + public TrackingNumberResponse getTrackingNumber(Long rentalId) { + Shipping shipping = shippingRepository.findByRentalIdAndDirection(rentalId, Direction.OUTBOUND) + .orElseThrow(() -> new BusinessException(ShippingError.SHIPPING_NOT_FOUND)); + return TrackingNumberResponse.of(shipping.getTrackingNumber(), shipping.getDetail().getStartAt()); + } + @Transactional public void startShipping(Long shippingId) { Shipping shipping = shippingRepository.findById(shippingId) diff --git a/src/main/java/ok/cherry/shipping/application/response/TrackingNumberResponse.java b/src/main/java/ok/cherry/shipping/application/response/TrackingNumberResponse.java new file mode 100644 index 00000000..6ffbcf1a --- /dev/null +++ b/src/main/java/ok/cherry/shipping/application/response/TrackingNumberResponse.java @@ -0,0 +1,20 @@ +package ok.cherry.shipping.application.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "운송장 번호 및 발송일 조회 응답 DTO") +public record TrackingNumberResponse( + + @Schema(description = "운송장 번호", example = "25090213363012345678") + String trackingNumber, + + @Schema(description = "발송일자", example = "2025-09-02") + LocalDate startAt +) { + public static TrackingNumberResponse of(String trackingNumber, LocalDateTime startAt) { + return new TrackingNumberResponse(trackingNumber, startAt.toLocalDate()); + } +} diff --git a/src/main/java/ok/cherry/shipping/presentation/ShippingController.java b/src/main/java/ok/cherry/shipping/presentation/ShippingController.java index 4989728f..0282d4ab 100644 --- a/src/main/java/ok/cherry/shipping/presentation/ShippingController.java +++ b/src/main/java/ok/cherry/shipping/presentation/ShippingController.java @@ -1,15 +1,26 @@ package ok.cherry.shipping.presentation; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import ok.cherry.global.swagger.shipping.ShippingControllerDoc; import ok.cherry.shipping.application.ShippingService; +import ok.cherry.shipping.application.response.TrackingNumberResponse; @RestController @RequestMapping("/api/v1/shipping") @RequiredArgsConstructor -public class ShippingController { +public class ShippingController implements ShippingControllerDoc { private final ShippingService shippingService; + + @GetMapping("/{rentalId}") + public ResponseEntity getTrackingNumber(@PathVariable Long rentalId) { + TrackingNumberResponse response = shippingService.getTrackingNumber(rentalId); + return ResponseEntity.ok(response); + } } diff --git a/src/test/java/ok/cherry/shipping/application/ShippingServiceTest.java b/src/test/java/ok/cherry/shipping/application/ShippingServiceTest.java index 3561d7dc..71415906 100644 --- a/src/test/java/ok/cherry/shipping/application/ShippingServiceTest.java +++ b/src/test/java/ok/cherry/shipping/application/ShippingServiceTest.java @@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; +import ok.cherry.global.exception.error.BusinessException; import ok.cherry.global.exception.error.DomainException; import ok.cherry.member.MemberBuilder; import ok.cherry.member.domain.Member; @@ -22,6 +23,7 @@ import ok.cherry.rental.infrastructure.RentalRepository; import ok.cherry.shipping.ShippingBuilder; import ok.cherry.shipping.application.command.CreateShippingCommand; +import ok.cherry.shipping.application.response.TrackingNumberResponse; import ok.cherry.shipping.domain.Address; import ok.cherry.shipping.domain.Shipping; import ok.cherry.shipping.domain.status.ShippingStatus; @@ -353,6 +355,47 @@ void completeShipping_notInDelivery() { .hasMessage(ShippingError.NOT_IN_DELIVERY.getMessage()); } + @Test + @DisplayName("대여 Id로 운송장 번호와 발송일 조회에 성공한다") + void getTrackingNumber_success() { + // given + Member member = memberRepository.save(MemberBuilder.create()); + Product product = productRepository.save(ProductBuilder.create()); + Rental rental = rentalRepository.save( + RentalBuilder.builder() + .withMember(member) + .withProduct(product) + .build() + ); + Shipping shipping = shippingRepository.save( + ShippingBuilder.builder() + .withRental(rental) + .withDirection(Direction.OUTBOUND) + .build() + ); + shipping.startShipping(); + + // when + TrackingNumberResponse response = shippingService.getTrackingNumber(rental.getId()); + + // then + Shipping savedShipping = shippingRepository.findById(shipping.getId()).orElseThrow(); + assertThat(response.trackingNumber()).isEqualTo(savedShipping.getTrackingNumber()); + assertThat(response.startAt()).isEqualTo(savedShipping.getDetail().getStartAt().toLocalDate()); + } + + @Test + @DisplayName("대여 Id로 OutBound 배송 정보를 찾을 수 없어 예외가 발생한다") + void getTrackingNumber_notFoundShipping() { + // given + Long nonExistRentalId = 999L; + + // when & then + assertThatThrownBy(() -> shippingService.getTrackingNumber(nonExistRentalId)) + .isInstanceOf(BusinessException.class) + .hasMessage(ShippingError.SHIPPING_NOT_FOUND.getMessage()); + } + private void flushAndClear() { entityManager.flush(); entityManager.clear();