Skip to content
Merged
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,53 @@
package com.cleanengine.coin.trade.application;

import com.cleanengine.coin.trade.entity.Trade;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;

import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID;
import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID;

@Slf4j
@Component
public class TradeExecutedNotificationHandler {

private final SimpMessagingTemplate messagingTemplate;

private static final String ASK = "ask"; // 매도
private static final String BID = "bid"; // 매수

public TradeExecutedNotificationHandler(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

@TransactionalEventListener
public void notifyAfterTradeExecuted(TradeExecutedEvent tradeExecutedEvent) {
Trade trade = tradeExecutedEvent.getTrade();
if (trade == null) {
log.error("체결 알림 실패! trade == null");
return ;
}

Integer sellUserId = trade.getSellUserId();
Integer buyUserId = trade.getBuyUserId();
if (sellUserId == null || buyUserId == null) {
log.error("체결 알림 실패! sellUserId: {}, buyUserId: {}", sellUserId, buyUserId);
return ;
}

if (sellUserId != SELL_ORDER_BOT_ID) {
TradeExecutedNotifyDto soldDto = TradeExecutedNotifyDto.of(trade, ASK);
messagingTemplate.convertAndSend("/topic/tradeNotification/" + sellUserId, soldDto);
}
if (buyUserId != BUY_ORDER_BOT_ID) {
TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID);
messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto);
}
if (sellUserId != SELL_ORDER_BOT_ID || buyUserId != BUY_ORDER_BOT_ID) {
log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId );
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.cleanengine.coin.trade.application;

import com.cleanengine.coin.trade.entity.Trade;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@JsonPropertyOrder({"ticker", "price", "size", "type", "tradedTime"})
public class TradeExecutedNotifyDto {

private String ticker;

private Double price;

private Double size;

private String type;

private LocalDateTime tradedTime;

@Builder
private TradeExecutedNotifyDto(String ticker, Double price, Double size, String type, LocalDateTime tradedTime) {
this.ticker = ticker;
this.price = price;
this.size = size;
this.type = type;
this.tradedTime = tradedTime;
}

public static TradeExecutedNotifyDto of(Trade trade, String type) {
return TradeExecutedNotifyDto.builder()
.ticker(trade.getTicker())
.price(trade.getPrice())
.size(trade.getSize())
.type(type)
.tradedTime(trade.getTradeTime())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.cleanengine.coin.trade.application;

import static com.cleanengine.coin.common.CommonValues.BUY_ORDER_BOT_ID;
import static com.cleanengine.coin.common.CommonValues.SELL_ORDER_BOT_ID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

import com.cleanengine.coin.trade.entity.Trade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.messaging.simp.SimpMessagingTemplate;

import java.time.LocalDateTime;

@DisplayName("체결 알림 단위테스트")
@ExtendWith(MockitoExtension.class)
class TradeExecutedNotificationHandlerTest {

@Mock
private SimpMessagingTemplate messagingTemplate;

private TradeExecutedNotificationHandler handler;

@BeforeEach
void setUp() {
handler = new TradeExecutedNotificationHandler(messagingTemplate);
}

@DisplayName("매도인은 봇인 정상 체결내역을 리스닝하면 웹소켓으로 전송한다.")
@Test
void shouldSendNotificationsForValidTrade() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), 3, SELL_ORDER_BOT_ID, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verify(messagingTemplate, times(1)).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class));
verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class));
}

@DisplayName("매수인은 봇인 정상 체결내역을 리스닝하면 웹소켓으로 전송한다.")
@Test
void shouldSendNotificationsForValidTrade2() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, 3, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verify(messagingTemplate, times(1)).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class));
verify(messagingTemplate).convertAndSend(eq("/topic/tradeNotification/3"), any(TradeExecutedNotifyDto.class));
}

@DisplayName("매수인과 매도인의 userId가 null이면 메시지를 전송하지 않는다.")
@Test
void shouldNotSendNotificationForNullUserIds() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), null, null, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verifyNoInteractions(messagingTemplate);
}

@DisplayName("매수인의 userId가 null이면 메시지를 전송하지 않는다.")
@Test
void shouldNotSendNotificationForNullBuyUserId() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), null, SELL_ORDER_BOT_ID, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verifyNoInteractions(messagingTemplate);
}

@DisplayName("매도인의 userId가 null이면 메시지를 전송하지 않는다.")
@Test
void shouldNotSendNotificationForNullSellUserId() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, null, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verifyNoInteractions(messagingTemplate);
}

@DisplayName("봇끼리의 체결은 메시지를 전송하지 않는다.")
@Test
void shouldNotSendNotificationForBotTrade() {
// given
Trade trade = Trade.of("BTC", LocalDateTime.now(), BUY_ORDER_BOT_ID, SELL_ORDER_BOT_ID, 50000.0, 1.0);
TradeExecutedEvent event = TradeExecutedEvent.of(trade, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verifyNoInteractions(messagingTemplate);
}

@DisplayName("체결이 null이면 메시지를 전송하지 않는다.")
@Test
void shouldNotSendNotificationForNullTrade() {
// given
TradeExecutedEvent event = TradeExecutedEvent.of(null, null, null);

// when
handler.notifyAfterTradeExecuted(event);

// then
verifyNoInteractions(messagingTemplate);
}

}
Loading