From 16adca5d6cc29daa333534c234246472673a981f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Filip=20Pit=C3=A1k?= <xpitak@fi.muni.cz>
Date: Sun, 21 Apr 2024 03:33:05 +0200
Subject: [PATCH] Add implementation for Exchange rates from public API

---
 .../ExchangeRateInitializer.java              | 39 ++++++++++++++++
 .../messaging/ProcessProducer.java            |  9 ----
 .../banking/application/proxy/Dummy.java      | 20 --------
 .../application/proxy/ExchangeRatesApi.java   | 10 ----
 .../rate/ExchangeRateResponseProcessor.java   | 46 +++++++++++++++++++
 .../proxy/rate/ExchangeRatesApi.java          | 16 +++++++
 .../service/ExchangeRateServiceImpl.java      | 25 ++++++----
 .../src/main/resources/application.yaml       |  5 ++
 8 files changed, 123 insertions(+), 47 deletions(-)
 create mode 100644 transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java
 delete mode 100644 transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java
 delete mode 100644 transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java
 create mode 100644 transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java
 create mode 100644 transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java

diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java
new file mode 100644
index 0000000..8cff4df
--- /dev/null
+++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/configuration/ExchangeRateInitializer.java
@@ -0,0 +1,39 @@
+package cz.muni.pa165.banking.application.configuration;
+
+import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Currency;
+import java.util.List;
+
+@Component
+public class ExchangeRateInitializer {
+    
+    private final Logger logger = LoggerFactory.getLogger(ExchangeRateInitializer.class);
+    
+    private final ExchangeRateService exchangeRateService;
+
+    @Value("#{'${banking.apps.rates.initial.currencies}'.split(',')}")
+    private List<String> currencies;
+    
+    public ExchangeRateInitializer(ExchangeRateService exchangeRateService) {
+        this.exchangeRateService = exchangeRateService;
+    }
+    
+    @PostConstruct
+    void initialize() {
+        for (String currency : currencies) {
+            try {
+                // ignore result, just call service to trigger mechanism to cache data
+                exchangeRateService.getRate(Currency.getInstance(currency), Currency.getInstance(currency));
+            } catch (Exception e) {
+                logger.warn(String.format("Initializing exchange rates for %s failed. API might be unavailable!", currency));
+            }
+        }
+    }
+    
+}
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java
index e625165..ff6cb78 100644
--- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java
+++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/messaging/ProcessProducer.java
@@ -3,13 +3,10 @@ package cz.muni.pa165.banking.application.messaging;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import cz.muni.pa165.banking.domain.messaging.MessageProducer;
 import cz.muni.pa165.banking.domain.messaging.ProcessRequest;
-import cz.muni.pa165.banking.domain.transaction.TransactionType;
 import cz.muni.pa165.banking.exception.ServerError;
-import jakarta.annotation.PostConstruct;
 import org.springframework.stereotype.Service;
 
 import java.util.Map;
-import java.util.UUID;
 
 @Service
 public class ProcessProducer implements MessageProducer {
@@ -22,12 +19,6 @@ public class ProcessProducer implements MessageProducer {
         this.messagingService = messagingService;
         this.mapper = mapper;
     }
-
-    @PostConstruct
-    void testing() {
-        ProcessRequest data = new ProcessRequest(UUID.randomUUID(), TransactionType.DEPOSIT);
-        send(data);
-    }
     
     @Override
     public void send(ProcessRequest data) {
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java
deleted file mode 100644
index f832677..0000000
--- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/Dummy.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cz.muni.pa165.banking.application.proxy;
-
-import cz.muni.pa165.banking.account.query.CustomerServiceApi;
-import jakarta.annotation.PostConstruct;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-@Service
-public class Dummy {
-    
-    @Autowired
-    private CustomerServiceApi customerServiceApi;
-    
-    @PostConstruct
-    public void test() {
-        var jj = customerServiceApi.getBalance("id1");
-        System.out.println("jozo");
-    }
-    
-}
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java
deleted file mode 100644
index 62e21b8..0000000
--- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/ExchangeRatesApi.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package cz.muni.pa165.banking.application.proxy;
-
-// some proxy for a REST API, which contains real-time data about the currency rates
-// e.g. https://exchangeratesapi.io/
-// https://nordicapis.com/10-apis-for-currency-exchange-rates/
-public interface ExchangeRatesApi {
-    
-    // TODO nejake methods po vybere a nastudovani API
-    
-}
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java
new file mode 100644
index 0000000..e5f4ba4
--- /dev/null
+++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRateResponseProcessor.java
@@ -0,0 +1,46 @@
+package cz.muni.pa165.banking.application.proxy.rate;
+
+import cz.muni.pa165.banking.exception.UnexpectedValueException;
+
+import java.math.BigDecimal;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ExchangeRateResponseProcessor {
+    
+    public static Map<Currency, BigDecimal> process(Map<String, Object> response) {
+        if (!response.get("result").equals("success")) {
+            throw new UnexpectedValueException("Exchange rate API unavailable");
+        }
+
+        Map<String, Object> rates = (Map<String, Object>) response.get("conversion_rates");
+        
+        Map<Currency, BigDecimal> result = new HashMap<>();
+        for (String currencyCode : rates.keySet()) {
+            Currency currency;
+            try {
+                currency = Currency.getInstance(currencyCode);
+            } catch (Exception e) {
+                continue;
+            }
+            Object value = rates.get(currencyCode);
+            
+            BigDecimal rate;
+            if (value instanceof Integer val) {
+                rate = BigDecimal.valueOf(val);
+            } else if (value instanceof Double val) {
+                rate = BigDecimal.valueOf(val);
+            } else {
+                String strVal = value.toString();
+                double val = Double.parseDouble(strVal);
+                rate = BigDecimal.valueOf(val);
+            }
+            
+            result.put(currency, rate);
+        }
+        
+        return result;
+    }
+    
+}
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java
new file mode 100644
index 0000000..f95e758
--- /dev/null
+++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/proxy/rate/ExchangeRatesApi.java
@@ -0,0 +1,16 @@
+package cz.muni.pa165.banking.application.proxy.rate;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.Map;
+
+// https://app.exchangerate-api.com
+@FeignClient(name = "ExchangeRateApi", url = "${banking.apps.rates.url}")
+public interface ExchangeRatesApi {
+    
+    @GetMapping("/latest/{currency}")
+    Map<String, Object> getRatesOfCurrency(@RequestParam String currency);
+    
+}
diff --git a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java
index fb0786f..4a0c3ca 100644
--- a/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java
+++ b/transaction-processor/src/main/java/cz/muni/pa165/banking/application/service/ExchangeRateServiceImpl.java
@@ -1,25 +1,34 @@
 package cz.muni.pa165.banking.application.service;
 
+import cz.muni.pa165.banking.application.proxy.rate.ExchangeRateResponseProcessor;
+import cz.muni.pa165.banking.application.proxy.rate.ExchangeRatesApi;
 import cz.muni.pa165.banking.domain.money.exchange.ExchangeRateService;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.util.Currency;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 @Service
 public class ExchangeRateServiceImpl implements ExchangeRateService {
     
-    // TODO call external API containing current information about exchange rates -> Milestone2
-    //  either actual API containing real data, or custom 'mock' API containing static data
-
-//    private final ExchangeRatesApi proxy;
+    private final ExchangeRatesApi exchangeRatesApi;
+    
+    Map<Currency, Map<Currency, BigDecimal>> exchangeRates = new ConcurrentHashMap<>();
 
-//    public ExchangeRateServiceImpl(ExchangeRatesApi proxy) {
-//        this.proxy = proxy;
-//    }
+    public ExchangeRateServiceImpl(ExchangeRatesApi exchangeRatesApi) {
+        this.exchangeRatesApi = exchangeRatesApi;
+    }
 
     @Override
     public BigDecimal getRate(Currency base, Currency target) {
-        return BigDecimal.ONE;
+        if (!exchangeRates.containsKey(base)) {
+            Map<String, Object> response = exchangeRatesApi.getRatesOfCurrency(base.getCurrencyCode());
+            exchangeRates.put(base, ExchangeRateResponseProcessor.process(response));
+        }
+        
+        return exchangeRates.get(base).get(target);
     }
+
 }
diff --git a/transaction-processor/src/main/resources/application.yaml b/transaction-processor/src/main/resources/application.yaml
index e901a7e..5ab2509 100644
--- a/transaction-processor/src/main/resources/application.yaml
+++ b/transaction-processor/src/main/resources/application.yaml
@@ -11,6 +11,11 @@ banking:
       host: localhost
       port: 8081
       url: ${banking.apps.query.host}:${banking.apps.query.port}
+    rates:
+      api-key: 5f40db3a892e886bda8a6967
+      url: https://v6.exchangerate-api.com/v6/${banking.apps.rates.api-key}
+      initial:
+        currencies: CZK, EUR, USD
 
 spring:
   application:
-- 
GitLab