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,