Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.magicbox.generalgoods.adapter.in.kafka;

import kr.magicbox.generalgoods.adapter.in.kafka.annotation.Idempotent;
import kr.magicbox.generalgoods.adapter.in.kafka.event.StockReserveCommandEvent;
import kr.magicbox.generalgoods.application.port.in.HandleStockReserveUseCase;
import kr.magicbox.generalgoods.global.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.RetryableTopic;
import org.springframework.kafka.retrytopic.DltStrategy;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class OrchestratorCommandKafkaListener {

private final HandleStockReserveUseCase handleStockReserveUseCase;

@Idempotent
@RetryableTopic(dltStrategy = DltStrategy.FAIL_ON_ERROR, dltTopicSuffix = "-dlt", exclude = {BusinessException.class})
@KafkaListener(topics = "outbox.event.stock-reserve.general-good", groupId = "general-goods-service")
public void handleStockReserve(ConsumerRecord<String, StockReserveCommandEvent> consumerRecord) {
log.info("[Inbox] stock-reserve.general-good 커맨드 수신. key={}", consumerRecord.key());
handleStockReserveUseCase.handleStockReserve(consumerRecord.value());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kr.magicbox.orchestrator.domain.event;
package kr.magicbox.generalgoods.adapter.in.kafka.event;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
Expand All @@ -7,29 +7,19 @@
import java.util.List;

@Builder
public record StockReserveCommand(
@JsonProperty("event_id") Long eventId,
public record StockReserveCommandEvent(
@JsonProperty("order_id") Long orderId,
@JsonProperty("customer_id") Long customerId,
@JsonProperty("total_amount") Long totalAmount,
@JsonProperty("items") List<ItemPayload> items,
@JsonProperty("occurred_at") Instant occurredAt
) implements OrchestratorCommandEvent {
) implements InboxEvent {

@Builder
public record ItemPayload(
@JsonProperty("order_line_id") Long orderLineId,
@JsonProperty("product_id") Long productId,
@JsonProperty("quantity") int quantity,
@JsonProperty("unit_price") long unitPrice
) {}

@Override
public String key() {
return orderId.toString();
}

@Override
public OrchestratorCommandEventType eventType() {
return OrchestratorCommandEventType.STOCK_RESERVE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,9 @@ public List<GeneralGoods> findAllByCursor(Long cursorId, int size) {
.map(generalGoodsMapper::toDomain)
.toList();
}

@Override
public boolean decreaseStock(Long productId, long quantity) {
return generalGoodsJpaRepository.decreaseStock(productId, quantity) > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class GeneralGoodsEntity extends BaseEntity {

@ElementCollection
@CollectionTable(name = "general_goods_category", joinColumns = @JoinColumn(name = "general_goods_id"))
@Enumerated(EnumType.STRING)
@Column(name = "category")
private Set<MagicGenre> categories;

Expand All @@ -48,9 +49,6 @@ public class GeneralGoodsEntity extends BaseEntity {
@Column(name = "is_deleted", nullable = false)
private boolean isDeleted = false;

@Version
private Integer version;

@Builder
public GeneralGoodsEntity(Long creatorId, String name, Long price, Long stock, String description, GeneralGoodsLevel level, Set<MagicGenre> categories) {
this.creatorId = creatorId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ public interface GeneralGoodsJpaRepository extends JpaRepository<GeneralGoodsEnt
@Modifying
@Query("UPDATE GeneralGoodsEntity g SET g.isDeleted = true WHERE g.creatorId = :creatorId AND g.isDeleted = false")
void softDeleteByCreatorId(@Param("creatorId") Long creatorId);

@Modifying
@Query("UPDATE GeneralGoodsEntity g SET g.stock = g.stock - :quantity WHERE g.id = :id AND g.stock >= :quantity AND g.isDeleted = false")
int decreaseStock(@Param("id") Long id, @Param("quantity") long quantity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.magicbox.generalgoods.application.port.in;

import kr.magicbox.generalgoods.adapter.in.kafka.event.StockReserveCommandEvent;

public interface HandleStockReserveUseCase {
void handleStockReserve(StockReserveCommandEvent event);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public interface GeneralGoodsRepositoryPort {
GeneralGoods findById(GeneralGoodsId id);

List<GeneralGoods> findAllByCursor(Long cursorId, int size);

boolean decreaseStock(Long productId, long quantity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package kr.magicbox.generalgoods.application.service;

import kr.magicbox.generalgoods.adapter.in.kafka.event.StockReserveCommandEvent;
import kr.magicbox.generalgoods.application.port.in.HandleStockReserveUseCase;
import kr.magicbox.generalgoods.application.port.out.GeneralGoodsOutboxPort;
import kr.magicbox.generalgoods.application.port.out.GeneralGoodsRepositoryPort;
import kr.magicbox.generalgoods.domain.event.StockReserveFailedEvent;
import kr.magicbox.generalgoods.domain.event.StockReserveSucceededEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;

@Slf4j
@Service
@RequiredArgsConstructor
public class HandleStockReserveService implements HandleStockReserveUseCase {

private final GeneralGoodsRepositoryPort generalGoodsRepositoryPort;
private final GeneralGoodsOutboxPort generalGoodsOutboxPort;

@Transactional
@Override
public void handleStockReserve(StockReserveCommandEvent event) {
if (event.items() == null || event.items().isEmpty()) {
log.warn("[StockReserve] items 없음. orderId={}", event.orderId());
generalGoodsOutboxPort.save(StockReserveFailedEvent.builder()
.orderId(event.orderId())
.customerId(event.customerId())
.reason("items 없음")
.occurredAt(Instant.now())
.build());
return;
}

for (StockReserveCommandEvent.ItemPayload item : event.items()) {
boolean decreased = generalGoodsRepositoryPort.decreaseStock(item.productId(), item.quantity());
if (!decreased) {
log.warn("[StockReserve] 재고 부족. orderId={}, productId={}", event.orderId(), item.productId());
generalGoodsOutboxPort.save(StockReserveFailedEvent.builder()
.orderId(event.orderId())
.customerId(event.customerId())
.reason("재고 부족. productId=" + item.productId())
.occurredAt(Instant.now())
.build());
return;
}
}

log.info("[StockReserve] 재고 예약 성공. orderId={}", event.orderId());
generalGoodsOutboxPort.save(StockReserveSucceededEvent.builder()
.orderId(event.orderId())
.customerId(event.customerId())
.totalAmount(event.totalAmount())
.occurredAt(Instant.now())
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public enum GeneralGoodsDomainEventType {

GENERAL_GOODS_CREATED("general-goods-created"),
GENERAL_GOODS_UPDATED("general-goods-updated"),
GENERAL_GOODS_DELETED("general-goods-deleted");
GENERAL_GOODS_DELETED("general-goods-deleted"),
STOCK_RESERVE_SUCCEEDED("stock-reserve-general-goods-succeeded"),
STOCK_RESERVE_FAILED("stock-reserve-general-goods-failed");

private final String value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.magicbox.generalgoods.domain.event;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;

import java.time.Instant;

@Builder
public record StockReserveFailedEvent(
@JsonProperty("order_id") Long orderId,
@JsonProperty("customer_id") Long customerId,
@JsonProperty("reason") String reason,
@JsonProperty("occurred_at") Instant occurredAt
) implements GeneralGoodsDomainEvent {

@Override
public String key() {
return orderId.toString();
}

@Override
public GeneralGoodsDomainEventType eventType() {
return GeneralGoodsDomainEventType.STOCK_RESERVE_FAILED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.magicbox.generalgoods.domain.event;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;

import java.time.Instant;

@Builder
public record StockReserveSucceededEvent(
@JsonProperty("order_id") Long orderId,
@JsonProperty("customer_id") Long customerId,
@JsonProperty("total_amount") Long totalAmount,
@JsonProperty("occurred_at") Instant occurredAt
) implements GeneralGoodsDomainEvent {

@Override
public String key() {
return orderId.toString();
}

@Override
public GeneralGoodsDomainEventType eventType() {
return GeneralGoodsDomainEventType.STOCK_RESERVE_SUCCEEDED;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package kr.magicbox.notification.adapter.in.web.controller;

import kr.magicbox.notification.adapter.in.web.dto.request.ReadNotificationsRequest;
import kr.magicbox.notification.adapter.in.web.dto.request.RegisterFcmTokenRequest;
import kr.magicbox.notification.application.dto.command.ReadNotificationCommand;
import kr.magicbox.notification.application.dto.command.RegisterFcmTokenCommand;
import kr.magicbox.notification.application.port.in.ReadNotificationUseCase;
import kr.magicbox.notification.application.port.in.RegisterFcmTokenUseCase;
import kr.magicbox.notification.domain.vo.UserId;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -18,6 +22,7 @@
public class NotificationCommandController {

private final RegisterFcmTokenUseCase registerFcmTokenUseCase;
private final ReadNotificationUseCase readNotificationUseCase;

@PostMapping("/fcm-token")
public ResponseEntity<Void> registerFcmToken(
Expand All @@ -26,4 +31,12 @@ public ResponseEntity<Void> registerFcmToken(
registerFcmTokenUseCase.register(RegisterFcmTokenCommand.of(userId.value(), request.token()));
return ResponseEntity.ok().build();
}

@PatchMapping("/read")
public ResponseEntity<Void> readNotifications(
@AuthenticationPrincipal UserId userId,
@RequestBody ReadNotificationsRequest request) {
readNotificationUseCase.readAll(ReadNotificationCommand.of(request.notificationIds(), userId.value()));
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.magicbox.notification.adapter.in.web.dto.request;

import java.util.List;

public record ReadNotificationsRequest(List<Long> notificationIds) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import kr.magicbox.notification.adapter.out.persistence.repository.NotificationJpaRepository;
import kr.magicbox.notification.application.port.out.NotificationRepositoryPort;
import kr.magicbox.notification.domain.aggregate.Notification;
import kr.magicbox.notification.domain.enums.NotificationStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
@RequiredArgsConstructor
Expand All @@ -20,6 +22,24 @@ public void save(Notification notification) {
notificationJpaRepository.save(NotificationEntity.from(notification));
}

@Override
public void update(Notification notification) {
notificationJpaRepository.findByIdAndUserId(
notification.getId().value(), notification.getUserId().value())
.ifPresent(NotificationEntity::markRead);
}

@Override
public void updateAllByIdsAndUserId(List<Long> notificationIds, Long userId) {
notificationJpaRepository.updateStatusByIdsAndUserId(notificationIds, userId, NotificationStatus.READ);
}

@Override
public Optional<Notification> findByIdAndUserId(Long notificationId, Long userId) {
return notificationJpaRepository.findByIdAndUserId(notificationId, userId)
.map(NotificationEntity::toDomain);
}

@Override
public List<Notification> findAllByUserId(Long userId) {
return notificationJpaRepository.findAllByUserIdOrderByCreatedAtDesc(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package kr.magicbox.notification.adapter.out.persistence.repository;

import kr.magicbox.notification.adapter.out.persistence.entity.NotificationEntity;
import kr.magicbox.notification.domain.enums.NotificationStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface NotificationJpaRepository extends JpaRepository<NotificationEntity, Long> {
List<NotificationEntity> findAllByUserIdOrderByCreatedAtDesc(Long userId);
Optional<NotificationEntity> findByIdAndUserId(Long id, Long userId);

@Modifying
@Query("UPDATE NotificationEntity n SET n.status = :status WHERE n.id IN :ids AND n.userId = :userId")
void updateStatusByIdsAndUserId(@Param("ids") List<Long> ids, @Param("userId") Long userId, @Param("status") NotificationStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.magicbox.notification.application.dto.command;

import java.util.List;

public record ReadNotificationCommand(
List<Long> notificationIds,
Long userId
) {
public static ReadNotificationCommand of(List<Long> notificationIds, Long userId) {
return new ReadNotificationCommand(notificationIds, userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.magicbox.notification.application.port.in;

import kr.magicbox.notification.application.dto.command.ReadNotificationCommand;

public interface ReadNotificationUseCase {
void readAll(ReadNotificationCommand command);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
import kr.magicbox.notification.domain.aggregate.Notification;

import java.util.List;
import java.util.Optional;

public interface NotificationRepositoryPort {
void save(Notification notification);
void update(Notification notification);
void updateAllByIdsAndUserId(List<Long> notificationIds, Long userId);
Optional<Notification> findByIdAndUserId(Long notificationId, Long userId);
List<Notification> findAllByUserId(Long userId);
}
Loading
Loading