Skip to content
Merged
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
Expand Up @@ -67,7 +67,6 @@ public ReservationCreateResponse createReservation(ReservationCreateRequest requ

@Transactional
public void cancelReservation(Reservation reservation) {
reservation.cancel();
Long reservationId = reservation.getId();
seatReservationService.deleteSeatReservationByReservationId(reservationId);
ticketService.deleteTicketByReservationId(reservationId);
Expand Down
106 changes: 105 additions & 1 deletion src/main/java/com/sudo/railo/payment/application/PaymentService.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package com.sudo.railo.payment.application;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.sudo.railo.booking.application.ReservationApplicationService;
import com.sudo.railo.booking.application.TicketService;
import com.sudo.railo.booking.domain.Reservation;
import com.sudo.railo.booking.exception.BookingError;
import com.sudo.railo.booking.infrastructure.reservation.ReservationRepository;
import com.sudo.railo.global.exception.error.BusinessException;
import com.sudo.railo.member.domain.Member;
import com.sudo.railo.member.exception.MemberError;
import com.sudo.railo.member.infrastructure.MemberRepository;
import com.sudo.railo.payment.application.dto.PaymentInfo;
import com.sudo.railo.payment.application.dto.projection.PaymentProjection;
import com.sudo.railo.payment.application.dto.request.PaymentProcessAccountRequest;
import com.sudo.railo.payment.application.dto.request.PaymentProcessCardRequest;
import com.sudo.railo.payment.application.dto.request.PaymentProcessRequest;
import com.sudo.railo.payment.application.dto.response.PaymentCancelResponse;
import com.sudo.railo.payment.application.dto.response.PaymentHistoryResponse;
import com.sudo.railo.payment.application.dto.response.PaymentProcessResponse;
import com.sudo.railo.payment.domain.Payment;
import com.sudo.railo.payment.domain.status.PaymentStatus;
Expand All @@ -37,6 +43,7 @@ public class PaymentService {
private final MemberRepository memberRepository;
private final PaymentRepository paymentRepository;
private final PaymentKeyGenerator paymentKeyGenerator;
private final ReservationApplicationService reservationApplicationService;
private final TicketService ticketService;

/**
Expand Down Expand Up @@ -146,7 +153,6 @@ private void executePaymentApproval(Payment payment, Reservation reservation) {

private void markReservationAsPaid(Reservation reservation) {
reservation.approve();
reservationRepository.save(reservation);

log.info("예약 결제 완료 처리: reservationId={}", reservation.getId());
}
Expand All @@ -164,4 +170,102 @@ private void generateTicket(Reservation reservation) {
.forEach(seatInfoProjection -> ticketService.createTicket(
reservation, seatInfoProjection.getSeat(), seatInfoProjection.getPassengerType()));
}

@Transactional
public PaymentCancelResponse cancelPayment(String memberNo, String paymentKey) {
Payment payment = findPayment(memberNo, paymentKey);

// 결제 취소 처리
payment.cancel("사용자 요청에 의한 취소");

markReservationAsCancelled(payment.getReservation());

log.info("결제 취소 완료: paymentKey={}, reservationId={}", paymentKey, payment.getReservation().getId());

// 즉각 환불 처리 (임시)
refundPayment(payment, payment.getReservation());

return PaymentCancelResponse.from(payment);
}

private Payment findPayment(String memberNo, String paymentKey) {
Member member = memberRepository.findByMemberNo(memberNo)
.orElseThrow(() -> new BusinessException(MemberError.USER_NOT_FOUND));

Payment payment = paymentRepository.findByPaymentKey(paymentKey)
.orElseThrow(() -> new BusinessException(PaymentError.PAYMENT_NOT_FOUND));

validatePaymentCancellableCondition(member, payment);

return payment;
}

private void validatePaymentCancellableCondition(Member member, Payment payment) {
// 결제 소유자 확인
if (!payment.getMember().getId().equals(member.getId())) {
throw new BusinessException(PaymentError.PAYMENT_ACCESS_DENIED);
}

// 결제 취소 가능 여부 확인
if (!payment.canBeCancelled()) {
throw new BusinessException(PaymentError.PAYMENT_NOT_CANCELLABLE);
}

// 예약 취소 가능 여부 확인
if (!payment.getReservation().canBeCancelled()) {
throw new BusinessException(BookingError.RESERVATION_DELETE_FAILED);
}
Comment thread
EndlessMilkyway marked this conversation as resolved.
}

private void markReservationAsCancelled(Reservation reservation) {
reservation.cancel();
reservationApplicationService.cancelReservation(reservation);

log.info("예약 취소 처리: reservationId={}", reservation.getId());
}
Comment thread
EndlessMilkyway marked this conversation as resolved.

/**
* 환불 처리
* @param payment {@link Payment} 객체
*/
public void refundPayment(Payment payment, Reservation reservation) {
// 해당 로직은 외부 엔드포인트가 존재하지 않고 추후 엔드포인트가 생긴다고 하더라도
// 신뢰할 수 있는 PG사에서 환불 완료된 결제에 대해서만 호출할 예정이기 때문에 검증 과정이 필요 없습니다.

// 즉각 환불 처리 로직 (임시)
if (payment.canBeRefunded()) {
payment.refund();
}

markReservationAsRefunded(reservation);

log.info("환불 처리 완료: paymentKey={}", payment.getPaymentKey());
}

private void markReservationAsRefunded(Reservation reservation) {
reservation.refund();

log.info("예약 환불 처리: reservationId={}", reservation.getId());
}

/**
* 결제 내역 조회
* @param memberNo 회원번호
* @return {@code List<PaymentHistoryResponse>}
*/
@Transactional(readOnly = true)
public List<PaymentHistoryResponse> getPaymentHistory(String memberNo) {
Member member = memberRepository.findByMemberNo(memberNo)
.orElseThrow(() -> new BusinessException(MemberError.USER_NOT_FOUND));

List<PaymentProjection> paymentProjections = paymentRepository.findPaymentHistoryByMemberId(member.getId());

return paymentProjections.stream()
.map(paymentProjection -> new PaymentHistoryResponse(
paymentProjection.getPaymentId(), paymentProjection.getPaymentKey(),
paymentProjection.getReservationCode(), paymentProjection.getAmount(),
paymentProjection.getPaymentMethod(), paymentProjection.getPaymentStatus(),
paymentProjection.getPaidAt(), paymentProjection.getCancelledAt(), paymentProjection.getRefundedAt()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.sudo.railo.payment.application.dto.projection;

import java.math.BigDecimal;
import java.time.LocalDateTime;

import com.querydsl.core.annotations.QueryProjection;
import com.sudo.railo.payment.domain.status.PaymentStatus;
import com.sudo.railo.payment.domain.type.PaymentMethod;

import lombok.Getter;

@Getter
public class PaymentProjection {

private final Long paymentId;
private final String paymentKey;
private final String reservationCode;
private final BigDecimal amount;
private final PaymentMethod paymentMethod;
private final PaymentStatus paymentStatus;
private final LocalDateTime paidAt;
private final LocalDateTime cancelledAt;
private final LocalDateTime refundedAt;

@QueryProjection
public PaymentProjection(Long paymentId, String paymentKey, String reservationCode,
BigDecimal amount, PaymentMethod paymentMethod, PaymentStatus paymentStatus,
LocalDateTime paidAt, LocalDateTime cancelledAt, LocalDateTime refundedAt) {
this.paymentId = paymentId;
this.paymentKey = paymentKey;
this.reservationCode = reservationCode;
this.amount = amount;
this.paymentMethod = paymentMethod;
this.paymentStatus = paymentStatus;
this.paidAt = paidAt;
this.cancelledAt = cancelledAt;
this.refundedAt = refundedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.sudo.railo.payment.domain.status.PaymentStatus;

@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
public interface PaymentRepository extends JpaRepository<Payment, Long>, PaymentRepositoryCustom {

/**
* 결제 키로 결제 정보 조회
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.sudo.railo.payment.infrastructure;

import java.util.List;

import com.sudo.railo.payment.application.dto.projection.PaymentProjection;

public interface PaymentRepositoryCustom {

/**
* 회원의 결제 히스토리를 프로젝션으로 조회 (가장 효율적)
*
* @param memberId 회원 ID
* @return PaymentHistoryResponse 리스트 (필요한 필드만 조회)
*/
List<PaymentProjection> findPaymentHistoryByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.sudo.railo.payment.infrastructure;

import static com.sudo.railo.booking.domain.QReservation.*;
import static com.sudo.railo.member.domain.QMember.*;
import static com.sudo.railo.payment.domain.QPayment.*;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.sudo.railo.payment.application.dto.projection.PaymentProjection;
import com.sudo.railo.payment.application.dto.projection.QPaymentProjection;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class PaymentRepositoryCustomImpl implements PaymentRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public List<PaymentProjection> findPaymentHistoryByMemberId(Long memberId) {
return queryFactory
.select(new QPaymentProjection(
payment.id,
payment.paymentKey,
reservation.reservationCode,
payment.amount,
payment.paymentMethod,
payment.paymentStatus,
payment.paidAt,
payment.cancelledAt,
payment.refundedAt))
.from(payment)
.join(payment.member, member)
.join(payment.reservation, reservation)
.where(payment.member.id.eq(memberId))
.orderBy(payment.paidAt.desc()).fetch();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.sudo.railo.payment.presentation;

import java.util.List;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -11,6 +15,8 @@
import com.sudo.railo.payment.application.PaymentService;
import com.sudo.railo.payment.application.dto.request.PaymentProcessAccountRequest;
import com.sudo.railo.payment.application.dto.request.PaymentProcessCardRequest;
import com.sudo.railo.payment.application.dto.response.PaymentCancelResponse;
import com.sudo.railo.payment.application.dto.response.PaymentHistoryResponse;
import com.sudo.railo.payment.application.dto.response.PaymentProcessResponse;
import com.sudo.railo.payment.success.PaymentSuccess;

Expand Down Expand Up @@ -51,4 +57,25 @@ public SuccessResponse<PaymentProcessResponse> processPaymentViaBankAccount(

return SuccessResponse.of(PaymentSuccess.PAYMENT_PROCESS_SUCCESS, response);
}

@Operation(summary = "결제 취소", description = "완료된 결제를 취소 및 환불처리 합니다.")
@PostMapping("/{paymentKey}/cancel")
public SuccessResponse<PaymentCancelResponse> cancelPayment(@PathVariable String paymentKey,
@AuthenticationPrincipal UserDetails userDetails) {
String memberNo = userDetails.getUsername();
PaymentCancelResponse response = paymentService.cancelPayment(memberNo, paymentKey);

return SuccessResponse.of(PaymentSuccess.PAYMENT_CANCEL_SUCCESS, response);
}

@Operation(summary = "결제 내역 조회", description = "사용자의 결제 내역을 조회합니다.")
@GetMapping
public SuccessResponse<List<PaymentHistoryResponse>> getPaymentHistory(
@AuthenticationPrincipal UserDetails userDetails) {

String memberNo = userDetails.getUsername();
List<PaymentHistoryResponse> response = paymentService.getPaymentHistory(memberNo);

return SuccessResponse.of(PaymentSuccess.PAYMENT_HISTORY_SUCCESS, response);
}
}
Loading