浏览代码

Merge pull request #2 from dbohry/chore/import-csv

add stocks api
Daniel Bohry 1 年之前
父节点
当前提交
80bd9f149b

+ 2 - 7
src/main/java/com/danielbohry/stocks/client/StockClient.java

@@ -1,16 +1,11 @@
 package com.danielbohry.stocks.client;
 
-import com.danielbohry.stocks.service.StockService.StockInfoResponse;
-import com.danielbohry.stocks.service.StockService.StockQuoteResponse;
-import feign.FeignException;
-import org.springframework.cloud.openfeign.FallbackFactory;
+import com.danielbohry.stocks.repository.StockRepository.StockInfoResponse;
+import com.danielbohry.stocks.repository.StockRepository.StockQuoteResponse;
 import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Component;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.server.ResponseStatusException;
 
 import java.util.List;
 

+ 0 - 26
src/main/java/com/danielbohry/stocks/client/StockClientFallbackFactory.java

@@ -1,26 +0,0 @@
-package com.danielbohry.stocks.client;
-
-import com.danielbohry.stocks.service.StockService;
-import org.springframework.cloud.openfeign.FallbackFactory;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-
-@Component
-public class StockClientFallbackFactory implements FallbackFactory<StockClient> {
-
-    @Override
-    public StockClient create(Throwable cause) {
-        return new StockClient() {
-            @Override
-            public List<StockService.StockQuoteResponse> getStockQuote(String symbol, String apiKey) {
-                return null;
-            }
-
-            @Override
-            public StockService.StockInfoResponse getStockInfo(String symbol, String apiKey) {
-                return null;
-            }
-        };
-    }
-}

+ 0 - 21
src/main/java/com/danielbohry/stocks/config/FeignClientConfiguration.java

@@ -1,21 +0,0 @@
-package com.danielbohry.stocks.config;
-
-import feign.Retryer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.client.RestTemplate;
-
-@Configuration
-public class FeignClientConfiguration {
-
-    @Bean
-    public Retryer retryer() {
-        return new Retryer.Default(100, 2000, 3);
-    }
-
-    @Bean
-    public RestTemplate restTemplate() {
-        return new RestTemplate();
-    }
-
-}

+ 80 - 0
src/main/java/com/danielbohry/stocks/controller/StockController.java

@@ -0,0 +1,80 @@
+package com.danielbohry.stocks.controller;
+
+import com.danielbohry.stocks.domain.Quote;
+import com.danielbohry.stocks.service.StockService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Objects;
+
+import static java.time.LocalDateTime.now;
+
+@Slf4j
+@RestController
+@RequestMapping("api/stocks")
+@AllArgsConstructor
+@CrossOrigin
+public class StockController {
+
+    private final StockService service;
+
+    @GetMapping
+    public ResponseEntity<List<Quote>> getAll() {
+        List<Quote> response = service.get();
+        return ResponseEntity.ok(response);
+    }
+
+    @GetMapping("{code}")
+    public ResponseEntity<Quote> getByCode(@PathVariable String code) {
+        Quote response = service.get(code.toUpperCase());
+        return ResponseEntity.ok(response);
+    }
+
+    @PostMapping("/upload-csv")
+    public ResponseEntity<List<Quote>> uploadCsvFile(@RequestParam("file") MultipartFile file) {
+        if (file.isEmpty()) {
+            return ResponseEntity.badRequest().build();
+        }
+
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
+            List<Quote> quotes = reader.lines()
+                    .map(this::convert)
+                    .filter(Objects::nonNull)
+                    .toList();
+
+            List<Quote> response = service.update(quotes);
+
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            log.error("Failed to import csv", e);
+            return ResponseEntity.status(400).build();
+        }
+    }
+
+    private Quote convert(String input) {
+        String[] value = input.split(",");
+
+        log.info("Importing [{}]", (Object) value);
+
+        if (value[3] != null && !value[3].equals("#N/A")) {
+            BigDecimal price = new BigDecimal(value[3]);
+            return price.compareTo(BigDecimal.ZERO) > 0
+                    ? new Quote(value[0], value[1], price, now())
+                    : null;
+        } else if (value[0] != null && Objects.equals(value[3], "#N/A")) {
+            return null;
+        } else {
+            return null;
+        }
+
+    }
+
+}

+ 116 - 0
src/main/java/com/danielbohry/stocks/repository/StockRepository.java

@@ -0,0 +1,116 @@
+package com.danielbohry.stocks.repository;
+
+import com.danielbohry.stocks.client.StockClient;
+import com.danielbohry.stocks.domain.Quote;
+import com.danielbohry.stocks.exception.NotFoundException;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import feign.FeignException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Repository;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static java.time.LocalDateTime.now;
+
+@Slf4j
+@Repository
+public class StockRepository {
+
+    private final QuoteRepository repository;
+    private final StockClient client;
+    private final String key;
+
+    public StockRepository(QuoteRepository repository, StockClient client, @Value("${clients.stock.key}") String key) {
+        this.repository = repository;
+        this.client = client;
+        this.key = key;
+    }
+
+    public List<Quote> findAll() {
+        return repository.findAll();
+    }
+
+    public Quote findByCode(String code) {
+        return repository.findByCode(code).orElseThrow(() -> new NotFoundException("Couldn't find any stock with: " + code));
+    }
+
+    public List<Quote> update(List<Quote> quote) {
+        return repository.saveAll(quote);
+    }
+
+    public boolean isValid(String code) {
+        Quote quote = repository.findByCode(code).orElse(null);
+
+        if (quote != null) return true;
+
+        try {
+            client.getStockInfo(code, key);
+            return true;
+        } catch (FeignException.NotFound e) {
+            return false;
+        }
+    }
+
+    public Quote getStockQuote(String code) {
+        Quote quote = repository.findByCode(code).orElse(new Quote(code, null, null, null));
+        quote.setPrice(getLastPrice(quote));
+        quote.setUpdatedAt(now());
+
+        if (quote.getName() == null) {
+            StockInfoResponse info = updateStockInformation(quote.getCode());
+            quote.setName(info.getName());
+        }
+
+        repository.save(quote);
+
+        return quote;
+    }
+
+    private StockInfoResponse updateStockInformation(String code) {
+        log.info("Current stock's name is null. Requesting latest information...");
+        return client.getStockInfo(code, key);
+    }
+
+    private BigDecimal getLastPrice(Quote quote) {
+        if (quote.getPrice() == null) {
+            log.info("Current quote for [{}] is null. Requesting latest quote...", quote);
+            return new BigDecimal(client.getStockQuote(quote.getCode(), key).get(0).getLastPrice());
+        } else if (quote.getUpdatedAt().isBefore(now().minusDays(1))) {
+            log.info("Current quote for [{}] is older than 1 day. Requesting latest quote...", quote);
+            return new BigDecimal(client.getStockQuote(quote.getCode(), key).get(0).getLastPrice());
+        } else {
+            return quote.getPrice();
+        }
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class StockQuoteResponse {
+        @JsonProperty("adjClose")
+        private String lastPrice;
+        @JsonProperty("adjOpen")
+        private String openPrice;
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class StockInfoResponse {
+        @JsonProperty("ticker")
+        private String code;
+        @JsonProperty("name")
+        private String name;
+        @JsonProperty("exchangeCode")
+        private String exchange;
+    }
+
+}

+ 15 - 82
src/main/java/com/danielbohry/stocks/service/StockService.java

@@ -1,105 +1,38 @@
 package com.danielbohry.stocks.service;
 
-import com.danielbohry.stocks.client.StockClient;
 import com.danielbohry.stocks.domain.Quote;
-import com.danielbohry.stocks.repository.QuoteRepository;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import feign.FeignException;
+import com.danielbohry.stocks.repository.StockRepository;
 import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
-import java.math.BigDecimal;
+import java.util.List;
 
-import static java.time.LocalDateTime.now;
-
-@Service
 @Slf4j
+@Service
+@AllArgsConstructor
 public class StockService {
 
-    private final QuoteRepository repository;
-    private final StockClient client;
-    private final String key;
-
-    public StockService(QuoteRepository repository,
-                        StockClient client,
-                        @Value("${clients.stock.key}") String key) {
-        this.repository = repository;
-        this.client = client;
-        this.key = key;
-    }
-
-    public boolean isValid(String code) {
-        Quote quote = repository.findByCode(code).orElse(null);
-
-        if (quote != null) return true;
+    private StockRepository repository;
 
-        try {
-            client.getStockInfo(code, key);
-            return true;
-        } catch (FeignException.NotFound e) {
-            return false;
-        }
+    public List<Quote> get() {
+        return repository.findAll();
     }
 
-    public Quote getStockQuote(String code) {
-        Quote quote = repository.findByCode(code).orElse(new Quote(code, null, null, null));
-        quote.setPrice(getLastPrice(quote));
-        quote.setUpdatedAt(now());
-
-        if (quote.getName() == null) {
-            StockInfoResponse info = updateStockInformation(quote.getCode());
-            quote.setName(info.getName());
-        }
-
-        repository.save(quote);
-
-        return quote;
+    public Quote get(String code) {
+        return repository.findByCode(code);
     }
 
-    private StockInfoResponse updateStockInformation(String code) {
-        log.info("Current stock's name is null. Requesting latest information...");
-        return client.getStockInfo(code, key);
+    public List<Quote> update(List<Quote> quotes) {
+        return repository.update(quotes);
     }
 
-    private BigDecimal getLastPrice(Quote quote) {
-        if (quote.getPrice() == null) {
-            log.info("Current quote for [{}] is null. Requesting latest quote...", quote);
-            return new BigDecimal(client.getStockQuote(quote.getCode(), key).get(0).getLastPrice());
-        } else if (quote.getUpdatedAt().isBefore(now().minusDays(1))) {
-            log.info("Current quote for [{}] is older than 1 day. Requesting latest quote...", quote);
-            return new BigDecimal(client.getStockQuote(quote.getCode(), key).get(0).getLastPrice());
-        } else {
-            return quote.getPrice();
-        }
-    }
-
-    @Data
-    @AllArgsConstructor
-    @NoArgsConstructor
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class StockQuoteResponse {
-        @JsonProperty("adjClose")
-        private String lastPrice;
-        @JsonProperty("adjOpen")
-        private String openPrice;
+    public boolean isValid(String code) {
+        return repository.isValid(code);
     }
 
-    @Data
-    @AllArgsConstructor
-    @NoArgsConstructor
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    public static class StockInfoResponse {
-        @JsonProperty("ticker")
-        private String code;
-        @JsonProperty("name")
-        private String name;
-        @JsonProperty("exchangeCode")
-        private String exchange;
+    public Quote getStockQuote(String code) {
+        return repository.getStockQuote(code);
     }
 
 }

+ 5 - 6
src/test/java/service/PortfolioServiceTest.java

@@ -6,6 +6,8 @@ import com.danielbohry.stocks.domain.Portfolio;
 import com.danielbohry.stocks.domain.Stock;
 import com.danielbohry.stocks.repository.PortfolioRepository;
 import com.danielbohry.stocks.repository.QuoteRepository;
+import com.danielbohry.stocks.repository.StockRepository;
+import com.danielbohry.stocks.repository.StockRepository.StockInfoResponse;
 import com.danielbohry.stocks.service.PortfolioService;
 import com.danielbohry.stocks.service.StockService;
 import org.junit.jupiter.api.AfterEach;
@@ -30,17 +32,14 @@ public class PortfolioServiceTest {
     @Autowired
     private PortfolioRepository portfolioRepository;
 
-    @Autowired
-    private QuoteRepository quoteRepository;
-
     @Mock
-    private StockClient client;
+    private StockRepository stockRepository;
 
     private PortfolioService portfolioService;
 
     @BeforeEach
     public void setup() {
-        StockService stockService = new StockService(quoteRepository, client, "key");
+        StockService stockService = new StockService(stockRepository);
         portfolioService = new PortfolioService(portfolioRepository, stockService);
     }
 
@@ -86,7 +85,7 @@ public class PortfolioServiceTest {
     @Test
     public void shouldUpdateAPortfolio() {
         //given
-        when(client.getStockInfo(any(), any())).thenReturn(new StockService.StockInfoResponse());
+        when(stockRepository.isValid(any())).thenReturn(true);
 
         Portfolio portfolio = portfolioService.create();
         Stock newStock = Stock.builder()