Skip to content

Commit 126c4d3

Browse files
authored
Merge pull request #205 from JA-yeong-eop-JA-moeu-JA/fix/#204-coupon
✨ feat: 쿠폰 적용 관련, 팀 결제 관련 수정 사항 적용
2 parents 41f8e3c + a7be4a6 commit 126c4d3

File tree

10 files changed

+162
-87
lines changed

10 files changed

+162
-87
lines changed

src/main/java/com/jajaja/domain/cart/entity/Cart.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.jajaja.domain.coupon.entity.Coupon;
44
import com.jajaja.domain.member.entity.Member;
5+
import com.jajaja.domain.member.entity.MemberCoupon;
56
import com.jajaja.global.apiPayload.code.status.ErrorStatus;
67
import com.jajaja.global.apiPayload.exception.custom.BadRequestException;
78
import com.jajaja.global.common.domain.BaseEntity;
@@ -26,10 +27,10 @@ public class Cart extends BaseEntity {
2627
@OneToOne(fetch = FetchType.LAZY)
2728
@JoinColumn(name = "member_id")
2829
private Member member;
29-
30-
@OneToOne(fetch = FetchType.LAZY)
31-
@JoinColumn(name = "coupon_id")
32-
private Coupon coupon;
30+
31+
@OneToOne(fetch = FetchType.LAZY)
32+
@JoinColumn(name = "member_coupon_id")
33+
private MemberCoupon memberCoupon;
3334

3435
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
3536
private List<CartProduct> cartProducts = new ArrayList<>();
@@ -71,17 +72,17 @@ public void deleteAllCartProductsByProductId(Long productId) {
7172
/**
7273
* Cart에 쿠폰을 적용합니다.
7374
*/
74-
public void applyCoupon(Coupon coupon) {
75-
this.coupon = coupon;
76-
}
77-
78-
/**
75+
public void applyCoupon(MemberCoupon memberCoupon) {
76+
this.memberCoupon = memberCoupon;
77+
}
78+
79+
/**
7980
* Cart에서 쿠폰을 제거합니다.
8081
*/
81-
public void removeCoupon() {
82-
this.coupon = null;
83-
}
84-
82+
public void removeCoupon() {
83+
this.memberCoupon = null;
84+
}
85+
8586
/**
8687
* 장바구니의 총 금액을 계산합니다.
8788
*/
@@ -90,4 +91,11 @@ public int calculateTotalAmount() {
9091
.mapToInt(cp -> cp.getUnitPrice() * cp.getQuantity())
9192
.sum();
9293
}
94+
95+
/**
96+
* 적용된 쿠폰 정보를 가져옵니다.
97+
*/
98+
public Coupon getCoupon() {
99+
return this.memberCoupon != null ? this.memberCoupon.getCoupon() : null;
100+
}
93101
}

src/main/java/com/jajaja/domain/cart/service/CartCommandServiceImpl.java

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
import com.jajaja.domain.cart.entity.Cart;
88
import com.jajaja.domain.cart.entity.CartProduct;
99
import com.jajaja.domain.cart.repository.CartProductRepository;
10-
import com.jajaja.domain.coupon.service.CouponCommonService;
11-
import com.jajaja.domain.member.entity.MemberCoupon;
12-
import com.jajaja.domain.member.repository.MemberCouponRepository;
1310
import com.jajaja.domain.product.entity.Product;
1411
import com.jajaja.domain.product.entity.ProductOption;
1512
import com.jajaja.domain.product.repository.ProductOptionRepository;
@@ -38,8 +35,6 @@ public class CartCommandServiceImpl implements CartCommandService {
3835
private final ProductRepository productRepository;
3936
private final TeamCommandRepository teamRepository;
4037
private final ProductOptionRepository productOptionRepository;
41-
private final MemberCouponRepository memberCouponRepository;
42-
private final CouponCommonService couponCommonService;
4338
private final ProductCommonService productCommonService;
4439
private final CartCommonService cartCommonService;
4540

@@ -49,7 +44,7 @@ public CartResponseDto addOrUpdateCartProduct(Long memberId, List<CartProductAdd
4944
List<CartProductResponseDto> items = request.stream().map(req -> {
5045
log.info("[CartCommandService] 사용자 {}의 장바구니에 아이템 {} 추가/수정", memberId, req.productId());
5146

52-
CartOpterationContext context = prepareCartOperationContext(req.productId(), req.optionId());
47+
CartOperationContext context = prepareCartOperationContext(req.productId(), req.optionId());
5348

5449
Optional<CartProduct> existingItem = req.optionId() != null ? cartProductRepository.findByCartIdAndProductIdAndProductOptionId(cart.getId(), context.product().getId(), context.productOption.getId())
5550
: cartProductRepository.findByCartIdAndProductIdAndProductOptionIsNull(cart.getId(), context.product().getId());
@@ -73,11 +68,8 @@ public CartResponseDto addOrUpdateCartProduct(Long memberId, List<CartProductAdd
7368
return CartProductResponseDto.of(cartProduct, productCommonService.calculateDiscountedPrice(cartProduct.getUnitPrice(), cartProduct.getProduct().getDiscountRate()), isTeamAvailable);
7469
}).toList();
7570

76-
PriceInfoDto priceInfo = cart.getCoupon() != null ?
77-
couponCommonService.calculateDiscount(cart, cart.getCoupon()) :
78-
PriceInfoDto.noDiscount(cart.calculateTotalAmount());
71+
PriceInfoDto priceInfo = PriceInfoDto.noDiscount(cart.calculateTotalAmount());
7972

80-
revalidateAppliedCouponIfExists(cart);
8173
return CartConverter.toCartResponseDto(cart, items, priceInfo);
8274
}
8375

@@ -91,7 +83,6 @@ public void deleteCartProduct(Long memberId, Long productId, Long optionId) {
9183
} catch (IllegalArgumentException e) {
9284
throw new CartHandler(ErrorStatus.CART_PRODUCT_NOT_FOUND);
9385
}
94-
revalidateAppliedCouponIfExists(cart);
9586
}
9687

9788
@Override
@@ -110,17 +101,15 @@ public void deleteCartProducts(Long memberId, List<Long> cartProductIds) {
110101

111102
cartProductRepository.deleteAll(cartProductsToDelete);
112103
cart.getCartProducts().removeAll(cartProductsToDelete);
113-
114-
revalidateAppliedCouponIfExists(cart);
115104
}
116105

117106
/**
118107
* 장바구니 내 아이템 명령 실행에 필요한 도메인 객체를 불러오는 과정입니다.
119108
* option null 처리 등 중복되는 로직 때문에 생성하였습니다.
120109
*
121-
* @return Cart, Product, ProductOption을 포함한 CartOpterationContext
110+
* @return Cart, Product, ProductOption을 포함한 CartOperationContext
122111
* */
123-
private CartOpterationContext prepareCartOperationContext(Long productId, Long optionId) {
112+
private CartOperationContext prepareCartOperationContext(Long productId, Long optionId) {
124113
Product product = productRepository.findById(productId)
125114
.orElseThrow(() -> new CartHandler(ErrorStatus.PRODUCT_NOT_FOUND));
126115

@@ -133,39 +122,8 @@ private CartOpterationContext prepareCartOperationContext(Long productId, Long o
133122
option = null;
134123
}
135124

136-
return new CartOpterationContext(product, option);
125+
return new CartOperationContext(product, option);
137126
}
138127

139-
private record CartOpterationContext(Product product, ProductOption productOption) {}
140-
141-
/**
142-
* 장바구니에 쿠폰이 적용되어 있는 경우 쿠폰 적용 조건을 재검증합니다.
143-
* 조건에 맞지 않으면 쿠폰을 자동으로 해제합니다.
144-
*/
145-
private void revalidateAppliedCouponIfExists(Cart cart) {
146-
if (cart.getCoupon() == null) {
147-
return;
148-
}
149-
150-
try {
151-
MemberCoupon memberCoupon = memberCouponRepository.findByMemberAndCoupon(cart.getMember(), cart.getCoupon())
152-
.orElse(null);
153-
154-
if (memberCoupon == null) {
155-
log.warn("[CartCommandService] 적용된 쿠폰의 MemberCoupon 정보를 찾을 수 없어 쿠폰을 해제합니다. cartId: {}, couponId: {}",
156-
cart.getId(), cart.getCoupon().getId());
157-
cart.removeCoupon();
158-
return;
159-
}
160-
161-
couponCommonService.validateCouponEligibility(cart, cart.getCoupon());
162-
log.info("[CartCommandService] 적용된 쿠폰이 여전히 유효합니다. cartId: {}, couponId: {}",
163-
cart.getId(), cart.getCoupon().getId());
164-
165-
} catch (Exception e) {
166-
log.warn("[CartCommandService] 장바구니 변경으로 인해 쿠폰 조건이 맞지 않아 쿠폰을 해제합니다. cartId: {}, couponId: {}, reason: {}",
167-
cart.getId(), cart.getCoupon().getId(), e.getMessage());
168-
cart.removeCoupon();
169-
}
170-
}
128+
private record CartOperationContext(Product product, ProductOption productOption) {}
171129
}

src/main/java/com/jajaja/domain/coupon/controller/CouponController.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jajaja.domain.coupon.controller;
22

3+
import com.jajaja.domain.coupon.dto.CouponApplyRequestDto;
34
import com.jajaja.domain.coupon.dto.CouponApplyResponseDto;
45
import com.jajaja.domain.coupon.dto.PagingCouponListResponseDto;
56
import com.jajaja.domain.coupon.service.CouponCommandService;
@@ -9,6 +10,7 @@
910
import io.swagger.v3.oas.annotations.Operation;
1011
import io.swagger.v3.oas.annotations.Parameter;
1112
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import jakarta.validation.Valid;
1214
import lombok.RequiredArgsConstructor;
1315
import org.springframework.web.bind.annotation.*;
1416

@@ -38,14 +40,14 @@ public ApiResponse<PagingCouponListResponseDto> getMyCoupons(
3840

3941
@Operation(
4042
summary = "쿠폰 적용/수정 API | by 엠마/신윤지",
41-
description = "선택한 쿠폰을 장바구니에 적용합니다."
43+
description = "선택한 쿠폰을 장바구니의 선택된 상품들에 적용합니다."
4244
)
43-
@PostMapping("/{couponId}/apply")
45+
@PostMapping("/apply")
4446
public ApiResponse<CouponApplyResponseDto> applyCoupon(
4547
@Auth Long memberId,
46-
@PathVariable Long couponId
48+
@Valid @RequestBody CouponApplyRequestDto request
4749
) {
48-
return ApiResponse.onSuccess(couponCommandService.applyCouponToCart(memberId, couponId));
50+
return ApiResponse.onSuccess(couponCommandService.applyCouponToCart(memberId, request));
4951
}
5052

5153
@Operation(
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.jajaja.domain.coupon.dto;
2+
3+
import jakarta.validation.constraints.NotEmpty;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.util.List;
7+
8+
public record CouponApplyRequestDto(
9+
@NotNull(message = "쿠폰 ID는 필수입니다.")
10+
Long couponId,
11+
12+
@NotEmpty(message = "적용할 장바구니 상품 목록은 필수입니다.")
13+
List<Long> items
14+
) {}

src/main/java/com/jajaja/domain/coupon/entity/Coupon.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.jajaja.domain.coupon.entity;
22

3-
import com.jajaja.domain.cart.entity.Cart;
43
import com.jajaja.domain.coupon.entity.enums.ConditionType;
54
import com.jajaja.domain.coupon.entity.enums.DiscountType;
65
import com.jajaja.domain.order.entity.Order;
@@ -52,8 +51,4 @@ public class Coupon extends BaseEntity {
5251

5352
@OneToMany(mappedBy = "coupon", cascade = CascadeType.ALL, orphanRemoval = true)
5453
private List<MemberCoupon> memberCoupons = new ArrayList<>();
55-
56-
@OneToOne(mappedBy = "coupon", fetch = FetchType.LAZY)
57-
private Cart cart;
58-
5954
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.jajaja.domain.coupon.service;
22

3+
import com.jajaja.domain.coupon.dto.CouponApplyRequestDto;
34
import com.jajaja.domain.coupon.dto.CouponApplyResponseDto;
45

56
public interface CouponCommandService {
6-
CouponApplyResponseDto applyCouponToCart(Long memberId, Long couponId);
7+
CouponApplyResponseDto applyCouponToCart(Long memberId, CouponApplyRequestDto request);
78
void unapplyCoupon(Long memberId);
89
}

src/main/java/com/jajaja/domain/coupon/service/CouponCommandServiceImpl.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.jajaja.domain.coupon.service;
22

33
import com.jajaja.domain.cart.entity.Cart;
4+
import com.jajaja.domain.cart.entity.CartProduct;
5+
import com.jajaja.domain.cart.repository.CartProductRepository;
46
import com.jajaja.domain.cart.repository.CartRepository;
7+
import com.jajaja.domain.coupon.dto.CouponApplyRequestDto;
58
import com.jajaja.domain.coupon.dto.CouponApplyResponseDto;
69
import com.jajaja.domain.coupon.entity.Coupon;
710
import com.jajaja.domain.coupon.entity.enums.CouponStatus;
@@ -16,6 +19,8 @@
1619
import org.springframework.stereotype.Service;
1720
import org.springframework.transaction.annotation.Transactional;
1821

22+
import java.util.List;
23+
1924
@Service
2025
@Transactional
2126
@RequiredArgsConstructor
@@ -24,26 +29,30 @@ public class CouponCommandServiceImpl implements CouponCommandService{
2429
private final MemberRepository memberRepository;
2530
private final CouponRepository couponRepository;
2631
private final CartRepository cartRepository;
32+
private final CartProductRepository cartProductRepository;
2733
private final MemberCouponRepository memberCouponRepository;
2834
private final CouponCommonService couponCommonService;
2935

3036
@Override
31-
public CouponApplyResponseDto applyCouponToCart(Long memberId, Long couponId) {
37+
public CouponApplyResponseDto applyCouponToCart(Long memberId, CouponApplyRequestDto request) {
3238
Member member = memberRepository.findById(memberId)
3339
.orElseThrow(() -> new CouponHandler(ErrorStatus.MEMBER_NOT_FOUND));
34-
Coupon coupon = validateAndGetCoupon(couponId);
40+
Coupon coupon = validateAndGetCoupon(request.couponId());
3541

3642
MemberCoupon memberCoupon = validateCouponOwnership(member, coupon);
3743
validateCouponStatus(memberCoupon);
44+
3845
Cart cart = cartRepository.findByMemberId(memberId);
3946
if (cart == null) {
4047
throw new CouponHandler(ErrorStatus.CART_NOT_FOUND);
4148
}
4249

43-
couponCommonService.validateCouponEligibility(cart, coupon);
44-
cart.applyCoupon(coupon);
50+
List<CartProduct> selectedItems = validateAndGetSelectedItems(cart, request.items());
51+
couponCommonService.validateCouponForSelectedItems(selectedItems, coupon);
52+
cart.applyCoupon(memberCoupon);
4553

46-
return CouponApplyResponseDto.withDiscount(cart.getId(), coupon.getId(), coupon.getName(), couponCommonService.calculateDiscount(cart, coupon));
54+
return CouponApplyResponseDto.withDiscount(cart.getId(), coupon.getId(), coupon.getName(),
55+
couponCommonService.calculateDiscountForSelectedItems(selectedItems, coupon));
4756
}
4857

4958
@Override
@@ -83,4 +92,24 @@ private void validateCouponStatus(MemberCoupon memberCoupon) {
8392
throw new CouponHandler(ErrorStatus.COUPON_NOT_AVAILABLE);
8493
}
8594
}
95+
96+
/**
97+
* 선택된 장바구니 상품들을 검증하고 반환합니다.
98+
*/
99+
private List<CartProduct> validateAndGetSelectedItems(Cart cart, List<Long> cartProductIds) {
100+
List<CartProduct> selectedItems = cartProductRepository.findAllById(cartProductIds);
101+
102+
if (selectedItems.size() != cartProductIds.size()) {
103+
throw new CouponHandler(ErrorStatus.CART_PRODUCT_NOT_FOUND);
104+
}
105+
106+
// 선택된 상품들이 모두 해당 장바구니의 상품인지 확인
107+
for (CartProduct item : selectedItems) {
108+
if (!item.getCart().getId().equals(cart.getId())) {
109+
throw new CouponHandler(ErrorStatus.CART_PRODUCT_NOT_FOUND);
110+
}
111+
}
112+
113+
return selectedItems;
114+
}
86115
}

0 commit comments

Comments
 (0)