-
Notifications
You must be signed in to change notification settings - Fork 1
[Feature] 주문하기 결제하기 #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] 주문하기 결제하기 #187
Changes from 22 commits
a47b1ad
4f3ebab
e719184
f676c3e
3ac8140
e2f9700
b7bdf24
adf7302
cd46b94
e95abbb
ec9fc60
5f614ac
3ec78a2
20e471e
4080205
bd8f432
e3adbc9
b041776
1d25dfa
9dada18
a955730
f2a0308
4205911
275499e
ab70c78
8e737ff
ae40ee7
829143e
c407e7c
b162bd0
eea163a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.irum.come2us.domain.coupon.application.service; | ||
|
|
||
| import com.irum.come2us.domain.coupon.domain.entity.AppliedCoupon; | ||
| import com.irum.come2us.domain.coupon.domain.entity.Coupon; | ||
| import com.irum.come2us.domain.coupon.domain.repository.AppliedCouponRepository; | ||
| import com.irum.come2us.domain.coupon.domain.repository.CouponRepository; | ||
| import com.irum.come2us.domain.payment.domain.entity.Payment; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AppliedCouponService { | ||
| private final AppliedCouponRepository appliedCouponRepository; | ||
| private final CouponRepository couponRepository; | ||
|
|
||
| /** 쿠폰 사용 처리 */ | ||
| public void createAppliedCouponList(Payment payment, List<UUID> couponIdList) { | ||
| List<Coupon> couponList = couponRepository.findAllById(couponIdList); | ||
|
|
||
| List<AppliedCoupon> appliedCouponList = | ||
| couponList.stream() | ||
| .map( | ||
| coupon -> | ||
| AppliedCoupon.builder() | ||
| .payment(payment) | ||
| .coupon(coupon) | ||
| .build()) | ||
| .toList(); | ||
|
|
||
| appliedCouponRepository.saveAll(appliedCouponList); | ||
| } | ||
|
|
||
| /** 롤백 */ | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void rollbackAppliedCouponList(Payment payment) { | ||
| appliedCouponRepository.deleteByPayment(payment); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,15 @@ | ||
| package com.irum.come2us.domain.coupon.domain.repository; | ||
|
|
||
| import com.irum.come2us.domain.coupon.domain.entity.AppliedCoupon; | ||
| import com.irum.come2us.domain.payment.domain.entity.Payment; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface AppliedCouponRepository extends JpaRepository<AppliedCoupon, UUID> { | ||
| List<AppliedCoupon> findByCouponId(UUID couponId); | ||
|
|
||
| boolean existsByCouponId(UUID couponId); | ||
|
|
||
| void deleteByPayment(Payment payment); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.irum.come2us.domain.order.application.mapper; | ||
|
|
||
| import com.irum.come2us.domain.order.domain.entity.Order; | ||
| import com.irum.come2us.domain.order.domain.entity.OrderDetail; | ||
| import com.irum.come2us.domain.order.presentation.dto.response.AddressResponse; | ||
| import com.irum.come2us.domain.order.presentation.dto.response.CustomerOrderResponse; | ||
| import java.util.List; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class CustomerOrderMapper { | ||
|
|
||
| public static CustomerOrderResponse toCustomerOrderResponse( | ||
| Order order, List<OrderDetail> orderDetailList) { | ||
| List<CustomerOrderResponse.ProductSummary> productSummaryList = | ||
| orderDetailList.stream().map(CustomerOrderMapper::toProductSummary).toList(); | ||
|
|
||
| return CustomerOrderResponse.builder() | ||
| .orderId(order.getOrderId()) | ||
| .address(AddressResponse.from(order.getDeliveryAddress().getAddress())) | ||
| .totalProductPrice(order.getTotalPrice()) | ||
| .totalDiscountAmount(order.getPayment().getTotalDiscountAmount()) | ||
| .totalPaymentAmount(order.getPayment().getAmount()) | ||
| .prodcutList(productSummaryList) | ||
| .build(); | ||
| } | ||
|
|
||
| private static CustomerOrderResponse.ProductSummary toProductSummary(OrderDetail orderDetail) { | ||
| return CustomerOrderResponse.ProductSummary.builder() | ||
| .orderDetailId(orderDetail.getOrderDetailId()) | ||
| .productName(orderDetail.getProductName()) | ||
| .price(orderDetail.getPrice()) | ||
| .quantity(orderDetail.getQuantity()) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| package com.irum.come2us.domain.order.application.service; | ||
|
|
||
| import com.irum.come2us.domain.coupon.application.service.AppliedCouponService; | ||
| import com.irum.come2us.domain.coupon.application.service.CouponService; | ||
| import com.irum.come2us.domain.deliveryaddress.domain.entity.DeliveryAddress; | ||
| import com.irum.come2us.domain.deliveryaddress.domain.repository.DeliveryAddressRepository; | ||
| import com.irum.come2us.domain.member.domain.entity.Member; | ||
| import com.irum.come2us.domain.order.application.mapper.CustomerOrderMapper; | ||
| import com.irum.come2us.domain.order.domain.entity.Order; | ||
| import com.irum.come2us.domain.order.domain.entity.OrderDetail; | ||
| import com.irum.come2us.domain.order.domain.entity.enums.OrderStatus; | ||
| import com.irum.come2us.domain.order.domain.repository.OrderDetailRepository; | ||
| import com.irum.come2us.domain.order.domain.repository.OrderRepository; | ||
| import com.irum.come2us.domain.order.presentation.dto.request.CustomerOrderRequest; | ||
| import com.irum.come2us.domain.order.presentation.dto.response.CustomerOrderResponse; | ||
| import com.irum.come2us.domain.payment.application.service.PaymentService; | ||
| import com.irum.come2us.domain.payment.domain.entity.Payment; | ||
| import com.irum.come2us.domain.payment.domain.entity.enums.PaymentCorp; | ||
| import com.irum.come2us.domain.product.domain.entity.Product; | ||
| import com.irum.come2us.domain.product.domain.entity.ProductOptionValue; | ||
| import com.irum.come2us.domain.product.domain.repository.ProductOptionValueRepository; | ||
| import com.irum.come2us.domain.product.domain.repository.ProductRepository; | ||
| import com.irum.come2us.domain.store.domain.entity.Store; | ||
| import com.irum.come2us.domain.store.domain.repository.StoreRepository; | ||
| import com.irum.come2us.global.presentation.advice.exception.CommonException; | ||
| import com.irum.come2us.global.presentation.advice.exception.errorcode.DeliveryAddressErrorCode; | ||
| import com.irum.come2us.global.presentation.advice.exception.errorcode.OrderErrorCode; | ||
| import com.irum.come2us.global.presentation.advice.exception.errorcode.ProductErrorCode; | ||
| import com.irum.come2us.global.presentation.advice.exception.errorcode.StoreErrorCode; | ||
| import com.irum.come2us.global.util.MemberUtil; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @Transactional | ||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| public class CustomerOrderService { | ||
| private final MemberUtil memberUtil; | ||
| private final ProductOptionValueRepository productOptionValueRepository; | ||
| private final ProductRepository productRepository; | ||
| private final StoreRepository storeRepository; | ||
| private final DeliveryAddressRepository deliveryAddressRepository; | ||
| private final OrderDetailRepository orderDetailRepository; | ||
| private final CouponService couponService; | ||
| private final AppliedCouponService appliedCouponService; | ||
| private final PaymentService paymentService; | ||
| private final OrderRepository orderRepository; | ||
|
|
||
| public CustomerOrderResponse prepareOrder(CustomerOrderRequest request) { | ||
| Member member = memberUtil.getCurrentMember(); | ||
| Store store = | ||
| storeRepository | ||
| .findById(request.storeId()) | ||
| .orElseThrow(() -> new CommonException(StoreErrorCode.STORE_NOT_FOUND)); | ||
| DeliveryAddress deliveryAddress = | ||
| deliveryAddressRepository | ||
| .findById(request.deliveryAddressId()) | ||
| .orElseThrow( | ||
| () -> | ||
| new CommonException( | ||
| DeliveryAddressErrorCode | ||
| .DELIVERY_ADDRESS_NOT_FOUND)); | ||
| log.info("[주문준비] 멤버 {} {} , 상점, 주소 검색", member.getMemberId(), member.getName()); | ||
|
|
||
| // 상품 확인, 재고확인, 상품 정보 조회 , 가격 계산, 주문 상세 엔티티 생성 준비 | ||
| int calculatedTotalPrice = 0; | ||
| int productCount = 0; | ||
| List<OrderDetail> orderDetails = new ArrayList<>(); | ||
| for (CustomerOrderRequest.ProductSummary productReq : request.productList()) { | ||
|
hyeonji91 marked this conversation as resolved.
|
||
| // 상품이 해당 상점의 상품인지 확인 | ||
| Product product = | ||
| productRepository | ||
| .findById(productReq.productId()) | ||
| .orElseThrow( | ||
| () -> new CommonException(ProductErrorCode.PRODUCT_NOT_FOUND)); | ||
| if (!product.getStore().getId().equals(store.getId())) { | ||
| throw new CommonException(OrderErrorCode.INVALID_ORDER); | ||
| } | ||
|
|
||
| // 재고 확인 | ||
| ProductOptionValue productOptionValue = | ||
| productOptionValueRepository | ||
| .findByIdWithLock(productReq.optionValueId()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: 재고 동시성 문제에 대해 많이 고민하신 것 같습니다. 👍 -> Repository 단에서 lock 유지 시간 3초로 제한해 두신 것 확인했습니다! 추후 분산 환경 처리 성능 따라 시간 조정은 검토해볼만 한 것 같네요:) |
||
| .orElseThrow( | ||
| () -> | ||
| new CommonException( | ||
| ProductErrorCode | ||
| .PRODUCT_OPTION_VALUE_NOT_FOUND)); | ||
| if (productOptionValue.getStockQuantity() < productReq.quantity()) { | ||
| throw new CommonException(ProductErrorCode.PRODUCT_OUT_OF_STOCK); | ||
| } | ||
|
|
||
| // 제품 가격 계산 | ||
| int productPrice = | ||
| (product.getPrice() + productOptionValue.getExtraPrice()) | ||
| * productReq.quantity(); | ||
| calculatedTotalPrice += productPrice; | ||
| productCount += productReq.quantity(); | ||
|
|
||
| // 재고 미리 차감 | ||
| productOptionValue.decreaseStock(productReq.quantity()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: 현재 구매를 진행중인 사용자의 재고 선점을 위해 구현하신 로직인 것 같습니다. |
||
|
|
||
| OrderDetail orderDetail = | ||
| OrderDetail.builder() | ||
| .product(product) | ||
| .price(productPrice) | ||
| .quantity(productReq.quantity()) | ||
| .orderStatusIndi(OrderStatus.PENDING) | ||
| .optionName(productOptionValue.getName()) | ||
| .productName(product.getName()) | ||
| .productOptionValue(productOptionValue) | ||
| .build(); | ||
| orderDetails.add(orderDetail); | ||
| } | ||
| log.info("상품 확인, 재고 확인, 재고 차감, 가격 계산 완료"); | ||
|
|
||
| /** 배송비 적용* */ | ||
| int deliveryFee = store.getDeliveryPolicy().getDefaultDeliveryFee(); | ||
| int deliveryMinAmount = store.getDeliveryPolicy().getMinAmount(); | ||
| int deliveryMinQuantity = store.getDeliveryPolicy().getMinQuantity(); | ||
| if (calculatedTotalPrice > deliveryMinAmount || productCount > deliveryMinQuantity) { | ||
| deliveryFee = 0; | ||
| } | ||
| log.info("배송비 {}", deliveryFee); | ||
|
|
||
| /** 할인 적용 */ | ||
| int discountAmount = | ||
| couponService.validAndCalCoupon( | ||
| request.couponIdList(), calculatedTotalPrice, member); | ||
| int finalPaymentAmount = calculatedTotalPrice - discountAmount; | ||
| log.info("할인 {}, 할인 후 가격 {}", discountAmount, finalPaymentAmount); | ||
|
|
||
| /** 결재 생성 PENDING 상태* */ | ||
| Payment payment = | ||
| paymentService.preparePayment( | ||
| member, finalPaymentAmount, discountAmount, PaymentCorp.TOSS); | ||
| // 쿠폰 미리 차감 | ||
| appliedCouponService.createAppliedCouponList(payment, request.couponIdList()); | ||
|
hyeonji91 marked this conversation as resolved.
|
||
|
|
||
| // 주문 엔티티 생성 PENDING 상태 | ||
| String orderId = "ORD-" + (int) ((Math.random() * 10000000)); | ||
|
hyeonji91 marked this conversation as resolved.
Outdated
|
||
|
|
||
| Order order = | ||
| Order.builder() | ||
| .orderNum(orderId) | ||
| .totalPrice(calculatedTotalPrice) | ||
| .deliveryFee(deliveryFee) | ||
| .deliveryRequest(request.deliveryRequest()) | ||
| .orderStatusAll(OrderStatus.PENDING) | ||
| .member(member) | ||
| .store(store) | ||
| .payment(payment) | ||
| .deliveryAddress(deliveryAddress) | ||
| .build(); | ||
| orderRepository.save(order); | ||
|
|
||
| /** 주문 상세 저장* */ | ||
| for (OrderDetail orderDetail : orderDetails) { | ||
| orderDetail.updateOrder(order); | ||
| orderDetailRepository.save(orderDetail); | ||
| } | ||
|
|
||
| return CustomerOrderMapper.toCustomerOrderResponse(order, orderDetails); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.