Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {
// add-ons
implementation(project(":modules:jpa"))
implementation(project(":modules:redis"))
implementation(project(":modules:kafka"))
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))
Expand Down Expand Up @@ -31,4 +32,5 @@ dependencies {
// test-fixtures
testImplementation(testFixtures(project(":modules:jpa")))
testImplementation(testFixtures(project(":modules:redis")))
testImplementation(testFixtures(project(":modules:kafka")))
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
package com.loopers.application.coupon;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.loopers.application.kafka.KafkaTopic;
import com.loopers.domain.coupon.Coupon;
import com.loopers.domain.coupon.CouponService;
import com.loopers.domain.coupon.IssuableCoupon;
import com.loopers.domain.coupon.MemberCoupon;
import com.loopers.domain.coupon.MemberCouponService;
import com.loopers.domain.coupon.MemberCouponStatus;
import com.loopers.domain.coupon.MemberCouponStatusCounts;
import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberService;
import com.loopers.infrastructure.coupon.CouponIssueStatusRedisRepository;
import com.loopers.support.auth.AdminValidator;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Component
@RequiredArgsConstructor
public class CouponFacade {
Expand All @@ -25,6 +39,9 @@ public class CouponFacade {
private final MemberCouponService memberCouponService;
private final MemberService memberService;
private final AdminValidator adminValidator;
private final KafkaTemplate<Object, Object> kafkaTemplate;
private final ObjectMapper objectMapper;
private final CouponIssueStatusRedisRepository couponIssueStatusRedisRepository;

@Transactional(readOnly = true)
public Page<CouponDetailInfo> getCouponsForAdmin(String ldap, Pageable pageable) {
Expand Down Expand Up @@ -106,6 +123,39 @@ public MemberCouponInfo issueCoupon(String loginId, String loginPw, Long couponI
return MemberCouponInfo.from(memberCoupon);
}

public CouponIssueRequestInfo requestCouponIssueAsync(String loginId, String loginPw, Long couponId) {
Member member = memberService.authenticate(loginId, loginPw);
couponService.validateCouponExists(couponId);

String requestId = UUID.randomUUID().toString();

try {
Map<String, Object> message = new LinkedHashMap<>();
message.put("requestId", requestId);
message.put("memberId", member.getId());
message.put("couponId", couponId);

String payload = objectMapper.writeValueAsString(message);
kafkaTemplate.send(KafkaTopic.COUPON_ISSUE_REQUESTS, String.valueOf(couponId), payload).get();
} catch (JsonProcessingException e) {
throw new IllegalStateException("Kafka 메시지 직렬화 실패", e);
} catch (Exception e) {
log.error("쿠폰 발급 요청 Kafka 발행 실패: couponId={}, error={}", couponId, e.getMessage());
throw new CoreException(ErrorType.SERVICE_UNAVAILABLE);
}

return CouponIssueRequestInfo.pending(requestId, couponId);
}

public CouponIssueRequestStatusInfo getCouponIssueRequestStatus(String loginId, String loginPw, String requestId) {
memberService.authenticate(loginId, loginPw);

Optional<String> status = couponIssueStatusRedisRepository.getRequestStatus(requestId);
return status
.map(s -> CouponIssueRequestStatusInfo.of(requestId, s, null))
.orElse(CouponIssueRequestStatusInfo.pending(requestId));
}

@Transactional(readOnly = true)
public MemberCouponListInfo getMyCoupons(String loginId, String loginPw, MemberCouponStatus status, Pageable pageable) {
var member = memberService.authenticate(loginId, loginPw);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.loopers.application.coupon;

public record CouponIssueRequestInfo(
String requestId,
Long couponId,
String status
) {

public static CouponIssueRequestInfo pending(String requestId, Long couponId) {
return new CouponIssueRequestInfo(requestId, couponId, "PENDING");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.loopers.application.coupon;

public record CouponIssueRequestStatusInfo(
String requestId,
String status,
Long couponId
) {

public static CouponIssueRequestStatusInfo of(String requestId, String status, Long couponId) {
return new CouponIssueRequestStatusInfo(requestId, status, couponId);
}

public static CouponIssueRequestStatusInfo pending(String requestId) {
return new CouponIssueRequestStatusInfo(requestId, "PENDING", null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.loopers.application.event;

public enum ActionType {
VIEW,
LIKE,
ORDER,
PAYMENT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.loopers.application.event;

import java.util.Set;

public record OrderCompletedEvent(
Long orderId,
Long memberId,
Set<Long> productIds,
Long totalAmount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.loopers.application.event;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.loopers.domain.actionlog.UserActionLog;
import com.loopers.domain.actionlog.UserActionLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.Map;

@Slf4j
@Component
public class OrderEventListener {

private final UserActionLogRepository userActionLogRepository;
private final ObjectMapper objectMapper;
private final TransactionTemplate transactionTemplate;

public OrderEventListener(UserActionLogRepository userActionLogRepository,
ObjectMapper objectMapper,
PlatformTransactionManager transactionManager) {
this.userActionLogRepository = userActionLogRepository;
this.objectMapper = objectMapper;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCompleted(OrderCompletedEvent event) {
transactionTemplate.executeWithoutResult(status -> {
UserActionLog actionLog = new UserActionLog(
event.memberId(),
ActionType.ORDER.name(),
event.orderId(),
"ORDER",
toJson(Map.of("totalAmount", event.totalAmount()))
);
userActionLogRepository.save(actionLog);
});
}

private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new IllegalStateException("JSON 직렬화 실패", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.loopers.application.event;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.loopers.domain.actionlog.UserActionLog;
import com.loopers.domain.actionlog.UserActionLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.Map;

@Slf4j
@Component
public class PaymentEventListener {

private final UserActionLogRepository userActionLogRepository;
private final ObjectMapper objectMapper;
private final TransactionTemplate transactionTemplate;

public PaymentEventListener(UserActionLogRepository userActionLogRepository,
ObjectMapper objectMapper,
PlatformTransactionManager transactionManager) {
this.userActionLogRepository = userActionLogRepository;
this.objectMapper = objectMapper;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlePaymentSuccess(PaymentSuccessEvent event) {
transactionTemplate.executeWithoutResult(status -> {
UserActionLog actionLog = new UserActionLog(
event.memberId(),
ActionType.PAYMENT.name(),
event.paymentId(),
"PAYMENT",
toJson(Map.of("orderId", event.orderId(), "amount", event.amount()))
);
userActionLogRepository.save(actionLog);
});
}

private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new IllegalStateException("JSON 직렬화 실패", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.loopers.application.event;

public record PaymentSuccessEvent(
Long paymentId,
Long orderId,
Long memberId,
Long amount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.loopers.application.event;

import com.loopers.domain.product.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ProductLikeCountListener {

private final ProductService productService;

@EventListener
public void handleProductLiked(ProductLikedEvent event) {
productService.increaseLikeCount(event.productId());
}

@EventListener
public void handleProductUnliked(ProductUnlikedEvent event) {
productService.decreaseLikeCount(event.productId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.application.event;

public record ProductLikedEvent(
Long productId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.loopers.application.event;

public record ProductUnlikedEvent(
Long productId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.loopers.application.event;

public record UserActionEvent(
Long memberId,
ActionType actionType,
Long targetId,
String targetType,
String metadata
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.loopers.application.event;

import com.loopers.domain.actionlog.UserActionLog;
import com.loopers.domain.actionlog.UserActionLogRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionTemplate;

@Component
public class UserActionEventListener {

private final UserActionLogRepository userActionLogRepository;
private final TransactionTemplate transactionTemplate;

public UserActionEventListener(UserActionLogRepository userActionLogRepository,
PlatformTransactionManager transactionManager) {
this.userActionLogRepository = userActionLogRepository;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserAction(UserActionEvent event) {
transactionTemplate.executeWithoutResult(status -> {
UserActionLog log = new UserActionLog(
event.memberId(),
event.actionType().name(),
event.targetId(),
event.targetType(),
event.metadata()
);
userActionLogRepository.save(log);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.loopers.application.kafka;

public final class KafkaTopic {

private KafkaTopic() {}

public static final String CATALOG_EVENTS = "catalog-events";
public static final String ORDER_EVENTS = "order-events";
public static final String COUPON_ISSUE_REQUESTS = "coupon-issue-requests";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.loopers.application.kafka;

public final class OutboxAggregateType {

private OutboxAggregateType() {}

public static final String PRODUCT = "PRODUCT";
public static final String ORDER = "ORDER";
public static final String COUPON = "COUPON";
}
Loading