diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java index 09e913129..9036f373e 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentClientConfig.java @@ -18,19 +18,33 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import java.text.DateFormat; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.Builder; +import reactor.netty.http.client.HttpClient; /** * Configuration for Investment service REST client (ClientApi). + * + *
This configuration creates the Investment API client with proper codec configuration. + * + *
Note: Connection pooling, timeouts, and rate limiting are configured in + * {@link InvestmentWebClientConfiguration} which should be imported alongside this class. + * The WebClient connection pool prevents resource exhaustion and 503 errors by limiting: + *
Properties are grouped by the model they govern so operators can reason about each + * concern independently without touching unrelated settings. + * + *
All values can be overridden via {@code application.yml} / {@code application.properties} + * using the prefix {@code backbase.bootstrap.ingestions.investment.service}. + * + *
Ingestion-flow feature flags ({@code contentEnabled}, {@code assetUniverseEnabled}, etc.) + * live in the sibling class {@link InvestmentIngestionConfigurationProperties}. + * + *
Example: + *
+ * backbase: + * bootstrap: + * ingestions: + * investment: + * service: + * portfolio: + * default-currency: USD + * activation-past-months: 3 + * allocation: + * model-portfolio-allocation-asset: "model_portfolio.allocation.asset" + * deposit: + * provider: real-bank + * default-amount: 25000.0 + *+ */ +@Data +@ConfigurationProperties(prefix = "backbase.bootstrap.ingestions.investment.service") +public class InvestmentIngestProperties { + + public static final double DEFAULT_INIT_CASH = 10_000d; + + private PortfolioConfig portfolio = new PortfolioConfig(); + private AllocationConfig allocation = new AllocationConfig(); + private DepositConfig deposit = new DepositConfig(); + private AssetConfig asset = new AssetConfig(); + + // ------------------------------------------------------------------------- + // Portfolio + // ------------------------------------------------------------------------- + + /** + * Settings that govern how individual investment portfolios are created and activated. + */ + @Data + public static class PortfolioConfig { + + /** + * ISO 4217 currency code applied to new portfolios when none is specified in the source data. + */ + private String defaultCurrency = "EUR"; + + /** + * How many months into the past the portfolio's {@code activated} timestamp is set. A value of {@code 1} means + * the portfolio is considered to have been activated 1 month ago. + */ + private int activationPastMonths = 1; + } + + // ------------------------------------------------------------------------- + // Allocation + // ------------------------------------------------------------------------- + + /** + * Settings that govern portfolio-product allocation behaviour. + */ + @Data + public static class AllocationConfig { + + /** + * The allocation-asset expansion key sent when listing or managing portfolio products. Changing this value + * allows switching between different allocation-asset model definitions without a code change. + */ + private String modelPortfolioAllocationAsset = "model_portfolio.allocation.asset"; + private int allocationConcurrency = 5; + + private double defaultAmount = DEFAULT_INIT_CASH; + } + + // ------------------------------------------------------------------------- + // Deposit + // ------------------------------------------------------------------------- + + /** + * Settings that govern the automatic seed deposit created for new portfolios. + */ + @Data + public static class DepositConfig { + + /** + * The payment provider identifier sent with every deposit request. Set this to the real provider name for + * non-mock environments. + */ + private String provider = null; + + /** + * The monetary amount used as the initial seed deposit when no previous deposit exists or when the current + * deposited total is below this threshold. + */ + private double defaultAmount = DEFAULT_INIT_CASH; + } + + // ------------------------------------------------------------------------- + // Deposit + // ------------------------------------------------------------------------- + + /** + * Settings that govern the automatic seed deposit created for new portfolios. + */ + @Data + public static class AssetConfig { + + private boolean ingestImages = true; + private int marketConcurrency = 5; + private int marketSpecialDayConcurrency = 5; + private int assetCategoryConcurrency = 5; + private int assetCategoryTypeConcurrency = 5; + + } + +} + diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentIngestionConfigurationProperties.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentIngestionConfigurationProperties.java index 53bb897db..8d4d3e1ed 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentIngestionConfigurationProperties.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentIngestionConfigurationProperties.java @@ -7,6 +7,10 @@ /** * Configuration properties governing investment client ingestion behavior. + * + *
Controls which high-level ingestion flows are enabled. Service-level tuning + * (portfolio currencies, deposit provider, allocation assets, etc.) lives in + * {@link InvestmentIngestProperties}. */ @Data @ConditionalOnBean(InvestmentServiceConfiguration.class) @@ -16,7 +20,6 @@ public class InvestmentIngestionConfigurationProperties { private boolean contentEnabled = true; private boolean assetUniverseEnabled = true; private boolean wealthEnabled = true; - private int portfolioActivationPastMonths = 1; } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentRestServiceApiConfiguration.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentRestServiceApiConfiguration.java index e4de101a5..cd4875e99 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentRestServiceApiConfiguration.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentRestServiceApiConfiguration.java @@ -90,9 +90,10 @@ public InvestmentRestDocumentContentService investmentRestContentDocumentService } @Bean - public InvestmentRestAssetUniverseService investmentRestAssetUniverseService(AssetUniverseApi assetUniverseApi, - com.backbase.investment.api.service.sync.ApiClient restInvestmentApiClient) { - return new InvestmentRestAssetUniverseService(assetUniverseApi, restInvestmentApiClient); + public InvestmentRestAssetUniverseService investmentRestAssetUniverseService( + com.backbase.investment.api.service.sync.ApiClient restInvestmentApiClient, + InvestmentIngestProperties portfolioProperties) { + return new InvestmentRestAssetUniverseService(restInvestmentApiClient, portfolioProperties); } } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java index dc3561f7d..cbdbbdc73 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java @@ -41,7 +41,8 @@ InvestmentClientConfig.class }) @EnableConfigurationProperties({ - InvestmentIngestionConfigurationProperties.class + InvestmentIngestionConfigurationProperties.class, + InvestmentIngestProperties.class }) @RequiredArgsConstructor @Configuration @@ -61,9 +62,9 @@ public CustomIntegrationApiService customIntegrationApiService(ApiClient apiClie @Bean public InvestmentPortfolioService investmentPortfolioService(PortfolioApi portfolioApi, InvestmentProductsApi investmentProductsApi, PaymentsApi paymentsApi, PortfolioTradingAccountsApi portfolioTradingAccountsApi, - InvestmentIngestionConfigurationProperties configurationProperties) { + InvestmentIngestProperties portfolioProperties) { return new InvestmentPortfolioService(investmentProductsApi, portfolioApi, paymentsApi, - portfolioTradingAccountsApi, configurationProperties); + portfolioTradingAccountsApi, portfolioProperties); } @Bean @@ -96,9 +97,9 @@ public InvestmentIntradayAssetPriceService investmentIntradayAssetPriceService(A @Bean public InvestmentPortfolioAllocationService investmentPortfolioAllocationService(AllocationsApi allocationsApi, AssetUniverseApi assetUniverseApi, InvestmentApi investmentApi, - CustomIntegrationApiService customIntegrationApiService) { + CustomIntegrationApiService customIntegrationApiService, InvestmentIngestProperties portfolioProperties) { return new InvestmentPortfolioAllocationService(allocationsApi, assetUniverseApi, investmentApi, - customIntegrationApiService); + customIntegrationApiService, portfolioProperties); } @Bean @@ -124,10 +125,11 @@ public InvestmentAssetUniverseSaga investmentStaticDataSaga( InvestmentIntradayAssetPriceService investmentIntradayAssetPriceService, InvestmentCurrencyService investmentCurrencyService, AsyncTaskService asyncTaskService, - InvestmentIngestionConfigurationProperties coreConfigurationProperties) { + InvestmentIngestionConfigurationProperties coreConfigurationProperties, + InvestmentIngestProperties portfolioProperties) { return new InvestmentAssetUniverseSaga(investmentAssetUniverseService, investmentAssetPriceService, investmentIntradayAssetPriceService, investmentCurrencyService, asyncTaskService, - coreConfigurationProperties); + coreConfigurationProperties, portfolioProperties); } @Bean diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentWebClientConfiguration.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentWebClientConfiguration.java new file mode 100644 index 000000000..4ade2ad4b --- /dev/null +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentWebClientConfiguration.java @@ -0,0 +1,95 @@ +package com.backbase.stream.configuration; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +/** + * WebClient configuration for Investment service communication. + * + *
Provides optimized connection pooling, timeouts, and rate limiting to prevent: + *
All tunable values are externalized via {@link InvestmentWebClientProperties} and can be + * overridden through {@code application.yml} without recompiling. + * + *
All beans are explicitly named so they are unambiguous and never accidentally + * replaced by Spring Boot auto-configuration or another generic bean of the same type. + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(InvestmentWebClientProperties.class) +public class InvestmentWebClientConfiguration { + + /** + * Dedicated {@link ConnectionProvider} for the Investment service client pool. + * + *
Using an explicit named bean (rather than {@code @ConditionalOnMissingBean ReactorResourceFactory}) + * avoids silently falling back to Spring Boot's auto-configured shared pool when one already exists + * in the application context. + * + * @param props investment HTTP-client configuration properties + * @return investment-specific ConnectionProvider + */ + @Bean("investmentConnectionProvider") + public ConnectionProvider investmentConnectionProvider(InvestmentWebClientProperties props) { + ConnectionProvider provider = ConnectionProvider.builder("investment-client-pool") + .maxConnections(props.getMaxConnections()) + .maxIdleTime(Duration.ofMinutes(props.getMaxIdleTimeMinutes())) + .maxLifeTime(Duration.ofMinutes(props.getMaxLifeTimeMinutes())) + .pendingAcquireMaxCount(props.getMaxPendingAcquires()) + .pendingAcquireTimeout(Duration.ofMillis(props.getPendingAcquireTimeoutMillis())) + .evictInBackground(Duration.ofSeconds(props.getEvictInBackgroundSeconds())) + .build(); + + log.info("Configured investment ConnectionProvider: maxConnections={}, maxIdleTime={}min," + + " pendingAcquireMaxCount={}", + props.getMaxConnections(), props.getMaxIdleTimeMinutes(), props.getMaxPendingAcquires()); + + return provider; + } + + /** + * Creates an {@link HttpClient} backed by the investment-specific connection pool and + * with explicit connect / read / write timeouts. + * + *
This client ensures: + *
All values can be overridden via {@code application.yml} / {@code application.properties} + * using the prefix {@code backbase.communication.services.investment.http-client}. + * + *
Example: + *
+ * backbase: + * communication: + * services: + * investment: + * http-client: + * max-connections: 20 + * max-idle-time-minutes: 5 + * max-pending-acquires: 100 + * pending-acquire-timeout-millis: 45000 + * connect-timeout-seconds: 10 + * read-timeout-seconds: 30 + * write-timeout-seconds: 30 + *+ */ +@Data +@ConfigurationProperties(prefix = "backbase.communication.services.investment.http-client") +public class InvestmentWebClientProperties { + + /** + * Maximum number of open TCP connections to the Investment service. + * Limiting this prevents the service from being overwhelmed (which causes 503 responses). + */ + private int maxConnections = 20; + + /** + * Maximum time (in minutes) that a connection can remain idle in the pool before being evicted. + */ + private long maxIdleTimeMinutes = 5; + + /** + * Maximum lifetime (in minutes) of a pooled connection regardless of activity. + */ + private long maxLifeTimeMinutes = 30; + + /** + * Maximum number of requests that can be queued waiting for a connection. + * Bounds the in-memory queue so callers receive a fast failure rather than an unbounded backlog. + */ + private int maxPendingAcquires = 100; + + /** + * Maximum time (in milliseconds) a request will wait to acquire a connection from the pool. + */ + private long pendingAcquireTimeoutMillis = 45_000; + + /** + * Background eviction interval (in seconds) for idle/expired connections in the pool. + */ + private long evictInBackgroundSeconds = 120; + + /** + * TCP connection timeout in seconds. Prevents hanging on unresponsive servers. + */ + private int connectTimeoutSeconds = 10; + + /** + * Read timeout in seconds. Prevents indefinite hangs waiting for a response. + */ + private int readTimeoutSeconds = 30; + + /** + * Write timeout in seconds. Prevents indefinite hangs while sending a request. + */ + private int writeTimeoutSeconds = 30; +} + diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ClientUser.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ClientUser.java index f136d66ae..2ae2b1add 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ClientUser.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ClientUser.java @@ -13,7 +13,7 @@ public class ClientUser { private UUID investmentClientId; private String internalUserId; private String externalUserId; - private String legalEntityExternalId; + private String legalEntityId; } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentArrangement.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentArrangement.java index f334d1063..a3f026ce5 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentArrangement.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentArrangement.java @@ -1,5 +1,6 @@ package com.backbase.stream.investment; +import java.math.BigDecimal; import java.util.List; import java.util.UUID; import lombok.Builder; @@ -16,8 +17,9 @@ public class InvestmentArrangement { private String externalUserId; private String productTypeExternalId; private String currency; + private BigDecimal initialCash; private UUID investmentProductId; - private List
Retryable errors: + *
Design considerations: + *
Retryable errors include: + *