Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +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.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.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -49,18 +53,21 @@ public ApiResponse<PaymentResponseDTO.CancelPaymentResultDTO> cancelPayment(
@Operation(summary = "결제 내역 조회", description = "로그인한 사용자의 결제 내역을 조회합니다.")
@GetMapping
public ApiResponse<PaymentResponseDTO.PaymentListResponseDTO> getPaymentList(
@RequestParam(name = "userId", required = false, defaultValue = "1") Long userId,
@AuthenticationPrincipal UserDetails 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에서 가져오도록 수정
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<PaymentResponseDTO.PaymentDetailResultDTO> getPaymentDetail(
@AuthenticationPrincipal UserDetails user,
@PathVariable Long paymentId) {
return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId));
String email = user.getUsername();
return ApiResponse.onSuccess(paymentService.getPaymentDetail(paymentId, email));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@
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;

public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByOrderId(String orderId);
Optional<Payment> findByOrderId(String orderId);

Optional<Payment> findByPaymentKey(String paymentKey);
Optional<Payment> findByPaymentKey(String paymentKey);

Page<Payment> 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<Payment> findAllByUserIdWithDetails(@Param("userId") Long userId, Pageable pageable);

Page<Payment> 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<Payment> 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 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<Payment> 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<Payment> 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<Payment> findByIdWithDetails(@Param("paymentId") Long paymentId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +43,7 @@ public class PaymentService {

private final PaymentRepository paymentRepository;
private final BookingRepository bookingRepository;
private final UserRepository userRepository;
private final TossPaymentService tossPaymentService;

@Transactional
Expand Down Expand Up @@ -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(
Expand All @@ -136,7 +141,6 @@ public PaymentResponseDTO.PaymentSuccessResultDTO confirmPayment(PaymentConfirmD
payment.getReceiptUrl());
}


@Transactional(noRollbackFor = GeneralException.class)
public PaymentResponseDTO.CancelPaymentResultDTO cancelPayment(String paymentKey,
PaymentRequestDTO.CancelPaymentDTO dto) {
Expand All @@ -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
Expand All @@ -172,18 +178,32 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int
Pageable pageable = PageRequest.of(pageNumber, size);

Page<Payment> 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.findAllByBooking_User_IdAndPaymentStatus(userId, paymentStatus,
pageable);
} else {
paymentPage = paymentRepository.findAllByBooking_User_Id(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<PaymentResponseDTO.PaymentHistoryResultDTO> payments = paymentPage.getContent().stream()
Expand Down Expand Up @@ -211,10 +231,19 @@ public PaymentResponseDTO.PaymentListResponseDTO getPaymentList(Long userId, Int
}

@Transactional(readOnly = true)
public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId) {
Payment payment = paymentRepository.findById(paymentId)
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));

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);
}

return new PaymentResponseDTO.PaymentDetailResultDTO(
payment.getId(),
payment.getBooking().getId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 6 additions & 4 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +72 to 74
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

apijwt 섹션 사이에 빈 줄 누락.

다른 최상위 섹션(payment, cloud 등) 사이에는 빈 줄이 있지만, api(Line 72)와 jwt(Line 74) 사이에는 빈 줄이 없어 일관성이 떨어집니다.

제안
 api:
   service-key: ${BIZ_API_KEY}
+
 jwt:
   secret: ${SECRET_KEY}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
api:
service-key: ${BIZ_API_KEY}
jwt:
api:
service-key: ${BIZ_API_KEY}
jwt:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application-local.yml` around lines 72 - 74, The YAML is
missing a blank line between the top-level sections 'api' and 'jwt', causing
inconsistent spacing with other sections like 'payment' and 'cloud'; insert a
single empty line between the 'api' block (service-key: ${BIZ_API_KEY}) and the
'jwt' key in application-local.yml so all top-level sections have the same
blank-line separation for consistency.

secret: ${SECRET_KEY}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading