diff --git a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java index 79e5b22..339642d 100644 --- a/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java +++ b/src/main/java/com/permitseoul/permitserver/domain/payment/api/service/PaymentService.java @@ -58,10 +58,14 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.List; +import static com.permitseoul.permitserver.global.util.LogFormUtil.maskPaymentKey; import static net.logstash.logback.argument.StructuredArguments.keyValue; @Slf4j @@ -130,14 +134,11 @@ public PaymentConfirmResponse getPaymentConfirm(final long userId, final TossPaymentResponse tossPaymentResponse = getTossPaymentConfirm(authorizationHeader, paymentKey, reservation.getOrderId(), reservation.getTotalAmount()); - log.info("토스 결제 승인 완료", - (Object[]) LogFormUtil.paymentLog( - userId, - tossPaymentResponse.orderId(), - tossPaymentResponse.paymentKey(), - reservation.getReservationId(), - tossPaymentResponse.totalAmount() - ) + log.info("[Payment] 토스 결제 승인 완료 - orderId={}, paymentKey={}, reservationId={}, amount={}", + tossPaymentResponse.orderId(), + LogFormUtil.maskPaymentKey(tossPaymentResponse.paymentKey()), + reservation.getReservationId(), + tossPaymentResponse.totalAmount() ); updateReservationStatusAndTossPaymentResponseTime(reservation.getReservationId(), ReservationStatus.PAYMENT_SUCCESS); @@ -202,16 +203,16 @@ public PaymentConfirmResponse getPaymentConfirm(final long userId, deleteReservationSessionByOrderId(orderId); throw handleFeignException(e, orderId, userId); - } catch (AlgorithmException e) { //todo: 결제는 됐는데, 티켓 발급 과정에서 실패했으므로, 따로 알림 구축해놔야될듯 - logPaymentSuccessButTicketIssueFailed(userId, reservationSessionKey, orderId, totalAmount, paymentKey, reservation.getReservationId()); + } catch (AlgorithmException e) { + logPaymentSuccessButTicketIssueFailed(orderId, totalAmount, paymentKey, reservation.getReservationId()); throw new TicketAlgorithmException(ErrorCode.INTERNAL_TICKET_ALGORITHM_ERROR); - } catch (IllegalEnumTransitionException e) { //todo: 결제는 됐는데, 티켓 발급 과정에서 실패했으므로, 따로 알림 구축해놔야될듯 - logPaymentSuccessButTicketIssueFailed(userId, reservationSessionKey, orderId, totalAmount, paymentKey, reservation.getReservationId()); + } catch (IllegalEnumTransitionException e) { + logPaymentSuccessButTicketIssueFailed(orderId, totalAmount, paymentKey, reservation.getReservationId()); throw new ReservationIllegalException(ErrorCode.INTERNAL_TRANSITION_ENUM_ERROR); - } catch (ReservationSessionNotFoundAfterPaymentSuccessException e) { //결제는 됐는데, 티켓 발급 과정에서 실패했으므로, 따로 알림 구축해놔야될듯 - logPaymentSuccessButTicketIssueFailed(userId, reservationSessionKey, orderId, totalAmount, paymentKey, reservation.getReservationId()); + } catch (ReservationSessionNotFoundAfterPaymentSuccessException e) { + logPaymentSuccessButTicketIssueFailed(orderId, totalAmount, paymentKey, reservation.getReservationId()); throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_RESERVATION_SESSION_AFTER_PAYMENT_SUCCESS); } } @@ -219,6 +220,8 @@ public PaymentConfirmResponse getPaymentConfirm(final long userId, public void cancelPayment(final long userId, final String orderId) { try { final Payment payment = paymentRetriever.findPaymentByOrderId(orderId); + validateCancelAvailablePeriod(payment.getEventId()); + final List ticketList = ticketRetriever.findAllTicketsByOrderIdAndUserId(payment.getOrderId(), userId); validateTicketStatusForCancel(ticketList); @@ -245,6 +248,8 @@ public void cancelPayment(final long userId, final String orderId) { } catch (PaymentNotFoundException e) { throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_PAYMENT); + } catch (EventNotfoundException e) { + throw new NotFoundPaymentException(ErrorCode.NOT_FOUND_EVENT); } catch(FeignException e) { throw handleFeignException(e, orderId, userId); } catch (TicketNotFoundException e) { @@ -263,6 +268,23 @@ public void cancelPayment(final long userId, final String orderId) { } } + private void validateCancelAvailablePeriod(final long eventId) { + final Event event = eventRetriever.findEventById(eventId); + + final LocalDate eventDate = event.getStartAt().toLocalDate(); + final LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul")); + + // 오늘과 행사일 사이의 일수 계산 + long daysUntilEvent = ChronoUnit.DAYS.between(today, eventDate); + + // 3일 전까지만 환불 가능 + if (daysUntilEvent < 3) { + log.warn("[Payment Cancel] 취소 기한 초과 - eventId={}, eventDate={}, today={}, daysUntilEvent={}", + eventId, eventDate, today, daysUntilEvent); + throw new PaymentBadRequestException(ErrorCode.BAD_REQUEST_CANCEL_PERIOD_EXPIRED); + } + } + private void deleteReservationSessionByOrderId(final String orderId) { try { reservationSessionRemover.deleteByOrderId(orderId); @@ -302,18 +324,15 @@ private void handleFailedTossPayment(final Reservation reservation, } } - private void logPaymentSuccessButTicketIssueFailed( final long userId, - final String sessionKey, - final String orderId, - final BigDecimal totalAmount, - final String paymentKey, - final long reservationId) { - log.error("토스 결제 승인 완료 -> 티켓 발급 실패", - keyValue(Constants.USER_ID, userId), - keyValue(Constants.ORDER_ID, orderId), - keyValue(Constants.PAYMENT_KEY, LogFormUtil.maskPaymentKey(paymentKey)), - keyValue(Constants.RESERVATION_ID, reservationId), - keyValue(Constants.TOTAL_AMOUNT, totalAmount) + private void logPaymentSuccessButTicketIssueFailed(final String orderId, + final BigDecimal totalAmount, + final String paymentKey, + final long reservationId) { + log.error("[Payment] 토스 결제 승인 완료 -> 티켓 발급 실패 - orderId={}, paymentKey={}, reservationId={}, amount={}", + orderId, + LogFormUtil.maskPaymentKey(paymentKey), + reservationId, + totalAmount ); } @@ -341,7 +360,7 @@ private void logRollbackFailed(final long userId, final BigDecimal totalAmount, final String paymentKey) { log.error("[결제 승인 API - redis Rollback Failed] userId: {}, sessionKey: {}, orderId: {}, totalAmount: {}, paymentKey: {}", - userId, sessionKey, orderId, totalAmount, LogFormUtil.maskPaymentKey(paymentKey)); + userId, sessionKey, orderId, totalAmount, maskPaymentKey(paymentKey)); } private void updateReservationStatusAndTossPaymentResponseTime(final long reservationId, final ReservationStatus status) { diff --git a/src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java b/src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java index 77adc34..014ac7f 100644 --- a/src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java +++ b/src/main/java/com/permitseoul/permitserver/global/response/code/ErrorCode.java @@ -30,6 +30,7 @@ public enum ErrorCode implements ApiCode { BAD_REQUEST_MISMATCH_TICKET_TYPE_ROUND(HttpStatus.BAD_REQUEST, 40016, "ticketType의 roundId와 다른 ticketRoundId 입니다."), BAD_REQUEST_MISMATCH_LIST_SIZE(HttpStatus.BAD_REQUEST, 40017, "list의 길이가 다릅니다."), BAD_REQUEST_REDIS_TICKET_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, 40018, "redis ticket tpye mismatch 에러입니다. "), + BAD_REQUEST_CANCEL_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, 40019, "환불 가능 기간이 아닙니다. 환불은 행사 시작일 기준 3일전까지만 가능합니다.(환불 정책 참고)."), diff --git a/src/main/java/com/permitseoul/permitserver/global/util/LogFormUtil.java b/src/main/java/com/permitseoul/permitserver/global/util/LogFormUtil.java index 856ab63..89fad4d 100644 --- a/src/main/java/com/permitseoul/permitserver/global/util/LogFormUtil.java +++ b/src/main/java/com/permitseoul/permitserver/global/util/LogFormUtil.java @@ -1,14 +1,6 @@ package com.permitseoul.permitserver.global.util; -import com.permitseoul.permitserver.global.Constants; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; -import net.logstash.logback.argument.StructuredArgument; - -import java.math.BigDecimal; - -import static net.logstash.logback.argument.StructuredArguments.keyValue; @UtilityClass public final class LogFormUtil { @@ -22,22 +14,6 @@ public final class LogFormUtil { private static final int PAYMENT_KEY_LONG_PREFIX = 4; private static final int PAYMENT_KEY_LONG_SUFFIX = 5; - public static StructuredArgument[] paymentLog( - final long userId, - final String orderId, - final String paymentKey, - final long reservationId, - final BigDecimal totalAmount - ) { - return new StructuredArgument[] { - keyValue(Constants.USER_ID, userId), - keyValue(Constants.ORDER_ID, orderId), - keyValue(Constants.PAYMENT_KEY, maskPaymentKey(paymentKey)), - keyValue(Constants.RESERVATION_ID, reservationId), - keyValue(Constants.TOTAL_AMOUNT, totalAmount) - }; - } - public static String maskPaymentKey(final String paymentKey) { if (paymentKey == null || paymentKey.isBlank()) { return paymentKey;