diff --git a/src/main/java/com/kt/controller/payment/PaymentController.java b/src/main/java/com/kt/controller/payment/PaymentController.java index f56612f3..ac33635d 100644 --- a/src/main/java/com/kt/controller/payment/PaymentController.java +++ b/src/main/java/com/kt/controller/payment/PaymentController.java @@ -38,6 +38,7 @@ import com.kt.properties.TossPaymentsProperties; import com.kt.security.CustomUserDetails; import com.kt.service.order.OrderService; +import com.kt.service.payment.PaymentFacade; import com.kt.service.payment.PaymentService; import io.swagger.v3.oas.annotations.Operation; @@ -53,6 +54,7 @@ public class PaymentController extends SwaggerAssistance { private final PaymentService paymentService; private final OrderService orderService; + private final PaymentFacade paymentFacade; private final TossPaymentsProperties tossPaymentsProperties; private final RestTemplate restTemplate = new RestTemplate(); @@ -121,7 +123,7 @@ public ResponseEntity confirmPayment(@RequestBody PaymentTossConfirmRequest r if (response.getStatusCode() == HttpStatus.OK) { Map tossPayment = response.getBody(); - Long paymentId = paymentService.create(tossPayment, currentUser.getId()); + Long paymentId = paymentFacade.completePayment(tossPayment, currentUser.getId(), orderIdLong); Map responseBody = new HashMap<>(tossPayment); responseBody.put("paymentId", paymentId); @@ -138,6 +140,8 @@ public ResponseEntity confirmPayment(@RequestBody PaymentTossConfirmRequest r .body(e.getResponseBodyAsString()); } catch (Exception e) { e.printStackTrace(); + + //TODO order 생성 기록 rollback return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of( diff --git a/src/main/java/com/kt/domain/order/Order.java b/src/main/java/com/kt/domain/order/Order.java index c95f0cd7..2fde59dd 100644 --- a/src/main/java/com/kt/domain/order/Order.java +++ b/src/main/java/com/kt/domain/order/Order.java @@ -92,4 +92,9 @@ public void updateStatus(OrderStatus newStatus) { this.deliveredAt = LocalDate.now(); } } + + public void addOrderProduct(OrderProduct orderProduct) { + orderProducts.add(orderProduct); + orderProduct.assignOrder(this); + } } diff --git a/src/main/java/com/kt/domain/order/OrderStatus.java b/src/main/java/com/kt/domain/order/OrderStatus.java index bcdf15b7..3f6b3b54 100644 --- a/src/main/java/com/kt/domain/order/OrderStatus.java +++ b/src/main/java/com/kt/domain/order/OrderStatus.java @@ -16,7 +16,10 @@ public enum OrderStatus { RETURN_REQUESTED("반품 요청"), RETURNED("반품 완료"), REFUND_REQUESTED("환불 요청"), - REFUNDED("환불 완료"); + REFUNDED("환불 완료"), + + PENDING_PAYMENT("결제 대기"), + PAYMENT_FAILED("결제 실패"); private final String description; } diff --git a/src/main/java/com/kt/domain/orderproduct/OrderProduct.java b/src/main/java/com/kt/domain/orderproduct/OrderProduct.java index 496d5b2a..c54c4046 100644 --- a/src/main/java/com/kt/domain/orderproduct/OrderProduct.java +++ b/src/main/java/com/kt/domain/orderproduct/OrderProduct.java @@ -38,4 +38,14 @@ public OrderProduct(Long count, Long variantId, Product product, Order order) { this.product = product; this.order = order; } + + public OrderProduct(Long count, Long variantId, Product product) { + this.count = count; + this.variantId = variantId; + this.product = product; + } + + public void assignOrder(Order order) { + this.order = order; + } } diff --git a/src/main/java/com/kt/service/order/OrderService.java b/src/main/java/com/kt/service/order/OrderService.java index 2f5731d6..ae66582e 100644 --- a/src/main/java/com/kt/service/order/OrderService.java +++ b/src/main/java/com/kt/service/order/OrderService.java @@ -2,12 +2,10 @@ import static com.kt.common.support.ObjectUtils.*; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -18,24 +16,19 @@ import com.kt.common.request.Paging; import com.kt.common.support.Preconditions; import com.kt.domain.discount.Discount; -import com.kt.domain.discount.policy.DiscountPolicy; -import com.kt.domain.discount.policy.DiscountPolicyFactory; -import com.kt.domain.membership.Membership; import com.kt.domain.order.Order; import com.kt.domain.order.OrderStatus; import com.kt.domain.orderproduct.OrderProduct; import com.kt.domain.product.ProductStatus; import com.kt.domain.shoppingaddress.ShoppingAddress; -import com.kt.domain.user.User; import com.kt.dto.discount.response.DiscountResult; import com.kt.dto.order.OrderCreateRequest; +import com.kt.dto.order.OrderProductRequest; import com.kt.dto.order.OrderStatusUpdateRequest; import com.kt.dto.order.OrderUpdateRequest; import com.kt.dto.order.response.OrderListResponse; import com.kt.dto.order.response.OrderDetailResponse; import com.kt.dto.order.response.OrderProductResponse; -import com.kt.repository.discount.DiscountRepository; -import com.kt.repository.discountmembership.DiscountMembershipRepository; import com.kt.repository.order.OrderRepository; import com.kt.repository.order.OrderRepositoryCustom; import com.kt.repository.orderproduct.OrderProductRepository; @@ -48,7 +41,6 @@ import lombok.RequiredArgsConstructor; @Service -@Transactional @RequiredArgsConstructor public class OrderService { private final UserRepository userRepository; @@ -58,15 +50,37 @@ public class OrderService { private final OrderProductRepository orderProductRepository; private final VariantRepository variantRepository; private final OrderRepositoryCustom orderRepositoryCustom; - private final DiscountMembershipRepository discountMembershipRepository; private final DiscountCalcService discountCalcService; + @Transactional public Long create(Long userId, OrderCreateRequest request) { var user = userRepository.findByIdOrThrow(userId, ErrorCode.NOT_FOUND_USER); + + // 1. 주소, 상품 유효성 검증 + var address = validateAddress(userId, request.receiverAddressId()); + var orderProducts = createOrderProducts(request.products()); + + // 2. 주문 생성 + var newOrder = new Order( + request.receiverName(), + request.receiverPhone(), + address.getAddress(), + user + ); + + orderProducts.forEach(newOrder::addOrderProduct); + + // 4. 저장 + orderRepository.save(newOrder); + orderProductRepository.saveAll(orderProducts); + return newOrder.getId(); + } + + private ShoppingAddress validateAddress(Long userId, Long receiverAddressId) { ShoppingAddress address; - if (request.receiverAddressId() != null) { - address = shoppingAddressRepository.findByIdOrThrow(request.receiverAddressId(), + if (receiverAddressId != null) { + address = shoppingAddressRepository.findByIdOrThrow(receiverAddressId, ErrorCode.NOT_FOUND_SHOPPING_ADDRESS); // 사용자 본인이 등록한 배송지인지 검증 @@ -76,51 +90,45 @@ public Long create(Long userId, OrderCreateRequest request) { .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_SHOPPING_ADDRESS)); } - // 1. 주문 생성 - var newOrder = new Order( - request.receiverName(), - request.receiverPhone(), - address.getAddress(), - user + return address; + } + + private List createOrderProducts(List products) { + return products.stream() + .map(this::validateAndCreateOrderProduct) + .toList(); + } + + private OrderProduct validateAndCreateOrderProduct(OrderProductRequest productRequest) { + var product = productRepository.findByIdOrThrow(productRequest.productId(), ErrorCode.NOT_FOUND_PRODUCT); + + //상품 상태 검증 + Preconditions.validate(product.getStatus().equals(ProductStatus.ACTIVATED), ErrorCode.CANNOT_PURCHASE_PRODUCT); + //상품 재고 검증 + Preconditions.validate(product.getStock() >= productRequest.productCount(), ErrorCode.NOT_ENOUGH_STOCK); + + //선택한 상품의 옵션이 맞는지 검증 + var variant = variantRepository.findByIdAndDeletedFalseOrThrow(productRequest.productVariantId(), ErrorCode.NOT_FOUND_VARIANT); + Preconditions.validate(variant.getProduct().getId().equals(product.getId()), ErrorCode.INVALID_VARIANT); + + return new OrderProduct( + productRequest.productCount(), + productRequest.productVariantId(), + product ); + } - List orderProducts = new ArrayList<>(); - - // 2. 전체 product 검증 - request.products().forEach(product -> { - var targetProduct = productRepository.findByIdOrThrow(product.productId(), ErrorCode.NOT_FOUND_PRODUCT); - - Preconditions.validate(targetProduct.getStatus().equals(ProductStatus.ACTIVATED), - ErrorCode.CANNOT_PURCHASE_PRODUCT); - Preconditions.validate(targetProduct.getStock() >= product.productCount(), ErrorCode.NOT_ENOUGH_STOCK); - //선택한 상품의 옵션이 맞는지 검증 - var variant = variantRepository.findByIdAndDeletedFalseOrThrow(product.productVariantId(), - ErrorCode.NOT_FOUND_VARIANT); - Preconditions.validate(variant.getProduct().getId().equals(targetProduct.getId()), - ErrorCode.INVALID_VARIANT); - - // OrderProduct 생성 - var newOrderProduct = new OrderProduct( - product.productCount(), - product.productVariantId(), - targetProduct, - newOrder - ); - - orderProducts.add(newOrderProduct); - }); + @Transactional + public void save(Long orderId){ + var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); - // 3. stock 차감 - orderProducts.forEach(newProduct -> { + // stock 차감 + order.getOrderProducts().forEach(newProduct -> { var product = newProduct.getProduct(); product.updateStock(product.getStock() - newProduct.getCount()); }); - // 4. 저장 - orderRepository.save(newOrder); - orderProductRepository.saveAll(orderProducts); - - return newOrder.getId(); + order.updateStatus(OrderStatus.PAID); } public Page getOrderList(Long userId, Paging paging) { @@ -159,6 +167,7 @@ public Page getOrderList(Long userId, Paging paging) { }); } + @Transactional public void cancel(Long orderId, Long userId) { // orderId 존재 여부 검증 var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -209,6 +218,7 @@ public OrderDetailResponse getOrderDetail(Long userId, Long orderId) { return OrderDetailResponse.from(order, products); } + @Transactional public void update(OrderUpdateRequest request, Long orderId, Long userId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -228,6 +238,7 @@ public void update(OrderUpdateRequest request, Long orderId, Long userId) { ); } + @Transactional public void cancelByAdmin(Long orderId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -238,6 +249,7 @@ public void cancelByAdmin(Long orderId) { order.cancel(); } + @Transactional public void requestRefund(Long orderId, Long userId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -249,6 +261,7 @@ public void requestRefund(Long orderId, Long userId) { order.requestRefund(); } + @Transactional public void requestReturn(Long orderId, Long userId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -259,6 +272,7 @@ public void requestReturn(Long orderId, Long userId) { order.requestReturn(); } + @Transactional public void approveRefund(Long orderId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -267,6 +281,7 @@ public void approveRefund(Long orderId) { order.approveRefund(); } + @Transactional public void approveReturn(Long orderId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); @@ -275,6 +290,7 @@ public void approveReturn(Long orderId) { order.approveReturn(); } + @Transactional public void updateStatus(OrderStatusUpdateRequest request, Long orderId) { var order = orderRepository.findByIdOrThrow(orderId, ErrorCode.NOT_FOUND_ORDER); diff --git a/src/main/java/com/kt/service/payment/PaymentFacade.java b/src/main/java/com/kt/service/payment/PaymentFacade.java new file mode 100644 index 00000000..828f3784 --- /dev/null +++ b/src/main/java/com/kt/service/payment/PaymentFacade.java @@ -0,0 +1,25 @@ +package com.kt.service.payment; + +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.kt.service.order.OrderService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PaymentFacade { + private final OrderService orderService; + private final PaymentService paymentService; + + @Transactional + public Long completePayment(Map tossResponse, Long userId, Long orderId) { + var paymentId = paymentService.create(tossResponse, userId); + orderService.save(orderId); + + return paymentId; + } +} diff --git a/src/main/java/com/kt/service/payment/PaymentService.java b/src/main/java/com/kt/service/payment/PaymentService.java index a5e6fbe9..31f0eb8e 100644 --- a/src/main/java/com/kt/service/payment/PaymentService.java +++ b/src/main/java/com/kt/service/payment/PaymentService.java @@ -4,34 +4,26 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.kt.common.exception.CustomException; import com.kt.common.exception.ErrorCode; import com.kt.common.support.Preconditions; import com.kt.domain.discount.Discount; -import com.kt.domain.discount.policy.DiscountPolicy; -import com.kt.domain.discount.policy.DiscountPolicyFactory; import com.kt.domain.order.Order; import com.kt.domain.order.OrderStatus; import com.kt.domain.orderproduct.OrderProduct; import com.kt.domain.payment.Payment; import com.kt.dto.discount.response.DiscountInfo; import com.kt.dto.discount.response.DiscountResult; -import com.kt.dto.order.response.OrderProductResponse; -import com.kt.dto.payment.PaymentCreateRequest; import com.kt.dto.payment.PaymentDetailResponse; import com.kt.dto.payment.PaymentListResponse; import com.kt.dto.payment.PaymentOrderInfoResponse; import com.kt.dto.payment.PaymentTossCancelRequest; import com.kt.dto.payment.PaymentTossCancelResponse; -import com.kt.dto.payment.PaymentTossConfirmRequest; import com.kt.repository.order.OrderRepository; import com.kt.repository.order.OrderRepositoryCustom; import com.kt.repository.orderproduct.OrderProductRepositoryCustom; @@ -40,6 +32,7 @@ import com.kt.repository.paymenttype.PaymentTypeRepository; import com.kt.repository.user.UserRepository; import com.kt.service.discount.DiscountCalcService; +import com.kt.service.order.OrderService; import lombok.RequiredArgsConstructor; @@ -78,8 +71,6 @@ public Long create(Map tossResponse, Long userId) { Payment savedPayment = paymentRepository.save(payment); - order.updateStatus(OrderStatus.PAID); - return savedPayment.getId(); } diff --git a/src/test/java/com/kt/service/payment/PaymentServiceTest.java b/src/test/java/com/kt/service/payment/PaymentServiceTest.java index 19149bb5..d4d25200 100644 --- a/src/test/java/com/kt/service/payment/PaymentServiceTest.java +++ b/src/test/java/com/kt/service/payment/PaymentServiceTest.java @@ -115,9 +115,6 @@ void setUp() { assertThat(payment.getTotalPrice()).isEqualTo(23000); assertThat(payment.getDeliveryFee()).isEqualTo(3000); assertThat(payment.getFinalPrice()).isEqualTo(20000); - - Order updatedOrder = orderRepository.findById(order.getId()).get(); - assertThat(updatedOrder.getOrderStatus()).isEqualTo(OrderStatus.PAID); } @Test