diff --git a/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/NotificationService.java b/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/NotificationService.java index d0b4991..5faa8f9 100644 --- a/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/NotificationService.java +++ b/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/NotificationService.java @@ -50,7 +50,7 @@ public class NotificationService { """ + ChatConstants.HELP_SUGGESTION; private static final int GREETING_NOTIFICATION_ORDER = Ordered.HIGHEST_PRECEDENCE; - private static final int USER_INTERACTION_NOTIFICATION_ORDER = GREETING_NOTIFICATION_ORDER + 1000; + static final int USER_INTERACTION_NOTIFICATION_ORDER = GREETING_NOTIFICATION_ORDER + 1000; private static final int DAILY_RANKING_NOTIFICATION_ORDER = 0; private final CustomTelegramClient customTelegramClient; diff --git a/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationService.java b/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationService.java new file mode 100644 index 0000000..327b2a3 --- /dev/null +++ b/src/main/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationService.java @@ -0,0 +1,66 @@ +package dev.rubasace.linkedin.games.ldrbot.chat; + +import dev.rubasace.linkedin.games.ldrbot.configuration.ExecutorsConfiguration; +import dev.rubasace.linkedin.games.ldrbot.session.GameSession; +import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRegistrationEvent; +import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRepository; +import dev.rubasace.linkedin.games.ldrbot.util.FormatUtils; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.Duration; +import java.util.List; + +@Service +public class RecordNotificationService { + + private static final String NEW_RECORD_MESSAGE = "🏆 New record! %s completed %s in %s, beating the previous record of %s!"; + private static final String FIRST_RECORD_MESSAGE = "🏆 New record! %s completed %s in %s!"; + + private final GameSessionRepository gameSessionRepository; + private final CustomTelegramClient customTelegramClient; + + RecordNotificationService(final GameSessionRepository gameSessionRepository, final CustomTelegramClient customTelegramClient) { + this.gameSessionRepository = gameSessionRepository; + this.customTelegramClient = customTelegramClient; + } + + @Order(NotificationService.USER_INTERACTION_NOTIFICATION_ORDER) + @Async(ExecutorsConfiguration.NOTIFICATION_LISTENER_EXECUTOR_NAME) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + void handleRecordNotification(final GameSessionRegistrationEvent event) { + List top2Sessions = gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc( + event.getChatId(), + event.getGameInfo().type() + ); + + if (top2Sessions.isEmpty()) { + return; + } + + GameSession bestSession = top2Sessions.get(0); + Duration bestDuration = bestSession.getDuration(); + + if (!bestDuration.equals(event.getDuration())) { + return; + } + + String userMention = FormatUtils.formatUserMention(event.getUserInfo()); + String gameName = event.getGameInfo().name(); + String formattedDuration = FormatUtils.formatDuration(bestDuration); + + String message; + if (top2Sessions.size() == 1) { + message = FIRST_RECORD_MESSAGE.formatted(userMention, gameName, formattedDuration); + } else { + Duration previousRecord = top2Sessions.get(1).getDuration(); + String formattedPreviousRecord = FormatUtils.formatDuration(previousRecord); + message = NEW_RECORD_MESSAGE.formatted(userMention, gameName, formattedDuration, formattedPreviousRecord); + } + + customTelegramClient.sendMessage(message, event.getChatInfo().chatId()); + } +} diff --git a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameInfo.java b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameInfo.java index 5eaef5d..2ddb025 100644 --- a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameInfo.java +++ b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameInfo.java @@ -1,5 +1,5 @@ package dev.rubasace.linkedin.games.ldrbot.session; -public record GameInfo(String name, String icon) { +public record GameInfo(GameType type, String name, String icon) { } diff --git a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameSessionRepository.java b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameSessionRepository.java index 320aeb8..79939dc 100644 --- a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameSessionRepository.java +++ b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameSessionRepository.java @@ -4,6 +4,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -25,4 +26,6 @@ public interface GameSessionRepository extends JpaRepository @Transactional void deleteByUserIdAndGroupChatIdAndGameDay(Long UserId, Long chatId, LocalDate gameDay); + + List findTop2ByGroupChatIdAndGameOrderByDurationAsc(Long chatId, GameType game); } diff --git a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameTypeAdapter.java b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameTypeAdapter.java index 6a2f277..598d947 100644 --- a/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameTypeAdapter.java +++ b/src/main/java/dev/rubasace/linkedin/games/ldrbot/session/GameTypeAdapter.java @@ -7,7 +7,7 @@ public class GameTypeAdapter { public GameInfo adapt(final GameType gameType) { - return new GameInfo(StringUtils.capitalize(gameType.name().toLowerCase()), gameIcon(gameType)); + return new GameInfo(gameType, StringUtils.capitalize(gameType.name().toLowerCase()), gameIcon(gameType)); } private static String gameIcon(final GameType gameType) { diff --git a/src/test/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationServiceTest.java b/src/test/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationServiceTest.java new file mode 100644 index 0000000..6c2d653 --- /dev/null +++ b/src/test/java/dev/rubasace/linkedin/games/ldrbot/chat/RecordNotificationServiceTest.java @@ -0,0 +1,160 @@ +package dev.rubasace.linkedin.games.ldrbot.chat; + +import dev.rubasace.linkedin.games.ldrbot.group.ChatInfo; +import dev.rubasace.linkedin.games.ldrbot.session.GameInfo; +import dev.rubasace.linkedin.games.ldrbot.session.GameSession; +import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRegistrationEvent; +import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRepository; +import dev.rubasace.linkedin.games.ldrbot.session.GameType; +import dev.rubasace.linkedin.games.ldrbot.user.UserInfo; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RecordNotificationServiceTest { + + @Mock + private GameSessionRepository gameSessionRepository; + + @Mock + private CustomTelegramClient customTelegramClient; + + @InjectMocks + private RecordNotificationService recordNotificationService; + + @Test + void shouldNotNotifyWhenNotARecord() { + GameSessionRegistrationEvent event = createEvent(Duration.ofMinutes(2)); + GameSession bestSession = createGameSession(Duration.ofMinutes(1)); + + when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS))) + .thenReturn(List.of(bestSession)); + + recordNotificationService.handleRecordNotification(event); + + verify(customTelegramClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), anyLong()); + } + + @Test + void shouldNotifyWhenFirstRecord() { + Duration recordDuration = Duration.ofMinutes(1); + GameSessionRegistrationEvent event = createEvent(recordDuration); + GameSession bestSession = createGameSession(recordDuration); + + when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS))) + .thenReturn(List.of(bestSession)); + + recordNotificationService.handleRecordNotification(event); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L)); + + String message = messageCaptor.getValue(); + assertTrue(message.contains("🏆")); + assertTrue(message.contains("New record")); + assertTrue(message.contains("@testuser")); + assertTrue(message.contains("Queens")); + assertTrue(message.contains("01:00")); + assertFalse(message.contains("beating")); + } + + @Test + void shouldNotifyWhenBrokenRecord() { + Duration recordDuration = Duration.ofMinutes(1); + Duration previousRecord = Duration.ofMinutes(2); + GameSessionRegistrationEvent event = createEvent(recordDuration); + GameSession bestSession = createGameSession(recordDuration); + GameSession secondBestSession = createGameSession(previousRecord); + + when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS))) + .thenReturn(List.of(bestSession, secondBestSession)); + + recordNotificationService.handleRecordNotification(event); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L)); + + String message = messageCaptor.getValue(); + assertTrue(message.contains("🏆")); + assertTrue(message.contains("New record")); + assertTrue(message.contains("@testuser")); + assertTrue(message.contains("Queens")); + assertTrue(message.contains("01:00")); + assertTrue(message.contains("beating")); + assertTrue(message.contains("02:00")); + } + + @Test + void shouldHandleUserWithoutUsername() { + Duration recordDuration = Duration.ofMinutes(1); + UserInfo userWithoutUsername = new UserInfo(456L, null, "John", "Doe"); + GameSessionRegistrationEvent event = createEventWithUser(recordDuration, userWithoutUsername); + GameSession bestSession = createGameSession(recordDuration); + + when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS))) + .thenReturn(List.of(bestSession)); + + recordNotificationService.handleRecordNotification(event); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L)); + + String message = messageCaptor.getValue(); + assertTrue(message.contains("")); + assertTrue(message.contains("John Doe")); + } + + @Test + void shouldNotNotifyWhenNoSessions() { + GameSessionRegistrationEvent event = createEvent(Duration.ofMinutes(1)); + + when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS))) + .thenReturn(Collections.emptyList()); + + recordNotificationService.handleRecordNotification(event); + + verify(customTelegramClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), anyLong()); + } + + private GameSessionRegistrationEvent createEvent(final Duration duration) { + UserInfo userInfo = new UserInfo(456L, "testuser", "Test", "User"); + return createEventWithUser(duration, userInfo); + } + + private GameSessionRegistrationEvent createEventWithUser(final Duration duration, final UserInfo userInfo) { + ChatInfo chatInfo = new ChatInfo(123L, "Test Group", true); + GameInfo gameInfo = new GameInfo(GameType.QUEENS, "Queens", "👑"); + return new GameSessionRegistrationEvent( + this, + chatInfo, + userInfo, + gameInfo, + duration, + LocalDate.now(), + 123L + ); + } + + private GameSession createGameSession(final Duration duration) { + GameSession session = new GameSession(); + session.setDuration(duration); + return session; + } +}