diff --git a/src/main/java/in/koreatech/payment/model/entity/PaymentIdempotencyKey.java b/src/main/java/in/koreatech/koin/domain/order/model/PaymentIdempotencyKey.java similarity index 97% rename from src/main/java/in/koreatech/payment/model/entity/PaymentIdempotencyKey.java rename to src/main/java/in/koreatech/koin/domain/order/model/PaymentIdempotencyKey.java index 5b4963a..4e5bf64 100644 --- a/src/main/java/in/koreatech/payment/model/entity/PaymentIdempotencyKey.java +++ b/src/main/java/in/koreatech/koin/domain/order/model/PaymentIdempotencyKey.java @@ -1,4 +1,4 @@ -package in.koreatech.payment.model.entity; +package in.koreatech.koin.domain.order.model; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; diff --git a/src/main/java/in/koreatech/payment/repository/PaymentIdempotencyKeyRepository.java b/src/main/java/in/koreatech/koin/domain/order/repository/PaymentIdempotencyKeyRepository.java similarity index 73% rename from src/main/java/in/koreatech/payment/repository/PaymentIdempotencyKeyRepository.java rename to src/main/java/in/koreatech/koin/domain/order/repository/PaymentIdempotencyKeyRepository.java index 8c162a5..bfe9c3d 100644 --- a/src/main/java/in/koreatech/payment/repository/PaymentIdempotencyKeyRepository.java +++ b/src/main/java/in/koreatech/koin/domain/order/repository/PaymentIdempotencyKeyRepository.java @@ -1,10 +1,10 @@ -package in.koreatech.payment.repository; +package in.koreatech.koin.domain.order.repository; import java.util.Optional; import org.springframework.data.repository.Repository; -import in.koreatech.payment.model.entity.PaymentIdempotencyKey; +import in.koreatech.koin.domain.order.model.PaymentIdempotencyKey; public interface PaymentIdempotencyKeyRepository extends Repository { diff --git a/src/main/java/in/koreatech/payment/event/TossPaymentRollBackEvent.java b/src/main/java/in/koreatech/payment/event/TossPaymentRollBackEvent.java new file mode 100644 index 0000000..835c0ca --- /dev/null +++ b/src/main/java/in/koreatech/payment/event/TossPaymentRollBackEvent.java @@ -0,0 +1,14 @@ +package in.koreatech.payment.event; + +import in.koreatech.payment.client.dto.response.TossPaymentConfirmResponse; +import in.koreatech.payment.model.redis.TemporaryPayment; + +public record TossPaymentRollBackEvent( + String paymentKey, + TemporaryPayment temporaryPayment, + TossPaymentConfirmResponse tossPaymentConfirmResponse +) { + public static TossPaymentRollBackEvent from(String paymentKey, TemporaryPayment temporaryPayment, TossPaymentConfirmResponse tossPaymentConfirmResponse) { + return new TossPaymentRollBackEvent(paymentKey, temporaryPayment, tossPaymentConfirmResponse); + } +} diff --git a/src/main/java/in/koreatech/payment/model/redis/TemporaryPayment.java b/src/main/java/in/koreatech/payment/model/redis/TemporaryPayment.java index 97b07ef..b0969a3 100644 --- a/src/main/java/in/koreatech/payment/model/redis/TemporaryPayment.java +++ b/src/main/java/in/koreatech/payment/model/redis/TemporaryPayment.java @@ -35,7 +35,7 @@ public class TemporaryPayment { private String phoneNumber; - private in.koreatech.koin.domain.order.model.OrderType orderType; + private OrderType orderType; private String address; diff --git a/src/main/java/in/koreatech/payment/service/PaymentRollBackService.java b/src/main/java/in/koreatech/payment/service/PaymentRollBackService.java new file mode 100644 index 0000000..0b42951 --- /dev/null +++ b/src/main/java/in/koreatech/payment/service/PaymentRollBackService.java @@ -0,0 +1,7 @@ +package in.koreatech.payment.service; + +import in.koreatech.payment.event.TossPaymentRollBackEvent; + +public interface PaymentRollBackService { + void paymentRollback(TossPaymentRollBackEvent event); +} diff --git a/src/main/java/in/koreatech/payment/service/TossPaymentRollBackService.java b/src/main/java/in/koreatech/payment/service/TossPaymentRollBackService.java new file mode 100644 index 0000000..783dfcc --- /dev/null +++ b/src/main/java/in/koreatech/payment/service/TossPaymentRollBackService.java @@ -0,0 +1,93 @@ +package in.koreatech.payment.service; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; +import static org.springframework.transaction.event.TransactionPhase.AFTER_ROLLBACK; + +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import in.koreatech.koin.domain.order.cart.repository.CartRepository; +import in.koreatech.koin.domain.order.model.Order; +import in.koreatech.koin.domain.order.model.Payment; +import in.koreatech.koin.domain.order.model.PaymentCancel; +import in.koreatech.koin.domain.order.model.PaymentIdempotencyKey; +import in.koreatech.koin.domain.order.repository.OrderRepository; +import in.koreatech.koin.domain.order.repository.PaymentCancelRepository; +import in.koreatech.koin.domain.order.repository.PaymentIdempotencyKeyRepository; +import in.koreatech.koin.domain.order.repository.PaymentRepository; +import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; +import in.koreatech.koin.domain.order.shop.repository.OrderableShopRepository; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.payment.client.TossPaymentClient; +import in.koreatech.payment.client.dto.response.PaymentCancelResponse; +import in.koreatech.payment.event.TossPaymentRollBackEvent; +import in.koreatech.payment.model.redis.TemporaryPayment; +import in.koreatech.payment.repository.redis.TemporaryPaymentRedisRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TossPaymentRollBackService implements PaymentRollBackService { + + private final TossPaymentClient tossPaymentClient; + private final PaymentIdempotencyKeyRepository paymentIdempotencyKeyRepository; + private final UserRepository userRepository; + private final OrderableShopRepository orderableShopRepository; + private final OrderRepository orderRepository; + private final PaymentRepository paymentRepository; + private final TemporaryPaymentRedisRepository temporaryPaymentRedisRepository; + private final CartRepository cartRepository; + private final PaymentCancelRepository paymentCancelRepository; + + private static final String PAYMENT_CANCEL_REASON = "코인 서버 오류로 인한 결제 취소"; + + @TransactionalEventListener(phase = AFTER_ROLLBACK) + @Transactional(propagation = REQUIRES_NEW, transactionManager = "koinTransactionManager") + public void paymentRollback(TossPaymentRollBackEvent event) { + TemporaryPayment temporaryPayment = event.temporaryPayment(); + User user = userRepository.getById(temporaryPayment.getUserId()); + PaymentIdempotencyKey paymentIdempotencyKey = paymentIdempotencyKeyRepository + .findByUserId(user.getId()) + .map(idempotencyKey -> { + if (idempotencyKey.isOlderThanExpireDays()) { + idempotencyKey.updateIdempotencyKey(UUID.randomUUID().toString()); + } + return idempotencyKey; + }) + .orElseGet(() -> paymentIdempotencyKeyRepository.save( + PaymentIdempotencyKey.builder() + .userId(user.getId()) + .idempotencyKey(UUID.randomUUID().toString()) + .build() + )); + + PaymentCancelResponse response = tossPaymentClient.requestCancel(event.paymentKey(), + PAYMENT_CANCEL_REASON, paymentIdempotencyKey.getIdempotencyKey()); + + try { + OrderableShop orderableShop = orderableShopRepository.getById(temporaryPayment.getOrderableShopId()); + Order order = temporaryPayment.toOrder(user, orderableShop); + orderRepository.save(order); + + Payment payment = event.tossPaymentConfirmResponse().toEntity(order); + payment.cancel(); + paymentRepository.save(payment); + + List paymentCancels = response.getPaymentCancels(payment); + paymentCancelRepository.saveAll(paymentCancels); + + temporaryPaymentRedisRepository.deleteById(order.getId()); + cartRepository.deleteByUserId(user.getId()); + } catch (Exception e) { + log.error("결제 취소 과정에서 오류 발생 - paymentId: {}, userId: {}, orderId: {}", event.paymentKey(), + temporaryPayment.getUserId(), temporaryPayment.getOrderId()); + } + } +} diff --git a/src/main/java/in/koreatech/payment/service/TossService.java b/src/main/java/in/koreatech/payment/service/TossService.java index 173d9c4..d5e032d 100644 --- a/src/main/java/in/koreatech/payment/service/TossService.java +++ b/src/main/java/in/koreatech/payment/service/TossService.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.UUID; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,14 +29,15 @@ import in.koreatech.payment.dto.request.TemporaryDeliveryPaymentSaveRequest; import in.koreatech.payment.dto.request.TemporaryTakeoutPaymentSaveRequest; import in.koreatech.payment.dto.response.PaymentConfirmResponse; +import in.koreatech.payment.event.TossPaymentRollBackEvent; import in.koreatech.payment.exception.OrderPriceMismatchException; import in.koreatech.payment.exception.PaymentAlreadyCanceledException; import in.koreatech.payment.exception.PaymentCancelException; import in.koreatech.payment.exception.PaymentConfirmException; import in.koreatech.payment.model.domain.TemporaryMenuItems; -import in.koreatech.payment.model.entity.PaymentIdempotencyKey; +import in.koreatech.koin.domain.order.model.PaymentIdempotencyKey; import in.koreatech.payment.model.redis.TemporaryPayment; -import in.koreatech.payment.repository.PaymentIdempotencyKeyRepository; +import in.koreatech.koin.domain.order.repository.PaymentIdempotencyKeyRepository; import in.koreatech.payment.repository.redis.TemporaryPaymentRedisRepository; import in.koreatech.payment.util.OrderIdGenerator; import in.koreatech.payment.util.TemporaryMenuItemConverter; @@ -58,6 +60,7 @@ public class TossService implements PaymentService { private final OrderableShopRepository orderableShopRepository; private final OrderRepository orderRepository; private final OrderMenuRepository orderMenuRepository; + private final ApplicationEventPublisher applicationEventPublisher; @Transactional public String createTemporaryDeliveryPayment(String accessToken, TemporaryDeliveryPaymentSaveRequest request) { @@ -147,6 +150,9 @@ public PaymentConfirmResponse confirmPayment(String accessToken, String paymentK throw PaymentConfirmException.withDetail("paymentStatus : " + tossPaymentResponse.status()); } + applicationEventPublisher.publishEvent( + TossPaymentRollBackEvent.from(paymentKey, temporaryPayment, tossPaymentResponse)); + OrderableShop orderableShop = orderableShopRepository.getById(temporaryPayment.getOrderableShopId()); Order order = temporaryPayment.toOrder(user, orderableShop); orderRepository.save(order); @@ -164,7 +170,7 @@ public PaymentConfirmResponse confirmPayment(String accessToken, String paymentK return response; } - @Transactional + @Transactional(transactionManager = "koinTransactionManager") public List cancelPayment(String accessToken, String paymentKey, String cancelReason) { Integer userId = jwtTokenResolver.getUserId(accessToken); User user = userRepository.getById(userId);