diff --git a/pom.xml b/pom.xml index e1842d3..4e1e81a 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,11 @@ lombok true + + org.springframework + spring-web + 6.2.0 + org.postgresql postgresql diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/TrackmycoinApplication.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/TrackmycoinApplication.java index 1f95d9d..23f33c4 100644 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/TrackmycoinApplication.java +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/TrackmycoinApplication.java @@ -4,8 +4,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling @EnableConfigurationProperties(TelegramBotSettings.class) public class TrackmycoinApplication { diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/BinanceApiClient.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/BinanceApiClient.java new file mode 100644 index 0000000..83692a9 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/BinanceApiClient.java @@ -0,0 +1,50 @@ +package com.vladyslavpalamarchuk.trackmycoin.adaptors.api; + +import com.vladyslavpalamarchuk.trackmycoin.adaptors.api.DTO.BinancePriceResponse; +import java.math.BigDecimal; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BinanceApiClient { + private final String BINANCE_URL = "https://api.binance.com/api/v3/ticker/price?symbol="; + private final RestTemplate restTemplate = new RestTemplate(); + private final String TICKER_CURRENCY = "USDT"; + + public boolean isCoinAvailable(String ticker) { + String url = BINANCE_URL + ticker.toUpperCase() + TICKER_CURRENCY; + try { + restTemplate.getForEntity(url, String.class); + return true; + } catch (HttpClientErrorException.NotFound e) { + log.warn("Coin not available: {}", ticker); + return false; + } catch (Exception e) { + log.error("Error while checking coin availability", e); + return false; + } + } + + public Optional getPrice(String ticker) { + String url = BINANCE_URL + ticker; + try { + RestTemplate restTemplate = new RestTemplate(); + BinancePriceResponse response = restTemplate.getForObject(url, BinancePriceResponse.class); + if (response != null && response.getPrice() != null) { + return Optional.of(response.getPrice()); + } else { + log.warn("Price not found for ticker: {}", ticker); + return Optional.empty(); + } + } catch (Exception e) { + log.error("Failed to fetch price for ticker: {}. Error: {}", ticker, e.getMessage(), e); + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/DTO/BinancePriceResponse.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/DTO/BinancePriceResponse.java new file mode 100644 index 0000000..b4286f0 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/api/DTO/BinancePriceResponse.java @@ -0,0 +1,21 @@ +package com.vladyslavpalamarchuk.trackmycoin.adaptors.api.DTO; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import lombok.Setter; + +@Setter +public class BinancePriceResponse { + private String symbol; + private BigDecimal price; + + @JsonProperty("symbol") + public String getSymbol() { + return symbol; + } + + @JsonProperty("price") + public BigDecimal getPrice() { + return price; + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/MonitoringRepository.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/MonitoringRepository.java new file mode 100644 index 0000000..e3c9f93 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/MonitoringRepository.java @@ -0,0 +1,12 @@ +package com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence; + +import com.vladyslavpalamarchuk.trackmycoin.domain.Monitoring; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MonitoringRepository extends JpaRepository { + + List findByUserId(Long userId); +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/UserRepository.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/UserRepository.java new file mode 100644 index 0000000..6932dd9 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/UserRepository.java @@ -0,0 +1,14 @@ +package com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence; + +import com.vladyslavpalamarchuk.trackmycoin.domain.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByChatId(Long chatId); + + User findUserByChatId(Long chatId); +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/package-info.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/package-info.java deleted file mode 100644 index dd23e31..0000000 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/persistence/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * This package contains persistence config for application TODO delete this file after adding new - * files - */ -package com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence; diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/telegram/TelegramBotConsumer.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/telegram/TelegramBotConsumer.java index 1a1b60e..a03fe68 100644 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/telegram/TelegramBotConsumer.java +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/adaptors/telegram/TelegramBotConsumer.java @@ -19,7 +19,9 @@ public class TelegramBotConsumer implements LongPollingSingleThreadUpdateConsume public void consume(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { logUpdateMessage(update.getMessage()); - commandProcessorRegistry.get(update.getMessage().getText()).process(update); + commandProcessorRegistry + .get(update.getMessage().getChatId(), update.getMessage().getText()) + .process(update); } } diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/Command.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/Command.java index 783f2e5..ccd060a 100644 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/Command.java +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/Command.java @@ -9,6 +9,8 @@ public enum Command { START("/start"), INFO("/info"), HELP("/help"), + GETMONITOR("/get"), + ADDMONITOR("/add"), NON_COMMAND(""); private final String command; diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/CommandProcessorRegistry.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/CommandProcessorRegistry.java index d17926f..28fe593 100644 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/CommandProcessorRegistry.java +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/CommandProcessorRegistry.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -12,22 +13,39 @@ public class CommandProcessorRegistry { private final Map commandToProcessors; + private final Map userLastCommands = new ConcurrentHashMap<>(); public CommandProcessorRegistry(List processors) { commandToProcessors = processors.stream() - .collect(Collectors.toMap(CommandProcessor::getCommand, Function.identity())); + .collect( + Collectors.toMap( + com.vladyslavpalamarchuk.trackmycoin.command.processor.CommandProcessor + ::getCommand, + Function.identity())); } - public CommandProcessor get(String messageText) { + public CommandProcessor get(Long chatId, String messageText) { + messageText = messageText.trim(); + if (!messageText.startsWith("/")) { + Command lastCommand = userLastCommands.get(chatId); + if (lastCommand == Command.ADDMONITOR) { + return commandToProcessors.get(Command.ADDMONITOR); + } return commandToProcessors.get(Command.NON_COMMAND); } - return Arrays.stream(Command.values()) - .filter(c -> c.getCommand().equalsIgnoreCase(messageText)) - .findFirst() - .map(commandToProcessors::get) - .orElse(commandToProcessors.get(Command.NON_COMMAND)); + // Знаходимо відповідну команду + String finalMessageText = messageText; + Command matchedCommand = + Arrays.stream(Command.values()) + .filter(c -> c.getCommand().equalsIgnoreCase(finalMessageText)) + .findFirst() + .orElse(Command.NON_COMMAND); + + userLastCommands.put(chatId, matchedCommand); + + return commandToProcessors.get(matchedCommand); } } diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/AddMonitoringCommandProcessor.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/AddMonitoringCommandProcessor.java new file mode 100644 index 0000000..4bbfe29 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/AddMonitoringCommandProcessor.java @@ -0,0 +1,95 @@ +package com.vladyslavpalamarchuk.trackmycoin.command.processor; + +import com.vladyslavpalamarchuk.trackmycoin.adaptors.api.BinanceApiClient; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient; +import com.vladyslavpalamarchuk.trackmycoin.command.Command; +import com.vladyslavpalamarchuk.trackmycoin.service.MonitoringService; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.meta.api.objects.Update; + +@Component +@RequiredArgsConstructor +public class AddMonitoringCommandProcessor implements CommandProcessor { + + private final BinanceApiClient binanceApiClient; + private final TelegramBotClient telegramBotClient; + private final MonitoringService monitoringService; + private final String ADD_COMMAND = "/add"; + private final int PART_LENGTH = 2; + + @Override + public void process(Update update) { + Long chatId = update.getMessage().getChatId(); + String userMessage = update.getMessage().getText(); + if (userMessage.equalsIgnoreCase(ADD_COMMAND)) { + telegramBotClient.sendMessage( + chatId, "Please enter the coin ticker and its current price (for example, BTC 50000):"); + return; + } + + handleTickerInput(update); + } + + private void handleTickerInput(Update update) { + Long chatId = getChatId(update); + + String[] inputParts = extractAndValidateInput(update, chatId); + + if (inputParts != null) { + processValidTicker(chatId, inputParts[0], inputParts[1]); + } + } + + private Long getChatId(Update update) { + return update.getMessage().getChatId(); + } + + private String[] extractAndValidateInput(Update update, Long chatId) { + String[] parts = update.getMessage().getText().split(" "); + if (parts.length != PART_LENGTH) { + telegramBotClient.sendMessage( + chatId, "Please enter two values: the coin ticker and its current price."); + return null; + } + return parts; + } + + private void processValidTicker(Long chatId, String ticker, String price) { + if (isCoinAvailable(ticker) && isPriceAvailable(price)) { + addTickerToMonitoring(ticker, price, chatId); + } else { + sendCoinNotFoundMessage(chatId); + } + } + + private boolean isCoinAvailable(String ticker) { + return binanceApiClient.isCoinAvailable(ticker); + } + + private boolean isPriceAvailable(String price) { + if (price == null || price.trim().isEmpty()) { + return false; + } + try { + new BigDecimal(price); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + private void addTickerToMonitoring(String ticker, String price, Long chatId) { + monitoringService.add(ticker, price, chatId); + } + + private void sendCoinNotFoundMessage(Long chatId) { + telegramBotClient.sendMessage(chatId, "Coin not found. Please try again."); + } + + @Override + public Command getCommand() { + return Command.ADDMONITOR; + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/GetMonitoringCommandProcessor.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/GetMonitoringCommandProcessor.java new file mode 100644 index 0000000..d357241 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/GetMonitoringCommandProcessor.java @@ -0,0 +1,33 @@ +package com.vladyslavpalamarchuk.trackmycoin.command.processor; + +import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.UserRepository; +import com.vladyslavpalamarchuk.trackmycoin.command.Command; +import com.vladyslavpalamarchuk.trackmycoin.service.MonitoringService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.meta.api.objects.Update; + +@Component +@RequiredArgsConstructor +public class GetMonitoringCommandProcessor implements CommandProcessor { + + private final UserRepository userRepository; + private final MonitoringService monitoringService; + + @Override + public void process(Update update) { + Long chatId = update.getMessage().getChatId(); + + userRepository + .findByChatId(chatId) + .ifPresent( + user -> { + monitoringService.get(user.getId()); + }); + } + + @Override + public Command getCommand() { + return Command.GETMONITOR; + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/StartCommandProcessor.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/StartCommandProcessor.java index f0dcc8a..96cdec7 100644 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/StartCommandProcessor.java +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/command/processor/StartCommandProcessor.java @@ -1,24 +1,42 @@ package com.vladyslavpalamarchuk.trackmycoin.command.processor; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.UserRepository; import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient; import com.vladyslavpalamarchuk.trackmycoin.command.Command; import com.vladyslavpalamarchuk.trackmycoin.config.TelegramBotDescription; +import com.vladyslavpalamarchuk.trackmycoin.domain.User; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.telegram.telegrambots.meta.api.objects.Update; +@Slf4j @Component @RequiredArgsConstructor public class StartCommandProcessor implements CommandProcessor { private final TelegramBotClient telegramBotClient; - private final TelegramBotDescription telegramBotDescription; + private final UserRepository userRepository; @Override public void process(Update update) { - telegramBotClient.sendMessage( - update.getMessage().getChatId(), telegramBotDescription.getStart()); + Long chatId = update.getMessage().getChatId(); + + userRepository + .findByChatId(chatId) + .ifPresentOrElse( + user -> { + telegramBotClient.sendMessage(chatId, telegramBotDescription.getStart()); + }, + () -> { + User newUser = new User(); + newUser.setChatId(chatId); + newUser.setCreatedBy("bot"); + newUser.setUpdatedBy("bot"); + userRepository.save(newUser); + telegramBotClient.sendMessage(chatId, telegramBotDescription.getStart()); + }); } @Override diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/Monitoring.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/Monitoring.java new file mode 100644 index 0000000..e8439d9 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/Monitoring.java @@ -0,0 +1,56 @@ +package com.vladyslavpalamarchuk.trackmycoin.domain; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "monitorings") +@Getter +@Setter +@NoArgsConstructor +public class Monitoring { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ticker", nullable = false, length = 100) + private String ticker; + + @Column(name = "target_price", nullable = false, precision = 18, scale = 8) + private BigDecimal targetPrice; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn( + name = "user_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_monitorings_users")) + private User user; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_by") + private String updatedBy; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/User.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/User.java new file mode 100644 index 0000000..c9c2cdb --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/User.java @@ -0,0 +1,45 @@ +package com.vladyslavpalamarchuk.trackmycoin.domain; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "chat_id", nullable = false) + private Long chatId; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "updated_by") + private String updatedBy; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/package-info.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/package-info.java deleted file mode 100644 index 8b02d69..0000000 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/domain/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** This package contains domain entities TODO delete this file after adding new files */ -package com.vladyslavpalamarchuk.trackmycoin.domain; diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/MonitoringService.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/MonitoringService.java new file mode 100644 index 0000000..5f98744 --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/MonitoringService.java @@ -0,0 +1,127 @@ +package com.vladyslavpalamarchuk.trackmycoin.service; + +import com.vladyslavpalamarchuk.trackmycoin.adaptors.api.BinanceApiClient; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.MonitoringRepository; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.UserRepository; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient; +import com.vladyslavpalamarchuk.trackmycoin.domain.Monitoring; +import com.vladyslavpalamarchuk.trackmycoin.domain.User; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Getter +@Slf4j +public class MonitoringService { + + private final MonitoringRepository monitoringRepository; + private final TelegramBotClient telegramBotClient; + private final UserRepository userRepository; + private final BinanceApiClient binanceApiClient; + private final NotificationService notificationService; + private final String USER_PREFIX = "User_"; + private final String TICKER_CURRENCY = "USDT"; + private final BigDecimal DECIMAL_FORMAT_TYPE = BigDecimal.valueOf(1); + + public void add(String ticker, String price, long chatId) { + Monitoring monitoring = new Monitoring(); + monitoring.setTicker(ticker.toUpperCase() + TICKER_CURRENCY); + + BigDecimal targetPrice = new BigDecimal(price); + monitoring.setTargetPrice(targetPrice); + + User user = userRepository.findUserByChatId(chatId); + monitoring.setUser(user); + monitoring.setCreatedBy(USER_PREFIX + chatId); + monitoring.setUpdatedBy(USER_PREFIX + chatId); + monitoringRepository.save(monitoring); + + telegramBotClient.sendMessage(chatId, "The coin is available! Monitoring successfully added."); + } + + public String get(Long userId) { + User user = + userRepository + .findById(userId) + .orElseThrow( + () -> new IllegalArgumentException("User with ID " + userId + " not found")); + + List monitorings = monitoringRepository.findByUserId(userId); + + if (monitorings.isEmpty()) { + telegramBotClient.sendMessage(user.getChatId(), "You have no active monitorings."); + return null; + } + + StringBuilder message = new StringBuilder("Your active monitorings:\n\n"); + + for (Monitoring monitoring : monitorings) { + if (DECIMAL_FORMAT_TYPE.compareTo(monitoring.getTargetPrice()) > 0) { + message.append( + String.format( + "%s: Target price %.8f\n\n", monitoring.getTicker(), monitoring.getTargetPrice())); + } else { + message.append( + String.format( + "%s: Target price %.2f\n\n", monitoring.getTicker(), monitoring.getTargetPrice())); + } + } + + telegramBotClient.sendMessage(user.getChatId(), message.toString()); + return null; + } + + @Scheduled(fixedRate = 1000) + public void monitoringCheckTask() { + try { + monitoringCheck(); + } catch (Exception e) { + log.warn("Error during monitoring check: {}", e.getMessage(), e); + } + } + + public void monitoringCheck() { + List monitorings = monitoringRepository.findAll(); + + if (monitorings.isEmpty()) { + return; + } + + Map> tickerToMonitorings = + monitorings.stream().collect(Collectors.groupingBy(Monitoring::getTicker)); + + tickerToMonitorings.forEach( + (ticker, monitoringList) -> { + Optional currentPrice = binanceApiClient.getPrice(ticker); + + currentPrice.ifPresent( + price -> { + monitoringList.forEach( + monitoring -> { + if (isTargetPriceReached(price, monitoring.getTargetPrice())) { + notificationService.notifyUser(monitoring); + } + }); + }); + }); + tickerToMonitorings.clear(); + } + + private boolean isTargetPriceReached(BigDecimal currentPrice, BigDecimal targetPrice) { + BigDecimal percentRange = targetPrice.multiply(new BigDecimal("0.001")); + BigDecimal lowerBound = targetPrice.subtract(percentRange); + BigDecimal upperBound = targetPrice.add(percentRange); + + return currentPrice.compareTo(lowerBound) >= 0 && currentPrice.compareTo(upperBound) <= 0; + } + +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/NotificationService.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/NotificationService.java new file mode 100644 index 0000000..f3c90dc --- /dev/null +++ b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/NotificationService.java @@ -0,0 +1,26 @@ +package com.vladyslavpalamarchuk.trackmycoin.service; + +import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.MonitoringRepository; +import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient; +import com.vladyslavpalamarchuk.trackmycoin.domain.Monitoring; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class NotificationService { + private final MonitoringRepository monitoringRepository; + private final TelegramBotClient telegramBotClient; + + @Transactional + public void notifyUser(Monitoring monitoring) { + BigDecimal targetPrice = monitoring.getTargetPrice(); + String message = + String.format("Target price reached for %s: %.2f", monitoring.getTicker(), targetPrice); + + telegramBotClient.sendMessage(monitoring.getUser().getChatId(), message); + monitoringRepository.delete(monitoring); + } +} diff --git a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/package-info.java b/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/package-info.java deleted file mode 100644 index c7c74f5..0000000 --- a/src/main/java/com/vladyslavpalamarchuk/trackmycoin/service/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * This package contains business logic of the application TODO delete this file after adding new - * files - */ -package com.vladyslavpalamarchuk.trackmycoin.service; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d3b2e20..83fd692 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,5 +25,5 @@ description: \nPerfect for traders and crypto enthusiasts! \nType /help for a list of all commands." - help: "List of commands 👾\nInfo -> /info\nHelp -> /help" + help: "List of commands 👾\nInfo -> /info\nHelp -> /help\nGet monitors -> /get\nAdd monitor -> /add" non_command: "non command /help " \ No newline at end of file diff --git a/src/main/resources/db.migration/V2__monitoring-table-creating.sql b/src/main/resources/db.migration/V2__monitoring-table-creating.sql index d5aecf6..4bf8ed9 100644 --- a/src/main/resources/db.migration/V2__monitoring-table-creating.sql +++ b/src/main/resources/db.migration/V2__monitoring-table-creating.sql @@ -2,7 +2,7 @@ CREATE TABLE monitorings ( id BIGSERIAL PRIMARY KEY, ticker VARCHAR(100) NOT NULL, - target_price DECIMAL(18, 2) NOT NULL, + target_price DECIMAL(18, 8) NOT NULL, user_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,