Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Kuma's Curious Paradise

[데브 캠프] 7일차 쿠폰 로직 이해 및 추가 기능 구현하기 본문

카테고리 없음

[데브 캠프] 7일차 쿠폰 로직 이해 및 추가 기능 구현하기

쿠마냥 2024. 4. 3. 12:17

1. 쿠폰 사용 전, 유효성 검사 

사용자가 만료되었거나 이미 사용된 쿠폰을 적용하려   있으므로 issuedCoupon.use() 메서드 호출하기 전에 쿠폰 유효성을 검사하고 쿠폰을 사용하도록 하였다. 

 

쿠폰을 사용하기 전, 쿠폰 유효성(isValid, isUsed, validFrom, validUntil)을 검사한다. 쿠폰이 더욱 늘어날 수 있다고 생각하여 쿠폰 타입을 검사하는 부분도 함께 구현하였다. 

 

IssuedCouponServiceImpl class - isValidCoupon(), isCouponTypeValid() 추가

@Service
@RequiredArgsConstructor
public class IssuedCouponServiceImpl implements IssuedCouponService {
    private final IssuedCouponRepository issuedCouponRepository;

    @Override
    public void useCoupon(IssuedCoupon issuedCoupon) {
        issuedCoupon.use();
        issuedCouponRepository.save(issuedCoupon);
    }


    @Override
    public boolean isValidCoupon(IssuedCoupon issuedCoupon) {
        Date now = new Date();
        return issuedCoupon.isValid() && !issuedCoupon.isUsed() && now.after(issuedCoupon.getValidFrom()) && now.before(issuedCoupon.getValidUntil());
    }

    @Override
    public boolean isCouponTypeValid(IssuedCoupon issuedCoupon) {
        return issuedCoupon.getCoupon().getCouponType().equalsIgnoreCase("PERCENT-OFF") || issuedCoupon.getCoupon().getCouponType().equalsIgnoreCase("FIXED-AMOUNT-OFF");
    }

    private Optional<IssuedCoupon> findCouponById(Long couponId) {
        return issuedCouponRepository.findById(couponId);
    }
}

 

 

2. 쿠폰 동시 적용 방지 

하나의 쿠폰을 가지고 주문1, 주문2, 주문3에 동시적으로 적용하려 하는 경우를 방지하고자 ,데이터베이스 레벨에서 쿠폰을 꺼내올 때 쿠폰 상태를 보고 꺼내오도록 로직을 수정하였다. 동시성을 제어하기에 완벽한 방법은 아니지만, 먼저 구현할 수 있는 레벨에서 최대한 구현해 보고자 하였다. 

 

@Override
    public void useCoupon(IssuedCoupon issuedCoupon) throws Exception {
        int updatedRows = issuedCouponRepository.useCouponIfValid(issuedCoupon.getId());
        if (updatedRows == 0) {
            throw new Exception("The coupon is either already used or not valid.");
        }

    }

 

@Repository
public interface IssuedCouponRepository extends JpaRepository<IssuedCoupon, Long> {
    @Modifying
    @Query("UPDATE IssuedCoupon ic SET ic.isUsed = true, ic.isValid = false, ic.usedAt = CURRENT_TIMESTAMP WHERE ic.id = :couponId AND ic.isUsed = false AND ic.isValid = true")
    int useCouponIfValid(Long couponId);

}

 

 

3. 쿠폰 + 포인트 적용 후 주문 금액이 음수가 되지 않도록 검증

getCheckoutPrice() 에서 쿠폰과 포인트 적용 최종 주문 금액이 음수가 되지 않도록 검증하는 로직을 추가하였다. 쇼핑몰에서는 일반적으로 쿠폰 할인을 먼저 적용한 후에 남은 금액에 대해 포인트를 사용한다. 더 저렴한 금액을 제공하기 때문. 따라서 밑의 로직도 쿠폰 적용 후 포인트를 사용하도록 구현하였다. 

public double getCheckoutPrice() {
    double amount = items.stream().mapToDouble(OrderItem::getEntryPrice).sum();

    Coupon coupon = this.usedIssuedCoupon.getCoupon();
    if (coupon != null) {
        if (coupon.getCouponType().equalsIgnoreCase("PERCENT-OFF")) {
            amount *= (1 - coupon.getAmount());
        } else if (coupon.getCouponType().equalsIgnoreCase("FIXED-AMOUNT-OFF")) {
            amount -= coupon.getAmount();
        }
    }

    amount -= this.pointAmountUsed;

    if (amount < 0) {
        throw new IllegalArgumentException("최종 주문 금액이 음수가 될 수 없습니다.");
    }
    this.amount = amount;
    return amount;
}

 

4. 쿠폰 적용 금액 미리보기 

order 생성 시 쿠폰을 적용했을 때의 최소 결제 금액을 반환해주도록 로직을 수정하였다. 사용자가 가진 모든 쿠폰들 중 가장 할인 금액이 높은 쿠폰을 적용한다. 

최근 쇼핑몰에서는 미리 쿠폰을 적용하여 가장 저렴하게 살 수 있는 가격을 표시해 준다. 이는 사용자의 편의성을 높일 뿐더러 사용자로 하여금 가격이 싸다고 느끼게 하기 때문에 물건을 사도록 유도하는 기능이 있다. 

 

[PaymentFacadeImpl class]

@Transactional
public OrderResponseDto initOrder(CreateOrderDto createOrderDto) throws Exception {
    Order order = orderService.createOrder(
            createOrderDto.getUser(),
            createOrderDto.getOrderItems(),
            createOrderDto.getShippingInfo());
    // 쿠폰 적용 후 주문 예상 금액
    double minimumOrderAmountAfterCoupons = orderService.calculateMinimumOrderAmountAfterCoupons(order);
    // 쿠폰 적용
    orderService.applyCouponToOrder(order.getId(), createOrderDto.getCoupon());
    // 포인트 적용
    double pointAmountToUse = createOrderDto.getPointAmountToUse(); // 사용자가 원하는 포인트 사용 금액
    double finalAmountAfterPoints = minimumOrderAmountAfterCoupons - pointAmountToUse;
    finalAmountAfterPoints = finalAmountAfterPoints > 0 ? finalAmountAfterPoints : 0; // 최종 금액이 0보다 작아지지 않도록 처리

    orderService.applyPointToOrder(order.getId(), pointAmountToUse);
    return new OrderResponseDto(order.getId(), finalAmountAfterPoints);
}

 

[OrderServiceImpl class]

public double calculateMinimumOrderAmountAfterCoupons(Order order) {
    double minimumAmount = Double.MAX_VALUE;

    List<IssuedCoupon> userCoupons = issuedCouponRepository.findAllByUser(order.getUser());
    for (IssuedCoupon coupon : userCoupons) {
        double tempAmount = applyCoupon(order, coupon.getCoupon());
        if (tempAmount < minimumAmount) {
            minimumAmount = tempAmount;
        }
    }

    return minimumAmount == Double.MAX_VALUE ? order.getAmount() : minimumAmount;
}

private double applyCoupon(Order order, Coupon coupon) {
    double discountAmount = 0.0;
    switch (coupon.getCouponType()) {
        case "PERCENT-OFF":
            discountAmount = order.getAmount() * coupon.getAmount() / 100;
            break;
        case "FIXED-AMOUNT-OFF":
            discountAmount = coupon.getAmount();
            break;
        default:
            break;
    }
    double finalAmount = order.getAmount() - discountAmount;
    return finalAmount > 0 ? finalAmount : 0;
}