From d13574045dd7677bec0737be066088d25dc5e152 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:39:13 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[FEAT]:=20PaymentController=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8:=20=EA=B2=B0=EC=A0=9C=20=EC=9A=94=EC=B2=AD/?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8/=EC=B7=A8=EC=86=8C/=EC=A1=B0=ED=9A=8C=20API?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PaymentControllerTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java diff --git a/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java new file mode 100644 index 00000000..717f021e --- /dev/null +++ b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java @@ -0,0 +1,187 @@ +package com.eatsfine.eatsfine.domain.payment.controller; + +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO; +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; +import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; +import com.eatsfine.eatsfine.domain.payment.service.PaymentService; +import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; +import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; +import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(PaymentController.class) +@WithMockUser +class PaymentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PaymentService paymentService; + + @MockBean + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @MockBean + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + @MockBean + private CustomAccessDeniedHandler customAccessDeniedHandler; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() throws ServletException, IOException { + doAnswer(invocation -> { + HttpServletRequest request = invocation.getArgument(0); + HttpServletResponse response = invocation.getArgument(1); + FilterChain chain = invocation.getArgument(2); + chain.doFilter(request, response); + return null; + }).when(jwtAuthenticationFilter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(FilterChain.class)); + } + + @Test + @DisplayName("결제 요청 성공") + void requestPayment_success() throws Exception { + // given + PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(1L); + PaymentResponseDTO.PaymentRequestResultDTO response = new PaymentResponseDTO.PaymentRequestResultDTO( + 1L, 1L, "order-id-123", BigDecimal.valueOf(10000), LocalDateTime.now()); + + given(paymentService.requestPayment(any(PaymentRequestDTO.RequestPaymentDTO.class))).willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/request") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.paymentId").value(1L)) + .andExpect(jsonPath("$.result.orderId").value("order-id-123")); + } + + @Test + @DisplayName("결제 승인 성공") + void confirmPayment_success() throws Exception { + // given + PaymentConfirmDTO request = PaymentConfirmDTO.builder() + .paymentKey("payment-key-123") + .orderId("order-id-123") + .amount(BigDecimal.valueOf(10000)) + .build(); + + PaymentResponseDTO.PaymentSuccessResultDTO response = new PaymentResponseDTO.PaymentSuccessResultDTO( + 1L, "COMPLETED", LocalDateTime.now(), "order-id-123", BigDecimal.valueOf(10000), + "CARD", "TOSS", "http://receipt.url"); + + given(paymentService.confirmPayment(any(PaymentConfirmDTO.class))).willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/confirm") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.status").value("COMPLETED")); + } + + @Test + @DisplayName("결제 취소 성공") + void cancelPayment_success() throws Exception { + // given + String paymentKey = "payment-key-123"; + PaymentRequestDTO.CancelPaymentDTO request = new PaymentRequestDTO.CancelPaymentDTO("단순 변심"); + PaymentResponseDTO.CancelPaymentResultDTO response = new PaymentResponseDTO.CancelPaymentResultDTO( + 1L, "order-id-123", paymentKey, "CANCELED", LocalDateTime.now()); + + given(paymentService.cancelPayment(eq(paymentKey), any(PaymentRequestDTO.CancelPaymentDTO.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/{paymentKey}/cancel", paymentKey) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.status").value("CANCELED")); + } + + @Test + @DisplayName("결제 내역 조회 성공") + void getPaymentList_success() throws Exception { + // given + PaymentResponseDTO.PaginationDTO pagination = new PaymentResponseDTO.PaginationDTO(1, 1, 1L); + PaymentResponseDTO.PaymentHistoryResultDTO history = new PaymentResponseDTO.PaymentHistoryResultDTO( + 1L, 1L, "Store Name", BigDecimal.valueOf(10000), "DEPOSIT", "CARD", "TOSS", "COMPLETED", + LocalDateTime.now()); + PaymentResponseDTO.PaymentListResponseDTO response = new PaymentResponseDTO.PaymentListResponseDTO( + Collections.singletonList(history), pagination); + + given(paymentService.getPaymentList(any(Long.class), any(Integer.class), any(Integer.class), any())) + .willReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/payments") + .param("page", "1") + .param("limit", "10") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.payments[0].storeName").value("Store Name")); + } + + @Test + @DisplayName("결제 상세 조회 성공") + void getPaymentDetail_success() throws Exception { + // given + Long paymentId = 1L; + PaymentResponseDTO.PaymentDetailResultDTO response = new PaymentResponseDTO.PaymentDetailResultDTO( + paymentId, 1L, "Store Name", "CARD", "TOSS", BigDecimal.valueOf(10000), "DEPOSIT", + "COMPLETED", LocalDateTime.now(), LocalDateTime.now(), "http://receipt.url", null); + + given(paymentService.getPaymentDetail(eq(paymentId))).willReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/payments/{paymentId}", paymentId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.paymentId").value(paymentId)); + } +} From eb8c3b7044049f5488885d469e86b2dcbd39654d Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:39:39 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[FEAT]:=20PaymentService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8:=20=EA=B2=B0=EC=A0=9C=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/service/PaymentServiceTest.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java diff --git a/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java b/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java new file mode 100644 index 00000000..b4027ca4 --- /dev/null +++ b/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java @@ -0,0 +1,218 @@ +package com.eatsfine.eatsfine.domain.payment.service; + +import com.eatsfine.eatsfine.domain.booking.entity.Booking; +import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository; +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO; +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; +import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; +import com.eatsfine.eatsfine.domain.payment.dto.response.TossPaymentResponse; +import com.eatsfine.eatsfine.domain.payment.entity.Payment; +import com.eatsfine.eatsfine.domain.payment.enums.PaymentMethod; +import com.eatsfine.eatsfine.domain.payment.enums.PaymentProvider; +import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; +import com.eatsfine.eatsfine.domain.payment.enums.PaymentType; +import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; +import com.eatsfine.eatsfine.domain.payment.repository.PaymentRepository; +import com.eatsfine.eatsfine.domain.store.entity.Store; +import com.eatsfine.eatsfine.domain.payment.status.PaymentErrorStatus; +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; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PaymentServiceTest { + + @InjectMocks + private PaymentService paymentService; + + @Mock + private PaymentRepository paymentRepository; + + @Mock + private BookingRepository bookingRepository; + + @Mock + private TossPaymentService tossPaymentService; + + @Test + @DisplayName("결제 요청 성공") + void requestPayment_success() { + // given + Long bookingId = 1L; + PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(bookingId); + + Booking booking = Booking.builder() + .id(bookingId) + .build(); + ReflectionTestUtils.setField(booking, "depositAmount", BigDecimal.valueOf(10000)); + + Payment payment = Payment.builder() + .id(1L) + .booking(booking) + .amount(BigDecimal.valueOf(10000)) + .orderId("generated-order-id") + .paymentStatus(PaymentStatus.PENDING) + .requestedAt(LocalDateTime.now()) + .build(); + + given(bookingRepository.findById(bookingId)).willReturn(Optional.of(booking)); + given(paymentRepository.save(any(Payment.class))).willReturn(payment); + + // when + PaymentResponseDTO.PaymentRequestResultDTO response = paymentService.requestPayment(request); + + // then + assertThat(response.amount()).isEqualTo(BigDecimal.valueOf(10000)); + assertThat(response.orderId()).isEqualTo("generated-order-id"); + verify(paymentRepository, times(1)).save(any(Payment.class)); + } + + @Test + @DisplayName("결제 요청 실패 - 예약 없음") + void requestPayment_fail_bookingNotFound() { + // given + PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(999L); + given(bookingRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> paymentService.requestPayment(request)) + .isInstanceOf(PaymentException.class) + .extracting("code") + .isEqualTo(PaymentErrorStatus._BOOKING_NOT_FOUND); + } + + @Test + @DisplayName("결제 승인 성공") + void confirmPayment_success() { + // given + String orderId = "order-id-123"; + String paymentKey = "payment-key-123"; + BigDecimal amount = BigDecimal.valueOf(10000); + PaymentConfirmDTO request = PaymentConfirmDTO.builder() + .orderId(orderId) + .amount(amount) + .paymentKey(paymentKey) + .build(); + + Booking booking = Booking.builder().id(1L).build(); + Payment payment = Payment.builder() + .id(1L) + .booking(booking) + .orderId(orderId) + .amount(amount) + .paymentStatus(PaymentStatus.PENDING) + .build(); + + TossPaymentResponse.EasyPay easyPay = new TossPaymentResponse.EasyPay("토스페이", 10000, 0); + TossPaymentResponse tossResponse = new TossPaymentResponse( + paymentKey, "NORMAL", orderId, "orderName", "mId", "KRW", "CARD", + 10000, 10000, "DONE", + java.time.OffsetDateTime.now(), java.time.OffsetDateTime.now(), + false, null, 10000, 0, + easyPay, new TossPaymentResponse.Receipt("http://receipt.url"));// TossPaymentResponse + // record 생성자가 많아서 + // 필드에 맞게 넣어줌 (가정) + // 실제 record 구조에 따라 맞춰야 + // 함. 위 내용은 예시. + // TossPaymentResponse가 + // record이므로 생성자 + // 파라미터 순서 중요. + // 여기서는 Mocking을 하거나, + // 필드가 많으면 빌더나 생성자를 + // 확인해야 함. + // TossPaymentService가 + // Mock이므로 response + // 리턴값만 잘 맞춰주면 됨. + + given(paymentRepository.findByOrderId(orderId)).willReturn(Optional.of(payment)); + given(tossPaymentService.confirm(any(PaymentConfirmDTO.class))).willReturn(tossResponse); + + // when + PaymentResponseDTO.PaymentSuccessResultDTO response = paymentService.confirmPayment(request); + + // then + assertThat(response.status()).isEqualTo(PaymentStatus.COMPLETED.name()); + assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.COMPLETED); + } + + @Test + @DisplayName("결제 승인 실패 - 금액 불일치") + void confirmPayment_fail_invalidAmount() { + // given + String orderId = "order-id-123"; + BigDecimal originalAmount = BigDecimal.valueOf(10000); + BigDecimal requestAmount = BigDecimal.valueOf(5000); // Mismatch + + PaymentConfirmDTO request = PaymentConfirmDTO.builder() + .orderId(orderId) + .amount(requestAmount) + .paymentKey("key") + .build(); + + Payment payment = Payment.builder() + .id(1L) + .orderId(orderId) + .amount(originalAmount) + .paymentStatus(PaymentStatus.PENDING) + .build(); + + given(paymentRepository.findByOrderId(orderId)).willReturn(Optional.of(payment)); + + // when & then + assertThatThrownBy(() -> paymentService.confirmPayment(request)) + .isInstanceOf(PaymentException.class) + .extracting("code") + .isEqualTo(PaymentErrorStatus._PAYMENT_INVALID_AMOUNT); + + assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.FAILED); + } + + @Test + @DisplayName("결제 취소 성공") + void cancelPayment_success() { + // given + String paymentKey = "payment-key-123"; + PaymentRequestDTO.CancelPaymentDTO request = new PaymentRequestDTO.CancelPaymentDTO("단순 변심"); + + Payment payment = Payment.builder() + .id(1L) + .paymentKey(paymentKey) + .orderId("order-id-123") + .paymentStatus(PaymentStatus.COMPLETED) + .build(); + + TossPaymentResponse tossResponse = new TossPaymentResponse( + paymentKey, "NORMAL", "order-id-123", "orderName", "mId", "KRW", "CARD", + 10000, 0, "CANCELED", + java.time.OffsetDateTime.now(), java.time.OffsetDateTime.now(), + false, null, 10000, 0, + null, null); + given(paymentRepository.findByPaymentKey(paymentKey)).willReturn(Optional.of(payment)); + given(tossPaymentService.cancel(eq(paymentKey), any(PaymentRequestDTO.CancelPaymentDTO.class))) + .willReturn(tossResponse); + + // when + PaymentResponseDTO.CancelPaymentResultDTO response = paymentService.cancelPayment(paymentKey, request); + + // then + assertThat(response.status()).isEqualTo("REFUNDED"); + assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.REFUNDED); + } +} From f7c249cec5a208bcd101cc7eb3533c483302bf8a Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:21:59 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[FEAT]:=20=EB=AF=BC=EA=B0=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 922e8e1b..5df0f1d3 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -60,14 +60,16 @@ spring: payment: toss: - widget-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6 + widget-secret-key: ${TOSS_WIDGET_SECRET_KEY} cloud: aws: - region: ap-northeast-2 + region: ${AWS_REGION} s3: - bucket: eatsfine-images - base-url: https://eatsfine-images.s3.ap-northeast-2.amazonaws.com + bucket: ${AWS_S3_BUCKET} + base-url: ${AWS_S3_BASE_URL} +api: + service-key: ${BIZ_API_KEY} jwt: secret: ${SECRET_KEY} From 9003e0ed0a5377d3160de080b6d0987f191714db Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:22:33 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[FEAT]:=20=EC=9D=B8=EC=A6=9D=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/payment/status/PaymentErrorStatus.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java index e6dddaa8..0d8305d0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/status/PaymentErrorStatus.java @@ -13,7 +13,8 @@ public enum PaymentErrorStatus implements BaseErrorCode { _PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "예약금이 유효하지 않습니다."), _PAYMENT_INVALID_AMOUNT(HttpStatus.BAD_REQUEST, "PAYMENT4002", "결제 금액이 일치하지 않습니다."), _PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4003", "결제 정보를 찾을 수 없습니다."), - _BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING4001", "예약을 찾을 수 없습니다."); + _BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING4001", "예약을 찾을 수 없습니다."), + _PAYMENT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PAYMENT4031", "해당 결제 정보에 접근할 권한이 없습니다."); private final HttpStatus httpStatus; private final String code; From 16762ccd081371ce0882a38cfdd1bf5c5d5d84a6 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:23:04 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[FEAT]:=20getPaymentDetail=20=EC=86=8C?= =?UTF-8?q?=EC=9C=A0=EA=B6=8C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/payment/service/PaymentService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java index 1a9bbf44..f5f9cb58 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java @@ -211,10 +211,14 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int } @Transactional(readOnly = true) - public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId) { + public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId, Long userId) { Payment payment = paymentRepository.findById(paymentId) .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); + if (!payment.getBooking().getUser().getId().equals(userId)) { + throw new PaymentException(PaymentErrorStatus._PAYMENT_ACCESS_DENIED); + } + return new PaymentResponseDTO.PaymentDetailResultDTO( payment.getId(), payment.getBooking().getId(), From c63af6455b77a10bd5e30dc5f4befbf0afd0f8a8 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:23:25 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[FEAT]:=20Security=20Context=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/controller/PaymentController.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java index f50e17a1..85ae4ced 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java @@ -3,11 +3,16 @@ import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; import com.eatsfine.eatsfine.domain.payment.service.PaymentService; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,6 +28,7 @@ public class PaymentController { private final PaymentService paymentService; + private final UserRepository userRepository; @Operation(summary = "결제 요청", description = "예약 ID를 받아 주문 ID를 생성하고 결제 정보를 초기화합니다.") @PostMapping("/request") @@ -49,18 +55,27 @@ public ApiResponse cancelPayment( @Operation(summary = "결제 내역 조회", description = "로그인한 사용자의 결제 내역을 조회합니다.") @GetMapping public ApiResponse getPaymentList( - @RequestParam(name = "userId", required = false, defaultValue = "1") Long userId, + @AuthenticationPrincipal User user, @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "10") Integer limit, @RequestParam(name = "status", required = false) String status) { - // TODO: userId는 추후 Security Context에서 가져오도록 수정 + Long userId = getUserId(user); return ApiResponse.onSuccess(paymentService.getPaymentList(userId, page, limit, status)); } @Operation(summary = "결제 상세 조회", description = "특정 결제 건의 상세 내역을 조회합니다.") @GetMapping("/{paymentId}") public ApiResponse getPaymentDetail( + @AuthenticationPrincipal User user, @PathVariable Long paymentId) { - return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId)); + Long userId = getUserId(user); + return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId, userId)); + } + + private Long getUserId(User user) { + String email = user.getUsername(); + return userRepository.findByEmail(email) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)) + .getId(); } } From 4fe69ca8c62fce88f184c4e6bd39e203952c4af5 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:24:18 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[FEAT]:=20PaymentControllerTest.java=20?= =?UTF-8?q?=EC=BB=B4=ED=8C=8C=EC=9D=BC=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PaymentControllerTest.java | 296 +++++++++--------- 1 file changed, 155 insertions(+), 141 deletions(-) diff --git a/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java index 717f021e..b6885dc6 100644 --- a/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java @@ -4,6 +4,9 @@ import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; import com.eatsfine.eatsfine.domain.payment.service.PaymentService; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.Role; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; @@ -27,6 +30,7 @@ import java.time.LocalDateTime; import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -43,145 +47,155 @@ @WithMockUser class PaymentControllerTest { - @Autowired - private MockMvc mockMvc; - - @MockBean - private PaymentService paymentService; - - @MockBean - private JwtAuthenticationFilter jwtAuthenticationFilter; - - @MockBean - private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - - @MockBean - private CustomAccessDeniedHandler customAccessDeniedHandler; - - @Autowired - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() throws ServletException, IOException { - doAnswer(invocation -> { - HttpServletRequest request = invocation.getArgument(0); - HttpServletResponse response = invocation.getArgument(1); - FilterChain chain = invocation.getArgument(2); - chain.doFilter(request, response); - return null; - }).when(jwtAuthenticationFilter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(FilterChain.class)); - } - - @Test - @DisplayName("결제 요청 성공") - void requestPayment_success() throws Exception { - // given - PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(1L); - PaymentResponseDTO.PaymentRequestResultDTO response = new PaymentResponseDTO.PaymentRequestResultDTO( - 1L, 1L, "order-id-123", BigDecimal.valueOf(10000), LocalDateTime.now()); - - given(paymentService.requestPayment(any(PaymentRequestDTO.RequestPaymentDTO.class))).willReturn(response); - - // when & then - mockMvc.perform(post("/api/v1/payments/request") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.isSuccess").value(true)) - .andExpect(jsonPath("$.result.paymentId").value(1L)) - .andExpect(jsonPath("$.result.orderId").value("order-id-123")); - } - - @Test - @DisplayName("결제 승인 성공") - void confirmPayment_success() throws Exception { - // given - PaymentConfirmDTO request = PaymentConfirmDTO.builder() - .paymentKey("payment-key-123") - .orderId("order-id-123") - .amount(BigDecimal.valueOf(10000)) - .build(); - - PaymentResponseDTO.PaymentSuccessResultDTO response = new PaymentResponseDTO.PaymentSuccessResultDTO( - 1L, "COMPLETED", LocalDateTime.now(), "order-id-123", BigDecimal.valueOf(10000), - "CARD", "TOSS", "http://receipt.url"); - - given(paymentService.confirmPayment(any(PaymentConfirmDTO.class))).willReturn(response); - - // when & then - mockMvc.perform(post("/api/v1/payments/confirm") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.isSuccess").value(true)) - .andExpect(jsonPath("$.result.status").value("COMPLETED")); - } - - @Test - @DisplayName("결제 취소 성공") - void cancelPayment_success() throws Exception { - // given - String paymentKey = "payment-key-123"; - PaymentRequestDTO.CancelPaymentDTO request = new PaymentRequestDTO.CancelPaymentDTO("단순 변심"); - PaymentResponseDTO.CancelPaymentResultDTO response = new PaymentResponseDTO.CancelPaymentResultDTO( - 1L, "order-id-123", paymentKey, "CANCELED", LocalDateTime.now()); - - given(paymentService.cancelPayment(eq(paymentKey), any(PaymentRequestDTO.CancelPaymentDTO.class))) - .willReturn(response); - - // when & then - mockMvc.perform(post("/api/v1/payments/{paymentKey}/cancel", paymentKey) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.isSuccess").value(true)) - .andExpect(jsonPath("$.result.status").value("CANCELED")); - } - - @Test - @DisplayName("결제 내역 조회 성공") - void getPaymentList_success() throws Exception { - // given - PaymentResponseDTO.PaginationDTO pagination = new PaymentResponseDTO.PaginationDTO(1, 1, 1L); - PaymentResponseDTO.PaymentHistoryResultDTO history = new PaymentResponseDTO.PaymentHistoryResultDTO( - 1L, 1L, "Store Name", BigDecimal.valueOf(10000), "DEPOSIT", "CARD", "TOSS", "COMPLETED", - LocalDateTime.now()); - PaymentResponseDTO.PaymentListResponseDTO response = new PaymentResponseDTO.PaymentListResponseDTO( - Collections.singletonList(history), pagination); - - given(paymentService.getPaymentList(any(Long.class), any(Integer.class), any(Integer.class), any())) - .willReturn(response); - - // when & then - mockMvc.perform(get("/api/v1/payments") - .param("page", "1") - .param("limit", "10") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.isSuccess").value(true)) - .andExpect(jsonPath("$.result.payments[0].storeName").value("Store Name")); - } - - @Test - @DisplayName("결제 상세 조회 성공") - void getPaymentDetail_success() throws Exception { - // given - Long paymentId = 1L; - PaymentResponseDTO.PaymentDetailResultDTO response = new PaymentResponseDTO.PaymentDetailResultDTO( - paymentId, 1L, "Store Name", "CARD", "TOSS", BigDecimal.valueOf(10000), "DEPOSIT", - "COMPLETED", LocalDateTime.now(), LocalDateTime.now(), "http://receipt.url", null); - - given(paymentService.getPaymentDetail(eq(paymentId))).willReturn(response); - - // when & then - mockMvc.perform(get("/api/v1/payments/{paymentId}", paymentId) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.isSuccess").value(true)) - .andExpect(jsonPath("$.result.paymentId").value(paymentId)); - } + @Autowired + private MockMvc mockMvc; + + @MockBean + private PaymentService paymentService; + + @MockBean + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @MockBean + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + @MockBean + private CustomAccessDeniedHandler customAccessDeniedHandler; + + @MockBean + private UserRepository userRepository; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() throws ServletException, IOException { + doAnswer(invocation -> { + HttpServletRequest request = invocation.getArgument(0); + HttpServletResponse response = invocation.getArgument(1); + FilterChain chain = invocation.getArgument(2); + chain.doFilter(request, response); + return null; + }).when(jwtAuthenticationFilter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(FilterChain.class)); + } + + @Test + @DisplayName("결제 요청 성공") + void requestPayment_success() throws Exception { + // given + PaymentRequestDTO.RequestPaymentDTO request = new PaymentRequestDTO.RequestPaymentDTO(1L); + PaymentResponseDTO.PaymentRequestResultDTO response = new PaymentResponseDTO.PaymentRequestResultDTO( + 1L, 1L, "order-id-123", BigDecimal.valueOf(10000), LocalDateTime.now()); + + given(paymentService.requestPayment(any(PaymentRequestDTO.RequestPaymentDTO.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/request") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.paymentId").value(1L)) + .andExpect(jsonPath("$.result.orderId").value("order-id-123")); + } + + @Test + @DisplayName("결제 승인 성공") + void confirmPayment_success() throws Exception { + // given + PaymentConfirmDTO request = PaymentConfirmDTO.builder() + .paymentKey("payment-key-123") + .orderId("order-id-123") + .amount(BigDecimal.valueOf(10000)) + .build(); + + PaymentResponseDTO.PaymentSuccessResultDTO response = new PaymentResponseDTO.PaymentSuccessResultDTO( + 1L, "COMPLETED", LocalDateTime.now(), "order-id-123", BigDecimal.valueOf(10000), + "CARD", "TOSS", "http://receipt.url"); + + given(paymentService.confirmPayment(any(PaymentConfirmDTO.class))).willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/confirm") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.status").value("COMPLETED")); + } + + @Test + @DisplayName("결제 취소 성공") + void cancelPayment_success() throws Exception { + // given + String paymentKey = "payment-key-123"; + PaymentRequestDTO.CancelPaymentDTO request = new PaymentRequestDTO.CancelPaymentDTO("단순 변심"); + PaymentResponseDTO.CancelPaymentResultDTO response = new PaymentResponseDTO.CancelPaymentResultDTO( + 1L, "order-id-123", paymentKey, "CANCELED", LocalDateTime.now()); + + given(paymentService.cancelPayment(eq(paymentKey), any(PaymentRequestDTO.CancelPaymentDTO.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/payments/{paymentKey}/cancel", paymentKey) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.status").value("CANCELED")); + } + + @Test + @DisplayName("결제 내역 조회 성공") + void getPaymentList_success() throws Exception { + // given + PaymentResponseDTO.PaginationDTO pagination = new PaymentResponseDTO.PaginationDTO(1, 1, 1L); + PaymentResponseDTO.PaymentHistoryResultDTO history = new PaymentResponseDTO.PaymentHistoryResultDTO( + 1L, 1L, "Store Name", BigDecimal.valueOf(10000), "DEPOSIT", "CARD", "TOSS", "COMPLETED", + LocalDateTime.now()); + PaymentResponseDTO.PaymentListResponseDTO response = new PaymentResponseDTO.PaymentListResponseDTO( + Collections.singletonList(history), pagination); + + User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build(); + given(userRepository.findByEmail("user")).willReturn(Optional.of(user)); + + given(paymentService.getPaymentList(eq(1L), any(Integer.class), any(Integer.class), any())) + .willReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/payments") + .param("page", "1") + .param("limit", "10") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.payments[0].storeName").value("Store Name")); + } + + @Test + @DisplayName("결제 상세 조회 성공") + void getPaymentDetail_success() throws Exception { + // given + Long paymentId = 1L; + PaymentResponseDTO.PaymentDetailResultDTO response = new PaymentResponseDTO.PaymentDetailResultDTO( + paymentId, 1L, "Store Name", "CARD", "TOSS", BigDecimal.valueOf(10000), "DEPOSIT", + "COMPLETED", LocalDateTime.now(), LocalDateTime.now(), "http://receipt.url", null); + + User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build(); + given(userRepository.findByEmail("user")).willReturn(Optional.of(user)); + + given(paymentService.getPaymentDetail(eq(paymentId), eq(1L))).willReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/payments/{paymentId}", paymentId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)) + .andExpect(jsonPath("$.result.paymentId").value(paymentId)); + } } From e6270c1e643999396177a549b1de7a446f01baa0 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:52:33 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[FEAT]:=20=EA=B2=B0=EC=A0=9C=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EB=B3=B4=EC=95=88=20=EA=B0=95?= =?UTF-8?q?=ED=99=94=20(N+1=20=EB=B0=A9=EC=A7=80,=20=EB=B3=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/repository/PaymentRepository.java | 13 +++++++++++-- .../domain/payment/service/PaymentService.java | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java index 91cf70b6..2260a0c5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java @@ -5,6 +5,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -13,7 +15,14 @@ public interface PaymentRepository extends JpaRepository { Optional findByPaymentKey(String paymentKey); - Page findAllByBooking_User_Id(Long userId, Pageable pageable); + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId", + countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId") + Page findAllByUserIdWithDetails(@Param("userId") Long userId, Pageable pageable); - Page findAllByBooking_User_IdAndPaymentStatus(Long userId, PaymentStatus status, Pageable pageable); + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId AND p.paymentStatus = :status", + countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId AND p.paymentStatus = :status") + Page findAllByUserIdAndStatusWithDetails(@Param("userId") Long userId, @Param("status") PaymentStatus status, Pageable pageable); + + @Query("SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store JOIN FETCH b.user WHERE p.id = :paymentId") + Optional findByIdWithDetails(@Param("paymentId") Long paymentId); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java index f5f9cb58..344d0b22 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java @@ -180,10 +180,10 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int // 유효하지 않은 status가 들어오면 BadRequest 예외 발생 throw new GeneralException(ErrorStatus._BAD_REQUEST); } - paymentPage = paymentRepository.findAllByBooking_User_IdAndPaymentStatus(userId, paymentStatus, + paymentPage = paymentRepository.findAllByUserIdAndStatusWithDetails(userId, paymentStatus, pageable); } else { - paymentPage = paymentRepository.findAllByBooking_User_Id(userId, pageable); + paymentPage = paymentRepository.findAllByUserIdWithDetails(userId, pageable); } List payments = paymentPage.getContent().stream() @@ -212,7 +212,7 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int @Transactional(readOnly = true) public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId, Long userId) { - Payment payment = paymentRepository.findById(paymentId) + Payment payment = paymentRepository.findByIdWithDetails(paymentId) .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); if (!payment.getBooking().getUser().getId().equals(userId)) { From c3bdf2350d5e06a7aaa386c88de3b1a69eec9590 Mon Sep 17 00:00:00 2001 From: CHAN <150508884+zerochani@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:50:36 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[REFACTOR]:=20=EA=B0=9C=EC=84=A0=EB=90=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BB=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20Spring=20Boot=203.4=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=EC=84=B1=20=ED=99=95=EB=B3=B4=20-=20Service=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=EC=9C=BC=EB=A1=9C=20User=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EA=B4=80=20=EB=B0=8F?= =?UTF-8?q?=20Controller=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20PaymentService=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8(=EC=84=B1=EA=B3=B5/=EC=8B=A4=ED=8C=A8/?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4)=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EA=B2=B0=EC=A0=9C=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C,=20=EC=98=88=EC=95=BD=EC=9E=90?= =?UTF-8?q?=EB=BF=90=EB=A7=8C=20=EC=95=84=EB=8B=88=EB=9D=BC=20=EC=83=81?= =?UTF-8?q?=EC=A0=90=20=EC=A3=BC=EC=9D=B8=EB=8F=84=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20-=20Sprin?= =?UTF-8?q?g=20Boot=203.4=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98:=20@MockBean=20->=20@MockitoBean=20=EA=B5=90=EC=B2=B4?= =?UTF-8?q?=20(Deprecated=20API=20=EB=8C=80=EC=9D=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/controller/PaymentController.java | 26 ++-- .../payment/repository/PaymentRepository.java | 26 ++-- .../payment/service/PaymentService.java | 55 +++++--- .../controller/HealthControllerTest.java | 8 +- .../controller/InquiryControllerTest.java | 10 +- .../controller/PaymentControllerTest.java | 28 +--- .../payment/service/PaymentServiceTest.java | 126 +++++++++++++++++- 7 files changed, 206 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java index 85ae4ced..579f59ec 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentController.java @@ -1,18 +1,17 @@ package com.eatsfine.eatsfine.domain.payment.controller; + import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO; import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; import com.eatsfine.eatsfine.domain.payment.service.PaymentService; -import com.eatsfine.eatsfine.domain.user.exception.UserException; -import com.eatsfine.eatsfine.domain.user.repository.UserRepository; -import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; + import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -28,7 +27,6 @@ public class PaymentController { private final PaymentService paymentService; - private final UserRepository userRepository; @Operation(summary = "결제 요청", description = "예약 ID를 받아 주문 ID를 생성하고 결제 정보를 초기화합니다.") @PostMapping("/request") @@ -55,27 +53,21 @@ public ApiResponse cancelPayment( @Operation(summary = "결제 내역 조회", description = "로그인한 사용자의 결제 내역을 조회합니다.") @GetMapping public ApiResponse getPaymentList( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserDetails user, @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "10") Integer limit, @RequestParam(name = "status", required = false) String status) { - Long userId = getUserId(user); - return ApiResponse.onSuccess(paymentService.getPaymentList(userId, page, limit, status)); + String email = user.getUsername(); + return ApiResponse.onSuccess(paymentService.getPaymentList(email, page, limit, status)); } @Operation(summary = "결제 상세 조회", description = "특정 결제 건의 상세 내역을 조회합니다.") @GetMapping("/{paymentId}") public ApiResponse getPaymentDetail( - @AuthenticationPrincipal User user, + @AuthenticationPrincipal UserDetails user, @PathVariable Long paymentId) { - Long userId = getUserId(user); - return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId, userId)); - } - - private Long getUserId(User user) { String email = user.getUsername(); - return userRepository.findByEmail(email) - .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)) - .getId(); + return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId, email)); } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java index 2260a0c5..53df1c9c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/repository/PaymentRepository.java @@ -11,18 +11,24 @@ import java.util.Optional; public interface PaymentRepository extends JpaRepository { - Optional findByOrderId(String orderId); + Optional findByOrderId(String orderId); - Optional findByPaymentKey(String paymentKey); + Optional findByPaymentKey(String paymentKey); - @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId", - countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId") - Page findAllByUserIdWithDetails(@Param("userId") Long userId, Pageable pageable); + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId", countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId") + Page findAllByUserIdWithDetails(@Param("userId") Long userId, Pageable pageable); - @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId AND p.paymentStatus = :status", - countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId AND p.paymentStatus = :status") - Page findAllByUserIdAndStatusWithDetails(@Param("userId") Long userId, @Param("status") PaymentStatus status, Pageable pageable); + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store WHERE b.user.id = :userId AND p.paymentStatus = :status", countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b WHERE b.user.id = :userId AND p.paymentStatus = :status") + Page findAllByUserIdAndStatusWithDetails(@Param("userId") Long userId, + @Param("status") PaymentStatus status, Pageable pageable); - @Query("SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store JOIN FETCH b.user WHERE p.id = :paymentId") - Optional findByIdWithDetails(@Param("paymentId") Long paymentId); + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store s WHERE s.owner.id = :userId", countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b JOIN b.store s WHERE s.owner.id = :userId") + Page findAllByOwnerIdWithDetails(@Param("userId") Long userId, Pageable pageable); + + @Query(value = "SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store s WHERE s.owner.id = :userId AND p.paymentStatus = :status", countQuery = "SELECT COUNT(p) FROM Payment p JOIN p.booking b JOIN b.store s WHERE s.owner.id = :userId AND p.paymentStatus = :status") + Page findAllByOwnerIdAndStatusWithDetails(@Param("userId") Long userId, + @Param("status") PaymentStatus status, Pageable pageable); + + @Query("SELECT p FROM Payment p JOIN FETCH p.booking b JOIN FETCH b.store JOIN FETCH b.user WHERE p.id = :paymentId") + Optional findByIdWithDetails(@Param("paymentId") Long paymentId); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java index 344d0b22..3cb68a28 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java @@ -12,11 +12,16 @@ import com.eatsfine.eatsfine.domain.payment.enums.PaymentProvider; import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; import com.eatsfine.eatsfine.domain.payment.enums.PaymentType; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.Role; import com.eatsfine.eatsfine.domain.payment.repository.PaymentRepository; import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; import com.eatsfine.eatsfine.domain.payment.status.PaymentErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -38,6 +43,7 @@ public class PaymentService { private final PaymentRepository paymentRepository; private final BookingRepository bookingRepository; + private final UserRepository userRepository; private final TossPaymentService tossPaymentService; @Transactional @@ -122,7 +128,6 @@ public PaymentResponseDTO.PaymentSuccessResultDTO confirmPayment(PaymentConfirmD log.info("Booking confirmed for OrderID: {}", dto.orderId()); } - log.info("Payment confirmed for OrderID: {}", dto.orderId()); return new PaymentResponseDTO.PaymentSuccessResultDTO( @@ -136,7 +141,6 @@ public PaymentResponseDTO.PaymentSuccessResultDTO confirmPayment(PaymentConfirmD payment.getReceiptUrl()); } - @Transactional(noRollbackFor = GeneralException.class) public PaymentResponseDTO.CancelPaymentResultDTO cancelPayment(String paymentKey, PaymentRequestDTO.CancelPaymentDTO dto) { @@ -162,8 +166,10 @@ public PaymentResponseDTO.CancelPaymentResultDTO cancelPayment(String paymentKey } @Transactional(readOnly = true) - public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Integer page, Integer limit, + public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(String email, Integer page, Integer limit, String status) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); // limit 기본값 처리 (만약 null이면 10) int size = (limit != null) ? limit : 10; // page 기본값 처리 (만약 null이면 1, 0보다 작으면 1로 보정). Spring Data는 0-based index이므로 -1 @@ -172,18 +178,32 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int Pageable pageable = PageRequest.of(pageNumber, size); Page paymentPage; - if (status != null && !status.isEmpty()) { - PaymentStatus paymentStatus; - try { - paymentStatus = PaymentStatus.valueOf(status.toUpperCase()); - } catch (IllegalArgumentException e) { - // 유효하지 않은 status가 들어오면 BadRequest 예외 발생 - throw new GeneralException(ErrorStatus._BAD_REQUEST); + if (user.getRole() == Role.ROLE_OWNER) { + if (status != null && !status.isEmpty()) { + PaymentStatus paymentStatus; + try { + paymentStatus = PaymentStatus.valueOf(status.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + paymentPage = paymentRepository.findAllByOwnerIdAndStatusWithDetails(user.getId(), + paymentStatus, pageable); + } else { + paymentPage = paymentRepository.findAllByOwnerIdWithDetails(user.getId(), pageable); } - paymentPage = paymentRepository.findAllByUserIdAndStatusWithDetails(userId, paymentStatus, - pageable); } else { - paymentPage = paymentRepository.findAllByUserIdWithDetails(userId, pageable); + if (status != null && !status.isEmpty()) { + PaymentStatus paymentStatus; + try { + paymentStatus = PaymentStatus.valueOf(status.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + paymentPage = paymentRepository.findAllByUserIdAndStatusWithDetails(user.getId(), + paymentStatus, pageable); + } else { + paymentPage = paymentRepository.findAllByUserIdWithDetails(user.getId(), pageable); + } } List payments = paymentPage.getContent().stream() @@ -211,11 +231,16 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int } @Transactional(readOnly = true) - public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId, Long userId) { + public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId, String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); Payment payment = paymentRepository.findByIdWithDetails(paymentId) .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); - if (!payment.getBooking().getUser().getId().equals(userId)) { + boolean isBooker = payment.getBooking().getUser().getId().equals(user.getId()); + boolean isStoreOwner = payment.getBooking().getStore().getOwner().getId().equals(user.getId()); + + if (!isBooker && !isStoreOwner) { throw new PaymentException(PaymentErrorStatus._PAYMENT_ACCESS_DENIED); } diff --git a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java index fc997345..1e1c17b0 100644 --- a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ActiveProfiles; @@ -41,13 +41,13 @@ class HealthControllerTest { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; - @MockBean + @MockitoBean private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - @MockBean + @MockitoBean private CustomAccessDeniedHandler customAccessDeniedHandler; @BeforeEach diff --git a/src/test/java/com/eatsfine/eatsfine/domain/inquiry/controller/InquiryControllerTest.java b/src/test/java/com/eatsfine/eatsfine/domain/inquiry/controller/InquiryControllerTest.java index 7d2c7eeb..dd3f028a 100644 --- a/src/test/java/com/eatsfine/eatsfine/domain/inquiry/controller/InquiryControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/domain/inquiry/controller/InquiryControllerTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; @@ -42,16 +42,16 @@ class InquiryControllerTest { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private InquiryService inquiryService; - @MockBean + @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; - @MockBean + @MockitoBean private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - @MockBean + @MockitoBean private CustomAccessDeniedHandler customAccessDeniedHandler; @Autowired diff --git a/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java index b6885dc6..df7a897c 100644 --- a/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentControllerTest.java @@ -4,9 +4,6 @@ import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; import com.eatsfine.eatsfine.domain.payment.service.PaymentService; -import com.eatsfine.eatsfine.domain.user.entity.User; -import com.eatsfine.eatsfine.domain.user.enums.Role; -import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; @@ -20,7 +17,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; @@ -29,8 +26,6 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Collections; -import java.util.List; -import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -50,21 +45,18 @@ class PaymentControllerTest { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private PaymentService paymentService; - @MockBean + @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; - @MockBean + @MockitoBean private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - @MockBean + @MockitoBean private CustomAccessDeniedHandler customAccessDeniedHandler; - @MockBean - private UserRepository userRepository; - @Autowired private ObjectMapper objectMapper; @@ -161,10 +153,7 @@ void getPaymentList_success() throws Exception { PaymentResponseDTO.PaymentListResponseDTO response = new PaymentResponseDTO.PaymentListResponseDTO( Collections.singletonList(history), pagination); - User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build(); - given(userRepository.findByEmail("user")).willReturn(Optional.of(user)); - - given(paymentService.getPaymentList(eq(1L), any(Integer.class), any(Integer.class), any())) + given(paymentService.getPaymentList(anyString(), any(Integer.class), any(Integer.class), any())) .willReturn(response); // when & then @@ -186,10 +175,7 @@ void getPaymentDetail_success() throws Exception { paymentId, 1L, "Store Name", "CARD", "TOSS", BigDecimal.valueOf(10000), "DEPOSIT", "COMPLETED", LocalDateTime.now(), LocalDateTime.now(), "http://receipt.url", null); - User user = User.builder().id(1L).email("user").role(Role.ROLE_CUSTOMER).build(); - given(userRepository.findByEmail("user")).willReturn(Optional.of(user)); - - given(paymentService.getPaymentDetail(eq(paymentId), eq(1L))).willReturn(response); + given(paymentService.getPaymentDetail(eq(paymentId), anyString())).willReturn(response); // when & then mockMvc.perform(get("/api/v1/payments/{paymentId}", paymentId) diff --git a/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java b/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java index b4027ca4..1f480d2f 100644 --- a/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java +++ b/src/test/java/com/eatsfine/eatsfine/domain/payment/service/PaymentServiceTest.java @@ -13,8 +13,13 @@ import com.eatsfine.eatsfine.domain.payment.enums.PaymentType; import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; import com.eatsfine.eatsfine.domain.payment.repository.PaymentRepository; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.store.entity.Store; import com.eatsfine.eatsfine.domain.payment.status.PaymentErrorStatus; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.Role; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,14 +28,18 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; @@ -51,6 +60,9 @@ class PaymentServiceTest { @Mock private TossPaymentService tossPaymentService; + @Mock + private UserRepository userRepository; + @Test @DisplayName("결제 요청 성공") void requestPayment_success() { @@ -215,4 +227,116 @@ void cancelPayment_success() { assertThat(response.status()).isEqualTo("REFUNDED"); assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.REFUNDED); } + + @Test + @DisplayName("결제 내역 조회 - 손님") + void getPaymentList_Customer_success() { + // given + User user = User.builder().id(1L).role(Role.ROLE_CUSTOMER).build(); + // Pageable pageable = PageRequest.of(0, 10); + + Payment payment = Payment.builder() + .id(1L) + .booking(Booking.builder().id(1L).store(Store.builder().storeName("Store").build()) + .build()) + .amount(BigDecimal.valueOf(10000)) + .paymentStatus(PaymentStatus.COMPLETED) + .paymentType(PaymentType.DEPOSIT) + .paymentMethod(PaymentMethod.SIMPLE_PAYMENT) + .paymentProvider(PaymentProvider.TOSS) + .approvedAt(LocalDateTime.now()) + .build(); + + Page paymentPage = new PageImpl<>(List.of(payment)); + + given(userRepository.findByEmail("customer")).willReturn(Optional.of(user)); + given(paymentRepository.findAllByUserIdWithDetails(eq(1L), any(Pageable.class))) + .willReturn(paymentPage); + + // when + PaymentResponseDTO.PaymentListResponseDTO response = paymentService.getPaymentList("customer", 1, 10, + null); + + // then + assertThat(response.payments()).hasSize(1); + assertThat(response.payments().get(0).storeName()).isEqualTo("Store"); + verify(paymentRepository, times(1)).findAllByUserIdWithDetails(eq(1L), any(Pageable.class)); + } + + @Test + @DisplayName("결제 내역 조회 - 사장님") + void getPaymentList_Owner_success() { + // given + User user = User.builder().id(2L).role(Role.ROLE_OWNER).build(); + // Pageable pageable = PageRequest.of(0, 10); + + Payment payment = Payment.builder() + .id(2L) + .booking(Booking.builder().id(1L) + .store(Store.builder().storeName("My Store").owner(user).build()) + .build()) + .amount(BigDecimal.valueOf(20000)) + .paymentStatus(PaymentStatus.COMPLETED) + .paymentType(PaymentType.DEPOSIT) + .paymentMethod(PaymentMethod.SIMPLE_PAYMENT) + .paymentProvider(PaymentProvider.TOSS) + .approvedAt(LocalDateTime.now()) + .build(); + + Page paymentPage = new PageImpl<>(List.of(payment)); + + given(userRepository.findByEmail("owner")).willReturn(Optional.of(user)); + given(paymentRepository.findAllByOwnerIdWithDetails(eq(2L), any(Pageable.class))) + .willReturn(paymentPage); + + // when + PaymentResponseDTO.PaymentListResponseDTO response = paymentService.getPaymentList("owner", 1, 10, + null); + + // then + assertThat(response.payments()).hasSize(1); + assertThat(response.payments().get(0).storeName()).isEqualTo("My Store"); + verify(paymentRepository, times(1)).findAllByOwnerIdWithDetails(eq(2L), any(Pageable.class)); + } + + @Test + @DisplayName("결제 상세 조회 실패 - 사용자 없음") + void getPaymentDetail_UserNotFound() { + // given + String email = "unknown@example.com"; + given(userRepository.findByEmail(email)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> paymentService.getPaymentDetail(1L, email)) + .isInstanceOf(UserException.class) + .extracting("code") + .isEqualTo(UserErrorStatus.MEMBER_NOT_FOUND); + } + + @Test + @DisplayName("결제 상세 조회 실패 - 권한 없음 (남의 결제 내역)") + void getPaymentDetail_AccessDenied() { + // given + String email = "hacker@example.com"; + User hacker = User.builder().id(999L).role(Role.ROLE_CUSTOMER).build(); + User owner = User.builder().id(1L).role(Role.ROLE_CUSTOMER).build(); + User storeOwner = User.builder().id(2L).role(Role.ROLE_OWNER).build(); + + Store store = Store.builder().id(1L).owner(storeOwner).build(); + Booking booking = Booking.builder().user(owner).store(store).build(); // owner(1L) is booker, + // storeOwner(2L) is owner + Payment payment = Payment.builder() + .id(1L) + .booking(booking) + .build(); + + given(userRepository.findByEmail(email)).willReturn(Optional.of(hacker)); + given(paymentRepository.findByIdWithDetails(1L)).willReturn(Optional.of(payment)); + + // when & then + assertThatThrownBy(() -> paymentService.getPaymentDetail(1L, email)) + .isInstanceOf(PaymentException.class) + .extracting("code") + .isEqualTo(PaymentErrorStatus._PAYMENT_ACCESS_DENIED); + } }