Skip to content

Commit 508b3ee

Browse files
authored
Merge pull request #13 from Kimjipang/round07
Round07
2 parents 6305753 + 0cd4814 commit 508b3ee

26 files changed

Lines changed: 483 additions & 28 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.loopers.application.coupon;
2+
3+
import com.loopers.domain.coupon.Coupon;
4+
import com.loopers.domain.coupon.CouponRepository;
5+
import com.loopers.domain.user.UserRepository;
6+
import com.loopers.interfaces.api.coupon.CouponV1Dto;
7+
import com.loopers.support.error.CoreException;
8+
import com.loopers.support.error.ErrorType;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Component;
11+
12+
import java.util.List;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class CouponFacade {
17+
private final CouponRepository couponRepository;
18+
private final UserRepository userRepository;
19+
20+
public CouponInfo issueCoupon(CouponV1Dto.CouponRequest request) {
21+
/*
22+
👨‍💻 쿠폰 발급 로직
23+
- [ ] 사용자 검증
24+
- [ ] 쿠폰 발급
25+
*/
26+
Long userId = request.userId();
27+
userRepository.findById(userId).orElseThrow(
28+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 회원입니다.")
29+
);
30+
31+
Coupon coupon = Coupon.create(request);
32+
Coupon saved = couponRepository.save(coupon);
33+
34+
return CouponInfo.from(saved);
35+
}
36+
37+
public List<CouponInfo> findCoupons(Long userId) {
38+
/*
39+
👨‍💻 사용 가능한 쿠폰 조회 로직
40+
- [ ] 사용자 검증
41+
- [ ] 쿠폰 조회
42+
*/
43+
userRepository.findById(userId).orElseThrow(
44+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 회원입니다.")
45+
);
46+
47+
List<Coupon> coupons = couponRepository.findAllByUserId(userId).orElseThrow(
48+
() -> new CoreException(ErrorType.BAD_REQUEST, "사용 가능한 쿠폰이 존재하지 않습니다.")
49+
);
50+
51+
return coupons.stream()
52+
.map(CouponInfo::from)
53+
.toList();
54+
}
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.loopers.application.coupon;
2+
3+
import com.loopers.domain.coupon.Coupon;
4+
import com.loopers.domain.coupon.CouponType;
5+
6+
public record CouponInfo(Long id, Long userId, CouponType couponType, int quantity) {
7+
public static CouponInfo from(Coupon coupon) {
8+
return new CouponInfo(
9+
coupon.getId(),
10+
coupon.getUserId(),
11+
coupon.getCouponType(),
12+
coupon.getQuantity()
13+
);
14+
}
15+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.loopers.application.event.coupon;
2+
3+
import com.loopers.application.order.OrderCreatedEvent;
4+
import com.loopers.domain.coupon.Coupon;
5+
import com.loopers.domain.coupon.CouponRepository;
6+
import com.loopers.support.error.CoreException;
7+
import com.loopers.support.error.ErrorType;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.transaction.annotation.Propagation;
11+
import org.springframework.transaction.annotation.Transactional;
12+
import org.springframework.transaction.event.TransactionPhase;
13+
import org.springframework.transaction.event.TransactionalEventListener;
14+
15+
@Component
16+
@RequiredArgsConstructor
17+
public class CouponEventHandler {
18+
private final CouponRepository couponRepository;
19+
20+
@Transactional(propagation = Propagation.REQUIRES_NEW)
21+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
22+
public void onOrderCreated(OrderCreatedEvent event) {
23+
Long couponId = event.couponId();
24+
Coupon coupon = couponRepository.findById(couponId).orElseThrow(
25+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 쿠폰입니다.")
26+
);
27+
28+
coupon.useCoupon();
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.loopers.application.event.like;
2+
3+
import com.loopers.application.like.LikeCreateEvent;
4+
import com.loopers.domain.product.Product;
5+
import com.loopers.domain.product.ProductRepository;
6+
import com.loopers.support.error.CoreException;
7+
import com.loopers.support.error.ErrorType;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.transaction.annotation.Propagation;
11+
import org.springframework.transaction.annotation.Transactional;
12+
import org.springframework.transaction.event.TransactionPhase;
13+
import org.springframework.transaction.event.TransactionalEventListener;
14+
15+
@Component
16+
@RequiredArgsConstructor
17+
public class LikeEventHandler {
18+
private final ProductRepository productRepository;
19+
20+
@Transactional(propagation = Propagation.REQUIRES_NEW)
21+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
22+
public void onLikeCreated(LikeCreateEvent event) {
23+
Product product = productRepository.findById(event.productId()).orElseThrow(
24+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")
25+
);
26+
27+
product.addLikeCount();
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.loopers.application.event.useraction;
2+
3+
import com.loopers.application.product.UserActionEvent;
4+
import com.loopers.domain.actionlog.UserActionLog;
5+
import com.loopers.infrastructure.actionlog.UserActionLogJpaRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.transaction.annotation.Propagation;
9+
import org.springframework.transaction.annotation.Transactional;
10+
import org.springframework.transaction.event.TransactionPhase;
11+
import org.springframework.transaction.event.TransactionalEventListener;
12+
13+
@Component
14+
@RequiredArgsConstructor
15+
public class UserActionLogEventHandler {
16+
private final UserActionLogJpaRepository userActionLogJpaRepository;
17+
18+
@Transactional(propagation = Propagation.REQUIRES_NEW)
19+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
20+
public void onUserAction(UserActionEvent event) {
21+
UserActionLog userActionLog = UserActionLog.create(event);
22+
userActionLogJpaRepository.save(userActionLog);
23+
}
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.loopers.application.like;
2+
3+
public record LikeCreateEvent(Long userId, Long productId) {
4+
}

apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.loopers.application.like;
22

3+
import com.loopers.application.product.UserActionEvent;
4+
import com.loopers.domain.actionlog.ActionType;
35
import com.loopers.domain.like.Like;
46
import com.loopers.domain.like.LikeRepository;
5-
import com.loopers.domain.product.Product;
67
import com.loopers.domain.product.ProductRepository;
78
import com.loopers.domain.user.UserRepository;
89
import com.loopers.interfaces.api.like.LikeV1Dto;
910
import com.loopers.support.error.CoreException;
1011
import com.loopers.support.error.ErrorType;
1112
import lombok.RequiredArgsConstructor;
13+
import org.springframework.context.ApplicationEventPublisher;
1214
import org.springframework.stereotype.Component;
1315
import org.springframework.transaction.annotation.Transactional;
1416

@@ -18,6 +20,7 @@ public class LikeFacade {
1820
private final LikeRepository likeRepository;
1921
private final UserRepository userRepository;
2022
private final ProductRepository productRepository;
23+
private final ApplicationEventPublisher publisher;
2124

2225
@Transactional
2326
public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
@@ -33,7 +36,7 @@ public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
3336
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 유저입니다.")
3437
);
3538

36-
Product product = productRepository.findById(productId).orElseThrow(
39+
productRepository.findById(productId).orElseThrow(
3740
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")
3841
);
3942

@@ -43,7 +46,8 @@ public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
4346
Like newLike = request.toEntity();
4447
likeRepository.save(newLike);
4548

46-
product.addLikeCount();
49+
publisher.publishEvent(new LikeCreateEvent(userId, productId));
50+
publisher.publishEvent(new UserActionEvent(userId, productId, ActionType.DO_LIKE));
4751

4852
return LikeInfo.from(newLike);
4953
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.loopers.application.order;
2+
3+
public record OrderCreatedEvent(Long couponId) {
4+
}

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.loopers.application.order;
22

33
import com.loopers.application.orderitem.OrderItemInfo;
4+
import com.loopers.application.product.UserActionEvent;
5+
import com.loopers.domain.coupon.Coupon;
6+
import com.loopers.domain.coupon.CouponRepository;
47
import com.loopers.domain.order.Order;
58
import com.loopers.domain.order.OrderRepository;
69
import com.loopers.domain.orderitem.OrderItem;
@@ -12,6 +15,7 @@
1215
import com.loopers.support.error.CoreException;
1316
import com.loopers.support.error.ErrorType;
1417
import lombok.RequiredArgsConstructor;
18+
import org.springframework.context.ApplicationEventPublisher;
1519
import org.springframework.stereotype.Component;
1620
import org.springframework.transaction.annotation.Transactional;
1721

@@ -25,17 +29,19 @@ public class OrderFacade {
2529
private final OrderItemRepository orderItemRepository;
2630
private final UserRepository userRepository;
2731
private final ProductRepository productRepository;
32+
private final CouponRepository couponRepository;
33+
private final ApplicationEventPublisher publisher;
2834

2935
@Transactional
3036
public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
3137
/*
32-
- [ ] 사용자 존재 여부 확인
33-
- [ ] OrderRequest 내에서 OrderItemRequest 목록을 순회하며 상품 존재 여부 확인
38+
- [ ] 유저 검증(존재 여부 확인)
39+
- [ ] OrderRequest 내에서 OrderItemRequest 목록을 순회하며 상품 검증(존재 여부 확인)
3440
- [ ] OrderItem 목록 생성
3541
*/
3642

43+
// 유저 검증
3744
Long userId = request.userId();
38-
3945
userRepository.findById(userId).orElseThrow(
4046
() -> new CoreException(ErrorType.BAD_REQUEST, "존재하는 유저가 아닙니다.")
4147
);
@@ -44,11 +50,18 @@ public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
4450

4551
List<OrderItem> orderItems = orderItemRequests.stream()
4652
.map(item -> {
53+
// 상품 검증
4754
Long productId = item.productId();
4855
Product product = productRepository.findById(productId).orElseThrow(
4956
() -> new CoreException(ErrorType.NOT_FOUND, "존재하는 상품이 아닙니다.")
5057
);
5158

59+
if (product.getStock() < item.quantity()) {
60+
throw new CoreException(ErrorType.BAD_REQUEST, product.getName() + " 상품의 재고가 부족합니다.");
61+
}
62+
63+
product.decreaseStock(item.quantity());
64+
5265
OrderItem orderItem = item.toEntity(
5366
null,
5467
product.getPrice().multiply(BigDecimal.valueOf(item.quantity()))
@@ -62,23 +75,29 @@ public OrderResultInfo createOrder(OrderV1Dto.OrderRequest request) {
6275
.map(OrderItem::getOrderPrice)
6376
.reduce(BigDecimal.ZERO, BigDecimal::add);
6477

65-
Order order = request.toEntity(totalPrice);
78+
Long couponId = request.couponId();
6679

67-
Order saved = orderRepository.save(order);
80+
Coupon coupon = couponRepository.findById(couponId).orElseThrow(
81+
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 쿠폰입니다.")
82+
);
6883

69-
orderItems.forEach(item -> item.assignOrderId(saved.getId()));
70-
orderItemRepository.saveAll(orderItems);
84+
if (coupon.getQuantity() <= 0) {
85+
throw new CoreException(ErrorType.BAD_REQUEST, "남은 쿠폰이 존재하지 않습니다.");
86+
}
87+
88+
int rate = coupon.getCouponType().getRate();
7189

72-
orderItems.forEach(item -> {
73-
Long productId = item.getProductId();
90+
totalPrice = totalPrice
91+
.multiply(BigDecimal.valueOf(100 - rate))
92+
.divide(BigDecimal.valueOf(100));
7493

75-
Product product = productRepository.findById(productId).orElseThrow(
76-
() -> new CoreException(ErrorType.NOT_FOUND, "존재하는 상품이 아닙니다.")
77-
);
94+
publisher.publishEvent(new OrderCreatedEvent(couponId));
7895

79-
product.decreaseStock(item.getQuantity());
96+
Order order = request.toEntity(totalPrice);
97+
Order saved = orderRepository.save(order);
8098

81-
});
99+
orderItems.forEach(item -> item.assignOrderId(saved.getId()));
100+
orderItemRepository.saveAll(orderItems);
82101

83102
List<OrderItemInfo> orderItemInfos = orderItems.stream()
84103
.map(orderItem -> OrderItemInfo.from(orderItem, orderItem.getOrderPrice()))

apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.loopers.application.product;
22

3+
import com.loopers.domain.actionlog.ActionType;
34
import com.loopers.domain.brand.BrandRepository;
4-
import com.loopers.domain.like.LikeRepository;
55
import com.loopers.domain.product.Product;
66
import com.loopers.domain.product.ProductRepository;
77
import com.loopers.interfaces.api.product.ProductV1Dto;
@@ -10,6 +10,7 @@
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.cache.annotation.CacheEvict;
1212
import org.springframework.cache.annotation.Cacheable;
13+
import org.springframework.context.ApplicationEventPublisher;
1314
import org.springframework.stereotype.Component;
1415
import org.springframework.transaction.annotation.Transactional;
1516

@@ -20,8 +21,8 @@
2021
@RequiredArgsConstructor
2122
public class ProductFacade {
2223
private final ProductRepository productRepository;
23-
private final LikeRepository likeRepository;
2424
private final BrandRepository brandRepository;
25+
private final ApplicationEventPublisher publisher;
2526

2627

2728
@Transactional
@@ -55,6 +56,9 @@ public ProductInfo findProductById(Long id) {
5556
() -> new CoreException(ErrorType.NOT_FOUND, "찾고자 하는 상품이 존재하지 않습니다.")
5657
);
5758

59+
// 유저 ID는 임시로 하드 코딩했습니다. 추후 인증/인가 기능이 추가되면 수정할 예정입니다.
60+
publisher.publishEvent(new UserActionEvent(1L, product.getId(), ActionType.PRODUCT_LOOKED_UP));
61+
5862
return ProductInfo.from(product);
5963
}
6064

0 commit comments

Comments
 (0)