From 117ae0ec89d6000115e84396045d5c7801ca84d9 Mon Sep 17 00:00:00 2001 From: alvaromaoc Date: Wed, 28 May 2025 12:21:11 +0200 Subject: [PATCH] Fix news and decisions endpoints --- .../marketfeeling/MarketFeelingClient.java | 38 ++++++++++++++++ .../client/marketfeeling/NewsResponse.java | 11 +++++ .../client/portfolio/PortfolioClient.java | 37 ++++++++++++++++ .../portfolio/PortfolioHoldingResponse.java | 8 ++++ .../configuration/SecurityConfiguration.java | 2 +- .../controller/NewsController.java | 43 +++++++++++++++++++ src/main/resources/application.yml | 10 +++-- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/autoinvestor/client/marketfeeling/MarketFeelingClient.java create mode 100644 src/main/java/io/autoinvestor/client/marketfeeling/NewsResponse.java create mode 100644 src/main/java/io/autoinvestor/client/portfolio/PortfolioClient.java create mode 100644 src/main/java/io/autoinvestor/client/portfolio/PortfolioHoldingResponse.java create mode 100644 src/main/java/io/autoinvestor/controller/NewsController.java diff --git a/src/main/java/io/autoinvestor/client/marketfeeling/MarketFeelingClient.java b/src/main/java/io/autoinvestor/client/marketfeeling/MarketFeelingClient.java new file mode 100644 index 0000000..f95ebee --- /dev/null +++ b/src/main/java/io/autoinvestor/client/marketfeeling/MarketFeelingClient.java @@ -0,0 +1,38 @@ +package io.autoinvestor.client.marketfeeling; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; + +@Component +public class MarketFeelingClient { + + private final WebClient webClient; + + public MarketFeelingClient( + WebClient.Builder webClientBuilder, + @Value("${autoinvestor.client.market-feeling.url}") String baseUrl + ) { + this.webClient = webClientBuilder.baseUrl(baseUrl).build(); + } + + public Mono> getNews(String userId, String assetId) { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/portfolio/holdings") + .queryParam("assetId", assetId) + .build()) + .header("X-User-Id", userId) + .exchangeToMono(clientResponse -> Mono.defer(() -> { + if (clientResponse.statusCode().value() == HttpStatus.OK.value()) { + return clientResponse.bodyToMono(NewsResponse[].class).map(Arrays::asList); + } + return clientResponse.createError(); + })); + } +} diff --git a/src/main/java/io/autoinvestor/client/marketfeeling/NewsResponse.java b/src/main/java/io/autoinvestor/client/marketfeeling/NewsResponse.java new file mode 100644 index 0000000..35a998f --- /dev/null +++ b/src/main/java/io/autoinvestor/client/marketfeeling/NewsResponse.java @@ -0,0 +1,11 @@ +package io.autoinvestor.client.marketfeeling; + +import java.time.LocalDateTime; + +public record NewsResponse( + String title, + LocalDateTime date, + String url, + String assetId +) { +} diff --git a/src/main/java/io/autoinvestor/client/portfolio/PortfolioClient.java b/src/main/java/io/autoinvestor/client/portfolio/PortfolioClient.java new file mode 100644 index 0000000..953a5ff --- /dev/null +++ b/src/main/java/io/autoinvestor/client/portfolio/PortfolioClient.java @@ -0,0 +1,37 @@ +package io.autoinvestor.client.portfolio; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; + +@Component +public class PortfolioClient { + + private final WebClient webClient; + + public PortfolioClient( + WebClient.Builder webClientBuilder, + @Value("${autoinvestor.client.portfolio.url}") String baseUrl + ) { + this.webClient = webClientBuilder.baseUrl(baseUrl).build(); + } + + public Mono> getHoldings(String userId) { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/portfolio/holdings") + .build()) + .header("X-User-Id", userId) + .exchangeToMono(clientResponse -> Mono.defer(() -> { + if (clientResponse.statusCode().value() == HttpStatus.OK.value()) { + return clientResponse.bodyToMono(PortfolioHoldingResponse[].class).map(Arrays::asList); + } + return clientResponse.createError(); + })); + } +} diff --git a/src/main/java/io/autoinvestor/client/portfolio/PortfolioHoldingResponse.java b/src/main/java/io/autoinvestor/client/portfolio/PortfolioHoldingResponse.java new file mode 100644 index 0000000..1a2c77f --- /dev/null +++ b/src/main/java/io/autoinvestor/client/portfolio/PortfolioHoldingResponse.java @@ -0,0 +1,8 @@ +package io.autoinvestor.client.portfolio; + +public record PortfolioHoldingResponse( + String assetId, + Integer amount, + Integer price +) { +} diff --git a/src/main/java/io/autoinvestor/configuration/SecurityConfiguration.java b/src/main/java/io/autoinvestor/configuration/SecurityConfiguration.java index 8026f21..fff26a2 100644 --- a/src/main/java/io/autoinvestor/configuration/SecurityConfiguration.java +++ b/src/main/java/io/autoinvestor/configuration/SecurityConfiguration.java @@ -19,7 +19,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec .pathMatchers("/api/oauth2/**", "/api/login/**").permitAll() - .anyExchange().permitAll() + .anyExchange().authenticated() ) .oauth2Login(Customizer.withDefaults()) .build(); diff --git a/src/main/java/io/autoinvestor/controller/NewsController.java b/src/main/java/io/autoinvestor/controller/NewsController.java new file mode 100644 index 0000000..bd35ea0 --- /dev/null +++ b/src/main/java/io/autoinvestor/controller/NewsController.java @@ -0,0 +1,43 @@ +package io.autoinvestor.controller; + +import io.autoinvestor.client.marketfeeling.MarketFeelingClient; +import io.autoinvestor.client.marketfeeling.NewsResponse; +import io.autoinvestor.client.portfolio.PortfolioClient; +import io.autoinvestor.client.portfolio.PortfolioHoldingResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Comparator; +import java.util.List; + +@RestController +@RequestMapping("/internal/news") +@RequiredArgsConstructor +public class NewsController { + + private final PortfolioClient portfolioClient; + private final MarketFeelingClient marketFeelingClient; + + @GetMapping + public Mono> getNews(@RequestHeader("X-User-Id") String userId) { + return portfolioClient.getHoldings(userId) + .map(holdings -> holdings.stream().map(PortfolioHoldingResponse::assetId).toList()) + .map(assetIds -> assetIds.stream().map(assetId -> marketFeelingClient.getNews(userId, assetId)).toList()) + .flatMap(NewsController::flatten) + .map(list -> list.stream().sorted(Comparator.comparing(NewsResponse::date).reversed()).toList()) + .map(ResponseEntity::ok); + } + + private static Mono> flatten(List>> monoList) { + return Flux.fromIterable(monoList) + .flatMap(mono -> mono) + .flatMapIterable(list -> list) + .collectList(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b932355..48a9ed7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,10 @@ autoinvestor: client: users: url: "${USERS_BASE_URL}" + portfolio: + url: "${PORTFOLIO_BASE_URL}" + market-feeling: + url: "${MARKET_FEELING_BASE_URL}" spring: security: @@ -94,16 +98,16 @@ spring: - RewritePath=/api/(?.*),/${segment} - id: get-news-endpoint - uri: "${MARKET_FEELING_BASE_URL}" + uri: http://localhost:8080 predicates: - Path=/api/news - Method=GET filters: - ClaimToHeader=userId,X-User-Id - - RewritePath=/api/(?.*),/${segment} + - RewritePath=/api/news, /internal/news - id: get-decisions-endpoint - uri: "${MARKET_FEELING_BASE_URL}" + uri: "${DECISION_MAKING_BASE_URL}" predicates: - Path=/api/decisions - Method=GET