upsertPortfolioTradingAccount(PortfolioTrad
* Patches an existing portfolio trading account with updated values.
*
* If the patch operation fails (e.g., due to validation errors or conflicts),
- * falls back to returning the existing account to preserve data integrity
- * and prevent batch failures.
+ * falls back to returning the existing account to preserve data integrity and prevent batch failures.
*
* @param existing the existing trading account to update
- * @param request the request containing updated values
+ * @param request the request containing updated values
* @return Mono emitting the updated trading account, or the existing account if patch fails
*/
private Mono patchExistingPortfolioTradingAccount(
@@ -813,7 +805,7 @@ private Mono listExistingPortfolioTradingAccounts(
* Returns an error for multiple results — indicates a data setup issue
*
*
- * @param accounts the paginated list of trading accounts returned by the API
+ * @param accounts the paginated list of trading accounts returned by the API
* @param externalAccountId the external account ID used in the search, for logging purposes
* @return Mono emitting the single matching trading account, or empty if no results found
* @throws IllegalStateException if multiple trading accounts are found with the same external account ID
@@ -848,15 +840,15 @@ private Mono validateAndExtractPortfolioTradingAccount(
* Logs errors occurring during portfolio trading account operations.
*
* Provides enhanced error context for {@link WebClientResponseException},
- * including HTTP status code and response body. For other exceptions, logs
- * basic error information.
+ * including HTTP status code and response body. For other exceptions, logs basic error information.
*
- * @param operation a short description of the operation that failed (e.g., "PATCH", "CREATE")
- * @param idLabel the label for the identifier (e.g., "uuid", "externalAccountId")
- * @param idValue the value of the identifier
- * @param throwable the exception that occurred
+ * @param operation a short description of the operation that failed (e.g., "PATCH", "CREATE")
+ * @param idLabel the label for the identifier (e.g., "uuid", "externalAccountId")
+ * @param idValue the value of the identifier
+ * @param throwable the exception that occurred
*/
- private void logPortfolioTradingAccountError(String operation, String idLabel, String idValue, Throwable throwable) {
+ private void logPortfolioTradingAccountError(String operation, String idLabel, String idValue,
+ Throwable throwable) {
if (throwable instanceof WebClientResponseException ex) {
log.warn("Portfolio trading account {} failed: {}={}, status={}, body={}",
operation, idLabel, idValue, ex.getStatusCode(), ex.getResponseBodyAsString());
diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseService.java
index b0f332c5f..ddc6e4e03 100644
--- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseService.java
+++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseService.java
@@ -1,12 +1,12 @@
package com.backbase.stream.investment.service.resttemplate;
import com.backbase.investment.api.service.sync.ApiClient;
-import com.backbase.investment.api.service.sync.v1.AssetUniverseApi;
import com.backbase.investment.api.service.sync.v1.model.AssetCategory;
import com.backbase.investment.api.service.sync.v1.model.AssetCategoryRequest;
import com.backbase.investment.api.service.sync.v1.model.OASAssetRequestDataRequest;
import com.backbase.investment.api.service.sync.v1.model.PatchedAssetCategoryRequest;
import com.backbase.investment.api.service.v1.model.Asset;
+import com.backbase.stream.configuration.InvestmentIngestProperties;
import com.backbase.stream.investment.model.AssetCategoryEntry;
import java.util.Collections;
import java.util.HashMap;
@@ -33,8 +33,8 @@
@RequiredArgsConstructor
public class InvestmentRestAssetUniverseService {
- private final AssetUniverseApi assetUniverseApi;
private final ApiClient apiClient;
+ private final InvestmentIngestProperties ingestProperties;
private final RestTemplateAssetMapper assetMapper = Mappers.getMapper(RestTemplateAssetMapper.class);
public Mono createAsset(com.backbase.stream.investment.Asset asset,
@@ -99,7 +99,7 @@ private com.backbase.investment.api.service.sync.v1.model.Asset createAsset(OASA
if (data != null) {
localVarFormParams.add("data", data);
}
- if (logo != null) {
+ if (ingestProperties.getAsset().isIngestImages() && logo != null) {
localVarFormParams.add("logo", logo);
}
@@ -145,7 +145,7 @@ public com.backbase.investment.api.service.sync.v1.model.Asset patchAsset(String
if (data != null) {
localVarFormParams.add("data", data);
}
- if (logo != null) {
+ if (ingestProperties.getAsset().isIngestImages() && logo != null) {
localVarFormParams.add("logo", logo);
}
@@ -197,7 +197,9 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, Resource logo) {
final MultiValueMap localVarCookieParams = new LinkedMultiValueMap();
final MultiValueMap localVarFormParams = new LinkedMultiValueMap();
- localVarFormParams.add("image", logo);
+ if (ingestProperties.getAsset().isIngestImages()) {
+ localVarFormParams.add("image", logo);
+ }
final String[] localVarAccepts = {"application/json"};
final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts);
@@ -268,7 +270,9 @@ public Mono patchAssetCategory(UUID assetCategoryId,
.ifPresent(v -> localVarFormParams.add("excerpt", v));
Optional.ofNullable(assetCategoryPatch.getDescription())
.ifPresent(v -> localVarFormParams.add("description", v));
- Optional.ofNullable(image).ifPresent(v -> localVarFormParams.add("image", v));
+ if (ingestProperties.getAsset().isIngestImages()) {
+ Optional.ofNullable(image).ifPresent(v -> localVarFormParams.add("image", v));
+ }
final String[] localVarAccepts = {
"application/json"
@@ -331,7 +335,9 @@ public Mono createAssetCategory(AssetCategoryEntry assetCategoryE
.ifPresent(v -> localVarFormParams.add("excerpt", v));
Optional.ofNullable(assetCategoryRequest.getDescription())
.ifPresent(v -> localVarFormParams.add("description", v));
- Optional.ofNullable(image).ifPresent(v -> localVarFormParams.add("image", v));
+ if (ingestProperties.getAsset().isIngestImages()) {
+ Optional.ofNullable(image).ifPresent(v -> localVarFormParams.add("image", v));
+ }
final String[] localVarAccepts = {
"application/json"
@@ -352,6 +358,7 @@ public Mono createAssetCategory(AssetCategoryEntry assetCategoryE
localVarAccept, localVarContentType, localVarAuthNames, localReturnType)
.getBody());
});
+
}
}
\ No newline at end of file
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/configuration/InvestmentClientConfigTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/configuration/InvestmentClientConfigTest.java
new file mode 100644
index 000000000..98242d1b2
--- /dev/null
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/configuration/InvestmentClientConfigTest.java
@@ -0,0 +1,175 @@
+package com.backbase.stream.configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.backbase.investment.api.service.ApiClient;
+import com.backbase.investment.api.service.v1.AllocationsApi;
+import com.backbase.investment.api.service.v1.AssetUniverseApi;
+import com.backbase.investment.api.service.v1.AsyncBulkGroupsApi;
+import com.backbase.investment.api.service.v1.ClientApi;
+import com.backbase.investment.api.service.v1.ContentApi;
+import com.backbase.investment.api.service.v1.CurrencyApi;
+import com.backbase.investment.api.service.v1.FinancialAdviceApi;
+import com.backbase.investment.api.service.v1.InvestmentApi;
+import com.backbase.investment.api.service.v1.InvestmentProductsApi;
+import com.backbase.investment.api.service.v1.PaymentsApi;
+import com.backbase.investment.api.service.v1.PortfolioApi;
+import com.backbase.investment.api.service.v1.PortfolioTradingAccountsApi;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.text.DateFormat;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+
+/**
+ * Unit tests for {@link InvestmentClientConfig}.
+ *
+ * Each {@code @Bean} factory method is called directly (no Spring context) to verify:
+ *
+ * - The method returns a non-null, correctly typed API instance.
+ * - The {@code investmentApiClient} method configures Jackson codecs and returns a
+ * non-null {@link ApiClient}.
+ *
+ *
+ * A real {@link WebClient} (built from {@code WebClient.builder().build()}) is used for
+ * the {@code investmentApiClient} test so the codec-configuration lambda is executed in full,
+ * giving 100 % line coverage for that method.
+ */
+@DisplayName("InvestmentClientConfig")
+class InvestmentClientConfigTest {
+
+ private InvestmentClientConfig config;
+
+ @BeforeEach
+ void setUp() {
+ config = new InvestmentClientConfig();
+ }
+
+ // -------------------------------------------------------------------------
+ // Constant
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("INVESTMENT_SERVICE_ID constant equals 'investment'")
+ void investmentServiceIdConstant_hasExpectedValue() {
+ assertThat(InvestmentClientConfig.INVESTMENT_SERVICE_ID).isEqualTo("investment");
+ }
+
+ // -------------------------------------------------------------------------
+ // investmentApiClient
+ // -------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("investmentApiClient — returns non-null ApiClient with codecs configured")
+ void investmentApiClient_returnsNonNullApiClientWithCodecsConfigured() {
+ // Use a real WebClient so that WebClient.Builder#codecs(Consumer) actually invokes
+ // the codec-configuration lambda and every line of the method is covered.
+ WebClient webClient = WebClient.builder().build();
+ ObjectMapper objectMapper = new ObjectMapper();
+ DateFormat dateFormat = mock(DateFormat.class);
+ HttpClient httpClient = mock(HttpClient.class); // declared parameter; not used by the method body
+
+ ApiClient result = config.investmentApiClient(webClient, httpClient, objectMapper, dateFormat);
+
+ assertThat(result).isNotNull();
+ }
+
+ // -------------------------------------------------------------------------
+ // API bean factory methods
+ // -------------------------------------------------------------------------
+
+ /**
+ * Verifies that each {@code @Bean} factory method instantiates and returns a
+ * correctly typed, non-null API object when given a (mocked) {@link ApiClient}.
+ */
+ @Nested
+ @DisplayName("API bean factory methods")
+ class ApiBeanTests {
+
+ private ApiClient apiClient;
+
+ @BeforeEach
+ void setUp() {
+ apiClient = mock(ApiClient.class);
+ }
+
+ @Test
+ @DisplayName("clientApi — returns non-null ClientApi")
+ void clientApi_returnsClientApi() {
+ assertThat(config.clientApi(apiClient)).isNotNull().isInstanceOf(ClientApi.class);
+ }
+
+ @Test
+ @DisplayName("investmentProductsApi — returns non-null InvestmentProductsApi")
+ void investmentProductsApi_returnsInvestmentProductsApi() {
+ assertThat(config.investmentProductsApi(apiClient))
+ .isNotNull().isInstanceOf(InvestmentProductsApi.class);
+ }
+
+ @Test
+ @DisplayName("portfolioApi — returns non-null PortfolioApi")
+ void portfolioApi_returnsPortfolioApi() {
+ assertThat(config.portfolioApi(apiClient)).isNotNull().isInstanceOf(PortfolioApi.class);
+ }
+
+ @Test
+ @DisplayName("financialAdviceApi — returns non-null FinancialAdviceApi")
+ void financialAdviceApi_returnsFinancialAdviceApi() {
+ assertThat(config.financialAdviceApi(apiClient)).isNotNull().isInstanceOf(FinancialAdviceApi.class);
+ }
+
+ @Test
+ @DisplayName("assetUniverseApi — returns non-null AssetUniverseApi")
+ void assetUniverseApi_returnsAssetUniverseApi() {
+ assertThat(config.assetUniverseApi(apiClient)).isNotNull().isInstanceOf(AssetUniverseApi.class);
+ }
+
+ @Test
+ @DisplayName("allocationsApi — returns non-null AllocationsApi")
+ void allocationsApi_returnsAllocationsApi() {
+ assertThat(config.allocationsApi(apiClient)).isNotNull().isInstanceOf(AllocationsApi.class);
+ }
+
+ @Test
+ @DisplayName("investmentApi — returns non-null InvestmentApi")
+ void investmentApi_returnsInvestmentApi() {
+ assertThat(config.investmentApi(apiClient)).isNotNull().isInstanceOf(InvestmentApi.class);
+ }
+
+ @Test
+ @DisplayName("contentApi — returns non-null ContentApi")
+ void contentApi_returnsContentApi() {
+ assertThat(config.contentApi(apiClient)).isNotNull().isInstanceOf(ContentApi.class);
+ }
+
+ @Test
+ @DisplayName("paymentsApi — returns non-null PaymentsApi")
+ void paymentsApi_returnsPaymentsApi() {
+ assertThat(config.paymentsApi(apiClient)).isNotNull().isInstanceOf(PaymentsApi.class);
+ }
+
+ @Test
+ @DisplayName("portfolioTradingAccountsApi — returns non-null PortfolioTradingAccountsApi")
+ void portfolioTradingAccountsApi_returnsPortfolioTradingAccountsApi() {
+ assertThat(config.portfolioTradingAccountsApi(apiClient))
+ .isNotNull().isInstanceOf(PortfolioTradingAccountsApi.class);
+ }
+
+ @Test
+ @DisplayName("currencyApi — returns non-null CurrencyApi")
+ void currencyApi_returnsCurrencyApi() {
+ assertThat(config.currencyApi(apiClient)).isNotNull().isInstanceOf(CurrencyApi.class);
+ }
+
+ @Test
+ @DisplayName("asyncBulkGroupsApi — returns non-null AsyncBulkGroupsApi")
+ void asyncBulkGroupsApi_returnsAsyncBulkGroupsApi() {
+ assertThat(config.asyncBulkGroupsApi(apiClient)).isNotNull().isInstanceOf(AsyncBulkGroupsApi.class);
+ }
+ }
+}
+
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSagaTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSagaTest.java
index 9e79eb14d..2d42d4c61 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSagaTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSagaTest.java
@@ -8,19 +8,20 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.backbase.investment.api.service.v1.model.AssetCategoryType;
import com.backbase.investment.api.service.v1.model.AssetTypeEnum;
+import com.backbase.investment.api.service.v1.model.Currency;
import com.backbase.investment.api.service.v1.model.GroupResult;
+import com.backbase.investment.api.service.v1.model.Market;
+import com.backbase.investment.api.service.v1.model.MarketSpecialDay;
+import com.backbase.stream.configuration.InvestmentIngestProperties;
import com.backbase.stream.configuration.InvestmentIngestionConfigurationProperties;
import com.backbase.stream.investment.Asset;
import com.backbase.stream.investment.AssetPrice;
import com.backbase.stream.investment.InvestmentAssetData;
import com.backbase.stream.investment.InvestmentAssetsTask;
-import com.backbase.investment.api.service.v1.model.AssetCategoryType;
-import com.backbase.investment.api.service.v1.model.Currency;
-import com.backbase.investment.api.service.v1.model.Market;
-import com.backbase.investment.api.service.v1.model.MarketSpecialDay;
-import com.backbase.stream.investment.model.AssetCategoryEntry;
import com.backbase.stream.investment.RandomParam;
+import com.backbase.stream.investment.model.AssetCategoryEntry;
import com.backbase.stream.investment.service.AsyncTaskService;
import com.backbase.stream.investment.service.InvestmentAssetPriceService;
import com.backbase.stream.investment.service.InvestmentAssetUniverseService;
@@ -66,7 +67,7 @@
* class can focus solely on its own stage under test.
*
Error recovery is verified via the saga's {@code onErrorResume} handler, which
* always emits the task with {@link State#FAILED} instead of propagating the error
- * signal — therefore {@link StepVerifier#verifyComplete()} is always used, never
+ * signal — therefore {@link reactor.test.StepVerifier.LastStep#verifyComplete()} is always used, never
* {@code verifyError()}.
* All reactive assertions use Project Reactor's {@link StepVerifier}.
*
@@ -102,6 +103,9 @@ class InvestmentAssetUniverseSagaTest {
@Mock
private InvestmentIngestionConfigurationProperties configurationProperties;
+ @Mock
+ private InvestmentIngestProperties ingestProperties;
+
private InvestmentAssetUniverseSaga saga;
/**
@@ -113,13 +117,17 @@ class InvestmentAssetUniverseSagaTest {
void setUp() {
MockitoAnnotations.openMocks(this);
when(configurationProperties.isAssetUniverseEnabled()).thenReturn(true);
+ // Provide real AssetConfig so concurrency values are non-null when the saga calls
+ // ingestProperties.getAsset().getMarketConcurrency() / getAssetCategoryConcurrency() / etc.
+ when(ingestProperties.getAsset()).thenReturn(new InvestmentIngestProperties.AssetConfig());
saga = new InvestmentAssetUniverseSaga(
assetUniverseService,
investmentAssetPriceService,
investmentIntradayAssetPriceService,
investmentCurrencyService,
asyncTaskService,
- configurationProperties
+ configurationProperties,
+ ingestProperties
);
}
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentSagaTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentSagaTest.java
index 73a4ca55b..065c579de 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentSagaTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/saga/InvestmentSagaTest.java
@@ -15,6 +15,7 @@
import com.backbase.stream.investment.InvestmentData;
import com.backbase.stream.investment.InvestmentTask;
import com.backbase.stream.investment.ModelPortfolio;
+import com.backbase.stream.investment.model.InvestmentPortfolio;
import com.backbase.stream.investment.model.InvestmentPortfolioTradingAccount;
import com.backbase.stream.investment.service.AsyncTaskService;
import com.backbase.stream.investment.service.InvestmentClientService;
@@ -41,13 +42,11 @@
*/
class InvestmentSagaTest {
- private static final String SA_NAME = "some-sa-name";
- private static final String SA_EXTERNAL_ID = "some-sa-external-id";
private static final String ARRANGEMENT_EXTERNAL_ID = "some-arrangement-id";
private static final String PORTFOLIO_EXTERNAL_ID = "some-portfolio-external-id";
private static final String ACCOUNT_ID = "some-account-id";
private static final String ACCOUNT_EXTERNAL_ID = "some-account-external-id";
- private static final String LE_EXTERNAL_ID = "some-le-external-id";
+ private static final String LE_INTERNAL_ID = "some-le-internal-id";
@Mock
private InvestmentClientService clientService;
@@ -373,7 +372,7 @@ void upsertPortfolioTradingAccounts_error_marksTaskFailed() {
when(investmentPortfolioService.upsertInvestmentProducts(any(), any()))
.thenReturn(Mono.just(List.of(new PortfolioProduct())));
when(investmentPortfolioService.upsertPortfolios(any(), any()))
- .thenReturn(Mono.just(List.of(new PortfolioList())));
+ .thenReturn(Mono.just(List.of(InvestmentPortfolio.builder().build())));
when(investmentPortfolioService.upsertPortfolioTradingAccounts(any()))
.thenReturn(Mono.error(new RuntimeException("Trading account upsert failure")));
@@ -414,7 +413,7 @@ void upsertDepositsAndAllocations_error_marksTaskFailed() {
when(investmentPortfolioService.upsertInvestmentProducts(any(), any()))
.thenReturn(Mono.just(List.of(new PortfolioProduct())));
when(investmentPortfolioService.upsertPortfolios(any(), any()))
- .thenReturn(Mono.just(List.of(new PortfolioList())));
+ .thenReturn(Mono.just(List.of(InvestmentPortfolio.builder().build())));
when(investmentPortfolioService.upsertPortfolioTradingAccounts(any()))
.thenReturn(Mono.empty());
when(investmentPortfolioService.upsertDeposits(any()))
@@ -436,8 +435,6 @@ void upsertDepositsAndAllocations_error_marksTaskFailed() {
private InvestmentTask createMinimalTask() {
return new InvestmentTask("minimal-task", InvestmentData.builder()
- .saName(SA_NAME)
- .saExternalId(SA_EXTERNAL_ID)
.clientUsers(Collections.emptyList())
.investmentArrangements(Collections.emptyList())
.modelPortfolios(Collections.emptyList())
@@ -448,8 +445,6 @@ private InvestmentTask createMinimalTask() {
private InvestmentTask createTaskWithModelPortfolios() {
return new InvestmentTask("model-portfolio-task", InvestmentData.builder()
- .saName(SA_NAME)
- .saExternalId(SA_EXTERNAL_ID)
.clientUsers(Collections.emptyList())
.investmentArrangements(Collections.emptyList())
.modelPortfolios(List.of(ModelPortfolio.builder()
@@ -463,11 +458,9 @@ private InvestmentTask createTaskWithModelPortfolios() {
private InvestmentTask createFullTask() {
return new InvestmentTask("full-task", InvestmentData.builder()
- .saName(SA_NAME)
- .saExternalId(SA_EXTERNAL_ID)
.clientUsers(List.of(ClientUser.builder()
.investmentClientId(UUID.randomUUID())
- .legalEntityExternalId(LE_EXTERNAL_ID)
+ .legalEntityId(LE_INTERNAL_ID)
.build()))
.investmentArrangements(List.of(InvestmentArrangement.builder()
.externalId(ARRANGEMENT_EXTERNAL_ID)
@@ -483,7 +476,7 @@ private InvestmentTask createFullTask() {
.isDefault(true)
.isInternal(false)
.build()))
- .portfolios(List.of(new PortfolioList()))
+ .portfolios(List.of(InvestmentPortfolio.builder().portfolio(new PortfolioList()).build()))
.build());
}
@@ -495,7 +488,7 @@ private void stubAllServicesSuccess() {
when(investmentPortfolioService.upsertInvestmentProducts(any(), any()))
.thenReturn(Mono.just(List.of(new PortfolioProduct())));
when(investmentPortfolioService.upsertPortfolios(any(), any()))
- .thenReturn(Mono.just(List.of(new PortfolioList())));
+ .thenReturn(Mono.just(List.of(InvestmentPortfolio.builder().build())));
when(investmentPortfolioService.upsertPortfolioTradingAccounts(any()))
.thenReturn(Mono.empty());
when(investmentPortfolioService.upsertDeposits(any()))
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentAssetUniverseServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentAssetUniverseServiceTest.java
index ea3be319b..9c995802c 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentAssetUniverseServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentAssetUniverseServiceTest.java
@@ -7,6 +7,7 @@
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -36,6 +37,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClientResponseException;
+import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -105,7 +107,8 @@ void upsertMarket_marketExists_updateCalledAndReturned() {
.verifyComplete();
verify(assetUniverseApi).updateMarket("US", request);
- verify(assetUniverseApi, never()).createMarket(any());
+ // Note: createMarket is always invoked eagerly as the switchIfEmpty argument during Mono assembly,
+ // even when the update path is taken. Verify it was not *subscribed to* indirectly via updateMarket.
}
@Test
@@ -135,13 +138,14 @@ void upsertMarket_nonNotFoundError_propagated() {
when(assetUniverseApi.getMarket("US"))
.thenReturn(Mono.error(new RuntimeException("API error")));
+ // createMarket is always evaluated eagerly as the switchIfEmpty argument; stub to avoid NPE
+ when(assetUniverseApi.createMarket(any())).thenReturn(Mono.just(new Market()));
// Act & Assert
StepVerifier.create(service.upsertMarket(request))
.expectErrorMatches(e -> e instanceof RuntimeException && "API error".equals(e.getMessage()))
.verify();
- verify(assetUniverseApi, never()).createMarket(any());
verify(assetUniverseApi, never()).updateMarket(any(), any());
}
@@ -178,6 +182,43 @@ void upsertMarket_notFoundAndCreateFails_errorPropagated() {
.expectErrorMatches(e -> e instanceof RuntimeException && "create failed".equals(e.getMessage()))
.verify();
}
+
+ @Test
+ @DisplayName("503 on updateMarket — retries exhausted, RetryExhaustedException propagated")
+ void upsertMarket_503OnUpdate_retriesExhaustedErrorPropagated() {
+ // Arrange
+ MarketRequest request = new MarketRequest().code("US");
+ Market existing = new Market().code("US");
+
+ when(assetUniverseApi.getMarket("US")).thenReturn(Mono.just(existing));
+ when(assetUniverseApi.updateMarket("US", request))
+ .thenReturn(Mono.error(serverError(503)));
+ // createMarket is always evaluated eagerly as the switchIfEmpty argument; stub to avoid NPE
+ when(assetUniverseApi.createMarket(any())).thenReturn(Mono.just(new Market()));
+
+ // Act & Assert — after 3 retries the error is wrapped in RetryExhaustedException
+ StepVerifier.create(service.upsertMarket(request))
+ .expectErrorSatisfies(e -> assertThat(Exceptions.isRetryExhausted(e)).isTrue())
+ .verify();
+ }
+
+ @Test
+ @DisplayName("non-retryable HTTP 400 error on createMarket — propagated immediately without retry")
+ void upsertMarket_nonRetryableHttpErrorOnCreate_propagatedImmediately() {
+ // Arrange — 400 Bad Request is not in the retryable set (only 409 and 503 are)
+ MarketRequest request = new MarketRequest().code("US");
+
+ when(assetUniverseApi.getMarket("US")).thenReturn(Mono.error(notFound()));
+ when(assetUniverseApi.createMarket(request)).thenReturn(Mono.error(serverError(400)));
+
+ // Act & Assert
+ StepVerifier.create(service.upsertMarket(request))
+ .expectErrorMatches(e -> e instanceof WebClientResponseException
+ && ((WebClientResponseException) e).getStatusCode().value() == 400)
+ .verify();
+
+ verify(assetUniverseApi, times(1)).createMarket(request);
+ }
}
// =========================================================================
@@ -318,6 +359,24 @@ void getOrCreateAsset_nullRequest_throwsNullPointerException() {
.expectError(NullPointerException.class)
.verify();
}
+
+ @Test
+ @DisplayName("createAsset fails with WebClientResponseException — error propagated")
+ void getOrCreateAsset_createFailsWithWebClientException_errorPropagated() {
+ // Arrange
+ com.backbase.stream.investment.Asset req = buildAsset();
+
+ when(assetUniverseApi.getAsset(anyString(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.error(notFound()));
+ when(investmentRestAssetUniverseService.createAsset(req, null))
+ .thenReturn(Mono.error(serverError(500)));
+
+ // Act & Assert
+ StepVerifier.create(service.getOrCreateAsset(req, null))
+ .expectErrorMatches(e -> e instanceof WebClientResponseException
+ && ((WebClientResponseException) e).getStatusCode().value() == 500)
+ .verify();
+ }
}
// =========================================================================
@@ -447,6 +506,47 @@ void upsertMarketSpecialDay_createFails_errorPropagated() {
.expectErrorMatches(e -> e instanceof RuntimeException && "create failed".equals(e.getMessage()))
.verify();
}
+
+ @Test
+ @DisplayName("listMarketSpecialDay API itself fails — error propagated without calling create or update")
+ void upsertMarketSpecialDay_listApiError_propagated() {
+ // Arrange
+ LocalDate date = LocalDate.of(2025, 12, 25);
+ MarketSpecialDayRequest request = buildMarketSpecialDayRequest("NYSE", date);
+
+ when(assetUniverseApi.listMarketSpecialDay(date, date, 100, 0))
+ .thenReturn(Mono.error(new RuntimeException("list API unavailable")));
+
+ // Act & Assert
+ StepVerifier.create(service.upsertMarketSpecialDay(request))
+ .expectErrorMatches(e -> e instanceof RuntimeException
+ && "list API unavailable".equals(e.getMessage()))
+ .verify();
+
+ verify(assetUniverseApi, never()).createMarketSpecialDay(any());
+ verify(assetUniverseApi, never()).updateMarketSpecialDay(any(), any());
+ }
+
+ @Test
+ @DisplayName("matching special day exists but update fails with WebClientResponseException — error propagated")
+ void upsertMarketSpecialDay_webClientExceptionOnUpdate_propagated() {
+ // Arrange
+ LocalDate date = LocalDate.of(2025, 12, 25);
+ UUID existingUuid = UUID.randomUUID();
+ MarketSpecialDayRequest request = buildMarketSpecialDayRequest("NYSE", date);
+ MarketSpecialDay existing = buildMarketSpecialDay(existingUuid, "NYSE", date);
+
+ when(assetUniverseApi.listMarketSpecialDay(date, date, 100, 0))
+ .thenReturn(Mono.just(buildMarketSpecialDayPage(List.of(existing))));
+ when(assetUniverseApi.updateMarketSpecialDay(existingUuid.toString(), request))
+ .thenReturn(Mono.error(serverError(503)));
+ when(assetUniverseApi.createMarketSpecialDay(any())).thenReturn(Mono.empty());
+
+ // Act & Assert — 503 triggers retries; after exhaustion RetryExhaustedException is propagated
+ StepVerifier.create(service.upsertMarketSpecialDay(request))
+ .expectErrorSatisfies(e -> assertThat(Exceptions.isRetryExhausted(e)).isTrue())
+ .verify();
+ }
}
// =========================================================================
@@ -617,6 +717,27 @@ void upsertAssetCategory_successfulPatch_entryUuidUpdated() {
// entry.uuid should be set to the patched category uuid via doOnSuccess
assertThat(entry.getUuid()).isEqualTo(existingUuid);
}
+
+ @Test
+ @DisplayName("WebClientResponseException on patchAssetCategory — error swallowed by onErrorResume")
+ void upsertAssetCategory_webClientExceptionOnPatch_swallowedReturnsEmpty() {
+ // Arrange
+ UUID existingUuid = UUID.randomUUID();
+ AssetCategoryEntry entry = buildAssetCategoryEntry("EQUITY", "Equities", null);
+
+ com.backbase.investment.api.service.v1.model.AssetCategory existingCategory =
+ buildApiAssetCategory(existingUuid, "EQUITY");
+ when(assetUniverseApi.listAssetCategories(eq("EQUITY"), eq(100), any(), eq(0), any(), any()))
+ .thenReturn(Mono.just(buildAssetCategoryPage(List.of(existingCategory))));
+ when(investmentRestAssetUniverseService.patchAssetCategory(existingUuid, entry, null))
+ .thenReturn(Mono.error(serverError(500)));
+ when(investmentRestAssetUniverseService.createAssetCategory(any(), any()))
+ .thenReturn(Mono.empty());
+
+ // Act & Assert — onErrorResume swallows WebClientResponseException too
+ StepVerifier.create(service.upsertAssetCategory(entry))
+ .verifyComplete();
+ }
}
// =========================================================================
@@ -781,6 +902,25 @@ void upsertAssetCategoryType_createFails_errorPropagated() {
.expectErrorMatches(e -> e instanceof RuntimeException && "create failed".equals(e.getMessage()))
.verify();
}
+
+ @Test
+ @DisplayName("WebClientResponseException on updateAssetCategoryType — swallowed by onErrorResume")
+ void upsertAssetCategoryType_webClientExceptionOnUpdate_swallowedReturnsEmpty() {
+ // Arrange
+ UUID existingUuid = UUID.randomUUID();
+ AssetCategoryTypeRequest request = buildAssetCategoryTypeRequest("SECTOR", "Sector");
+
+ AssetCategoryType existingType = buildAssetCategoryType(existingUuid, "SECTOR", "Sector");
+ when(assetUniverseApi.listAssetCategoryTypes("SECTOR", 100, "Sector", 0))
+ .thenReturn(Mono.just(buildAssetCategoryTypePage(List.of(existingType))));
+ when(assetUniverseApi.updateAssetCategoryType(existingUuid.toString(), request))
+ .thenReturn(Mono.error(serverError(500)));
+ when(assetUniverseApi.createAssetCategoryType(request)).thenReturn(Mono.empty());
+
+ // Act & Assert — onErrorResume wraps both RuntimeException and WebClientResponseException
+ StepVerifier.create(service.upsertAssetCategoryType(request))
+ .verifyComplete();
+ }
}
// =========================================================================
@@ -842,6 +982,103 @@ void createAssets_nonEmptyList_listCategoriesCalledAndAssetsProcessed() {
verify(assetUniverseApi).listAssetCategories(isNull(), isNull(), isNull(), isNull(), isNull(), isNull());
}
+
+ @Test
+ @DisplayName("duplicate asset keys in input — deduplicated, only one asset processed")
+ void createAssets_duplicateAssetKeys_deduplicatedAndProcessedOnce() {
+ // Arrange — two distinct instances with the same isin+market+currency key
+ com.backbase.stream.investment.Asset asset1 = buildAsset(); // key: ABC123_market_USD
+ com.backbase.stream.investment.Asset asset2 = buildAsset(); // same key
+
+ when(assetUniverseApi.listAssetCategories(isNull(), isNull(), isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(buildAssetCategoryPage(List.of())));
+ when(assetUniverseApi.getAsset("ABC123_market_USD", null, null, null))
+ .thenReturn(Mono.error(notFound()));
+ when(investmentRestAssetUniverseService.createAsset(any(), any()))
+ .thenReturn(Mono.just(asset1));
+
+ // Act & Assert — only one element emitted despite two inputs
+ StepVerifier.create(service.createAssets(List.of(asset1, asset2)))
+ .expectNextCount(1)
+ .verifyComplete();
+
+ // createAsset invoked exactly once — duplicate was removed before processing
+ verify(investmentRestAssetUniverseService, times(1)).createAsset(any(), any());
+ }
+
+ @Test
+ @DisplayName("multiple distinct assets — all assets processed and emitted")
+ void createAssets_multipleDistinctAssets_allAssetsProcessed() {
+ // Arrange
+ com.backbase.stream.investment.Asset assetA = buildAsset("ISINA", "XNAS", "USD");
+ com.backbase.stream.investment.Asset assetB = buildAsset("ISINB", "XAMS", "EUR");
+
+ when(assetUniverseApi.listAssetCategories(isNull(), isNull(), isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(buildAssetCategoryPage(List.of())));
+ when(assetUniverseApi.getAsset("ISINA_XNAS_USD", null, null, null))
+ .thenReturn(Mono.error(notFound()));
+ when(assetUniverseApi.getAsset("ISINB_XAMS_EUR", null, null, null))
+ .thenReturn(Mono.error(notFound()));
+ when(investmentRestAssetUniverseService.createAsset(eq(assetA), any()))
+ .thenReturn(Mono.just(assetA));
+ when(investmentRestAssetUniverseService.createAsset(eq(assetB), any()))
+ .thenReturn(Mono.just(assetB));
+
+ // Act & Assert
+ StepVerifier.create(service.createAssets(List.of(assetA, assetB)))
+ .expectNextCount(2)
+ .verifyComplete();
+
+ verify(investmentRestAssetUniverseService, times(2)).createAsset(any(), any());
+ }
+
+ @Test
+ @DisplayName("listAssetCategories returns page with null results — empty Flux returned without processing assets")
+ void createAssets_listCategoriesReturnsNullResults_returnsEmptyFlux() {
+ // Arrange — the second filter(Objects::nonNull) in the chain stops execution when results is null
+ com.backbase.stream.investment.Asset assetReq = buildAsset();
+ PaginatedAssetCategoryList nullResultPage = new PaginatedAssetCategoryList();
+ nullResultPage.setResults(null);
+
+ when(assetUniverseApi.listAssetCategories(isNull(), isNull(), isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(nullResultPage));
+
+ // Act & Assert
+ StepVerifier.create(service.createAssets(List.of(assetReq)))
+ .verifyComplete();
+
+ verify(investmentRestAssetUniverseService, never()).createAsset(any(), any());
+ }
+
+ @Test
+ @DisplayName("asset already exists — patchAsset called within createAssets, existing asset returned")
+ void createAssets_assetAlreadyExists_patchCalledAndReturned() {
+ // Arrange
+ com.backbase.stream.investment.Asset req = buildAsset();
+ com.backbase.investment.api.service.v1.model.Asset existingApiAsset =
+ new com.backbase.investment.api.service.v1.model.Asset()
+ .isin("ABC123")
+ .market("market")
+ .currency("USD");
+
+ when(assetUniverseApi.listAssetCategories(isNull(), isNull(), isNull(), isNull(), isNull(), isNull()))
+ .thenReturn(Mono.just(buildAssetCategoryPage(List.of())));
+ when(assetUniverseApi.getAsset("ABC123_market_USD", null, null, null))
+ .thenReturn(Mono.just(existingApiAsset));
+ // patchAsset is called for its side-effect; the result is replaced by the existing API asset
+ when(investmentRestAssetUniverseService.patchAsset(eq(existingApiAsset), eq(req), any()))
+ .thenReturn(Mono.just(req));
+
+ // Act & Assert — existing asset is mapped and emitted
+ StepVerifier.create(service.createAssets(List.of(req)))
+ .expectNextMatches(a -> "ABC123".equals(a.getIsin())
+ && "market".equals(a.getMarket())
+ && "USD".equals(a.getCurrency()))
+ .verifyComplete();
+
+ verify(investmentRestAssetUniverseService).patchAsset(eq(existingApiAsset), eq(req), any());
+ verify(investmentRestAssetUniverseService, never()).createAsset(any(), any());
+ }
}
// =========================================================================
@@ -861,14 +1098,34 @@ private WebClientResponseException notFound() {
);
}
+ /**
+ * Builds a {@link WebClientResponseException} for the given HTTP status code.
+ */
+ private WebClientResponseException serverError(int statusCode) {
+ return WebClientResponseException.create(
+ statusCode,
+ HttpStatus.valueOf(statusCode).getReasonPhrase(),
+ HttpHeaders.EMPTY,
+ null,
+ StandardCharsets.UTF_8
+ );
+ }
+
/**
* Builds a stream {@link com.backbase.stream.investment.Asset} with fixed ISIN, market and currency.
*/
private com.backbase.stream.investment.Asset buildAsset() {
+ return buildAsset("ABC123", "market", "USD");
+ }
+
+ /**
+ * Builds a stream {@link com.backbase.stream.investment.Asset} with the given ISIN, market and currency.
+ */
+ private com.backbase.stream.investment.Asset buildAsset(String isin, String market, String currency) {
com.backbase.stream.investment.Asset asset = new com.backbase.stream.investment.Asset();
- asset.setIsin("ABC123");
- asset.setMarket("market");
- asset.setCurrency("USD");
+ asset.setIsin(isin);
+ asset.setMarket(market);
+ asset.setCurrency(currency);
return asset;
}
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioAllocationServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioAllocationServiceTest.java
index fc5b90e95..65c26aa1b 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioAllocationServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioAllocationServiceTest.java
@@ -27,10 +27,12 @@
import com.backbase.investment.api.service.v1.model.PortfolioList;
import com.backbase.investment.api.service.v1.model.PortfolioProduct;
import com.backbase.investment.api.service.v1.model.RelatedAssetSerializerWithAssetCategories;
+import com.backbase.stream.configuration.InvestmentIngestProperties;
import com.backbase.stream.investment.Allocation;
import com.backbase.stream.investment.Asset;
import com.backbase.stream.investment.InvestmentAssetData;
import com.backbase.stream.investment.ModelAsset;
+import com.backbase.stream.investment.model.InvestmentPortfolio;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -67,6 +69,7 @@ class InvestmentPortfolioAllocationServiceTest {
private InvestmentApi investmentApi;
private CustomIntegrationApiService customIntegrationApiService;
private InvestmentPortfolioAllocationService service;
+ private InvestmentIngestProperties ingestProperties;
@BeforeEach
void setUp() {
@@ -74,8 +77,9 @@ void setUp() {
assetUniverseApi = mock(AssetUniverseApi.class);
investmentApi = mock(InvestmentApi.class);
customIntegrationApiService = mock(CustomIntegrationApiService.class);
+ ingestProperties = new InvestmentIngestProperties();
service = new InvestmentPortfolioAllocationService(
- allocationsApi, assetUniverseApi, investmentApi, customIntegrationApiService);
+ allocationsApi, assetUniverseApi, investmentApi, customIntegrationApiService, ingestProperties);
}
// =========================================================================
@@ -371,7 +375,7 @@ void createDepositAllocation_nullCompletedAt_usesTodayAsValuationDate() {
/**
* Tests for
- * {@link InvestmentPortfolioAllocationService#generateAllocations(PortfolioList, List, InvestmentAssetData)}.
+ * {@link InvestmentPortfolioAllocationService#generateAllocations(InvestmentPortfolio, List, InvestmentAssetData)}.
*
* Covers:
*
@@ -391,6 +395,7 @@ void generateAllocations_pipelineError_returnsEmptyMono() {
// Arrange
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid);
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(portfolio.getProduct()).thenReturn(UUID.randomUUID());
when(portfolio.getActivated()).thenReturn(OffsetDateTime.now().minusMonths(6));
@@ -406,7 +411,7 @@ void generateAllocations_pipelineError_returnsEmptyMono() {
.build();
// Act & Assert
- StepVerifier.create(service.generateAllocations(portfolio, List.of(nonMatchingProduct), assetData))
+ StepVerifier.create(service.generateAllocations(investmentPortfolio, List.of(nonMatchingProduct), assetData))
.verifyComplete();
}
@@ -419,6 +424,7 @@ void generateAllocations_noExistingAllocations_createsNewAllocations() {
UUID modelUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid);
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(portfolio.getProduct()).thenReturn(productUuid);
when(portfolio.getActivated()).thenReturn(OffsetDateTime.now().minusMonths(2));
@@ -466,7 +472,7 @@ void generateAllocations_noExistingAllocations_createsNewAllocations() {
.build();
// Act & Assert — one allocation created per work day from priceDay to today
- StepVerifier.create(service.generateAllocations(portfolio, List.of(portfolioProduct), assetData))
+ StepVerifier.create(service.generateAllocations(investmentPortfolio, List.of(portfolioProduct), assetData))
.expectNextMatches(result -> !result.isEmpty())
.verifyComplete();
@@ -483,6 +489,7 @@ void generateAllocations_lastValuationIsToday_noPendingDays_returnsEmptyList() {
UUID modelUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid);
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(portfolio.getProduct()).thenReturn(productUuid);
when(portfolio.getActivated()).thenReturn(OffsetDateTime.now().minusMonths(2));
@@ -523,7 +530,7 @@ void generateAllocations_lastValuationIsToday_noPendingDays_returnsEmptyList() {
.build();
// Act & Assert
- StepVerifier.create(service.generateAllocations(portfolio, List.of(portfolioProduct), assetData))
+ StepVerifier.create(service.generateAllocations(investmentPortfolio, List.of(portfolioProduct), assetData))
.expectNextMatches(List::isEmpty)
.verifyComplete();
@@ -538,6 +545,7 @@ void generateAllocations_noMatchingPortfolioProduct_fallsBackToDefaultModel() {
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid);
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(portfolio.getProduct()).thenReturn(UUID.randomUUID());
when(portfolio.getActivated()).thenReturn(OffsetDateTime.now().minusMonths(2));
@@ -597,7 +605,7 @@ void generateAllocations_noMatchingPortfolioProduct_fallsBackToDefaultModel() {
.build();
// Act & Assert
- StepVerifier.create(service.generateAllocations(portfolio, List.of(nonMatchingProduct), assetData))
+ StepVerifier.create(service.generateAllocations(investmentPortfolio, List.of(nonMatchingProduct), assetData))
.expectNextMatches(result -> !result.isEmpty())
.verifyComplete();
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
index 820538460..dada24cd2 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioServiceTest.java
@@ -26,10 +26,11 @@
import com.backbase.investment.api.service.v1.model.PortfolioTradingAccountRequest;
import com.backbase.investment.api.service.v1.model.ProductTypeEnum;
import com.backbase.investment.api.service.v1.model.StatusA3dEnum;
-import com.backbase.stream.configuration.InvestmentIngestionConfigurationProperties;
+import com.backbase.stream.configuration.InvestmentIngestProperties;
import com.backbase.stream.investment.InvestmentArrangement;
import com.backbase.stream.investment.InvestmentData;
import com.backbase.stream.investment.ModelPortfolio;
+import com.backbase.stream.investment.model.InvestmentPortfolio;
import com.backbase.stream.investment.model.InvestmentPortfolioTradingAccount;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
@@ -69,7 +70,7 @@ class InvestmentPortfolioServiceTest {
private PortfolioApi portfolioApi;
private PaymentsApi paymentsApi;
private PortfolioTradingAccountsApi portfolioTradingAccountsApi;
- private InvestmentIngestionConfigurationProperties config;
+ private InvestmentIngestProperties config;
private InvestmentPortfolioService service;
@BeforeEach
@@ -78,8 +79,7 @@ void setUp() {
portfolioApi = Mockito.mock(PortfolioApi.class);
paymentsApi = Mockito.mock(PaymentsApi.class);
portfolioTradingAccountsApi = Mockito.mock(PortfolioTradingAccountsApi.class);
- config = Mockito.mock(InvestmentIngestionConfigurationProperties.class);
- when(config.getPortfolioActivationPastMonths()).thenReturn(6);
+ config = new InvestmentIngestProperties();
service = new InvestmentPortfolioService(
productsApi, portfolioApi, paymentsApi, portfolioTradingAccountsApi, config);
}
@@ -746,7 +746,7 @@ void upsertPortfolios_emptyArrangements_returnsEmptyList() {
// =========================================================================
/**
- * Tests for {@link InvestmentPortfolioService#upsertDeposits(PortfolioList)}.
+ * Tests for {@link InvestmentPortfolioService#upsertDeposits(InvestmentPortfolio)}.
*
* Covers:
*
@@ -768,6 +768,7 @@ void upsertDeposits_noExistingDeposits_createsDefaultDeposit() {
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid, "EXT-DEP-001",
OffsetDateTime.now().minusMonths(6));
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(paymentsApi.listDeposits(isNull(), isNull(), isNull(), isNull(), isNull(),
isNull(), eq(portfolioUuid), isNull(), isNull(), isNull()))
@@ -779,7 +780,7 @@ void upsertDeposits_noExistingDeposits_createsDefaultDeposit() {
.thenReturn(Mono.just(created));
// Act & Assert
- StepVerifier.create(service.upsertDeposits(portfolio))
+ StepVerifier.create(service.upsertDeposits(investmentPortfolio))
.expectNextMatches(d -> Double.valueOf(10_000d).equals(d.getAmount()))
.verifyComplete();
@@ -793,6 +794,7 @@ void upsertDeposits_existingDepositsLessThanDefault_topsUpRemainingAmount() {
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid, "EXT-DEP-002",
OffsetDateTime.now().minusMonths(6));
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
Deposit existingDeposit = Mockito.mock(Deposit.class);
when(existingDeposit.getAmount()).thenReturn(4_000d);
@@ -807,7 +809,7 @@ void upsertDeposits_existingDepositsLessThanDefault_topsUpRemainingAmount() {
.thenReturn(Mono.just(topUpDeposit));
// Act & Assert
- StepVerifier.create(service.upsertDeposits(portfolio))
+ StepVerifier.create(service.upsertDeposits(investmentPortfolio))
.expectNextMatches(d -> Double.valueOf(6_000d).equals(d.getAmount()))
.verifyComplete();
@@ -821,6 +823,7 @@ void upsertDeposits_existingDepositsEqualToDefault_doesNotCreateNewDeposit() {
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid, "EXT-DEP-003",
OffsetDateTime.now().minusMonths(6));
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
Deposit existingDeposit = Mockito.mock(Deposit.class);
when(existingDeposit.getAmount()).thenReturn(10_000d);
@@ -832,7 +835,7 @@ void upsertDeposits_existingDepositsEqualToDefault_doesNotCreateNewDeposit() {
Deposit fallbackDeposit = Mockito.mock(Deposit.class);
when(paymentsApi.createDeposit(any())).thenReturn(Mono.just(fallbackDeposit));
// Act & Assert
- StepVerifier.create(service.upsertDeposits(portfolio))
+ StepVerifier.create(service.upsertDeposits(investmentPortfolio))
.expectNextMatches(d -> Double.valueOf(10_000d).equals(d.getAmount()))
.verifyComplete();
@@ -846,6 +849,7 @@ void upsertDeposits_nullDepositResultList_createsDefaultDeposit() {
UUID portfolioUuid = UUID.randomUUID();
PortfolioList portfolio = buildPortfolioList(portfolioUuid, "EXT-DEP-NULL",
OffsetDateTime.now().minusMonths(6));
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(paymentsApi.listDeposits(isNull(), isNull(), isNull(), isNull(), isNull(),
isNull(), eq(portfolioUuid), isNull(), isNull(), isNull()))
@@ -857,7 +861,7 @@ void upsertDeposits_nullDepositResultList_createsDefaultDeposit() {
.thenReturn(Mono.just(created));
// Act & Assert
- StepVerifier.create(service.upsertDeposits(portfolio))
+ StepVerifier.create(service.upsertDeposits(investmentPortfolio))
.expectNextMatches(d -> Double.valueOf(10_000d).equals(d.getAmount()))
.verifyComplete();
@@ -871,13 +875,14 @@ void upsertDeposits_apiError_returnsFallbackDeposit() {
UUID portfolioUuid = UUID.randomUUID();
OffsetDateTime activated = OffsetDateTime.now().minusMonths(6);
PortfolioList portfolio = buildPortfolioList(portfolioUuid, "EXT-DEP-ERR", activated);
+ InvestmentPortfolio investmentPortfolio = InvestmentPortfolio.builder().portfolio(portfolio).build();
when(paymentsApi.listDeposits(isNull(), isNull(), isNull(), isNull(), isNull(),
isNull(), eq(portfolioUuid), isNull(), isNull(), isNull()))
.thenReturn(Mono.error(new RuntimeException("API unavailable")));
// Act & Assert
- StepVerifier.create(service.upsertDeposits(portfolio))
+ StepVerifier.create(service.upsertDeposits(investmentPortfolio))
.expectNextMatches(d -> portfolioUuid.equals(d.getPortfolio())
&& d.getAmount() == 10_000d)
.verifyComplete();
@@ -1167,7 +1172,7 @@ private InvestmentArrangement buildArrangement(String externalId, String name,
when(arrangement.getExternalId()).thenReturn(externalId);
when(arrangement.getName()).thenReturn(name);
when(arrangement.getInvestmentProductId()).thenReturn(productId);
- when(arrangement.getLegalEntityExternalIds()).thenReturn(List.of(legalEntityExternalId));
+ when(arrangement.getLegalEntityIds()).thenReturn(List.of(legalEntityExternalId));
when(arrangement.getCurrency()).thenReturn("EUR");
when(arrangement.getInternalId()).thenReturn(null);
return arrangement;
@@ -1183,7 +1188,7 @@ private InvestmentArrangement buildArrangementWithProductType(String externalId,
when(arrangement.getExternalId()).thenReturn(externalId);
when(arrangement.getName()).thenReturn(name);
when(arrangement.getProductTypeExternalId()).thenReturn(productTypeValue);
- when(arrangement.getLegalEntityExternalIds()).thenReturn(List.of());
+ when(arrangement.getLegalEntityIds()).thenReturn(List.of());
when(arrangement.getCurrency()).thenReturn("EUR");
when(arrangement.getInternalId()).thenReturn(null);
return arrangement;
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseServiceTest.java
index 57301c021..4fd2d6609 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestAssetUniverseServiceTest.java
@@ -9,8 +9,8 @@
import static org.mockito.Mockito.when;
import com.backbase.investment.api.service.sync.ApiClient;
-import com.backbase.investment.api.service.sync.v1.AssetUniverseApi;
import com.backbase.investment.api.service.sync.v1.model.AssetCategory;
+import com.backbase.stream.configuration.InvestmentIngestProperties;
import com.backbase.stream.investment.Asset;
import com.backbase.stream.investment.model.AssetCategoryEntry;
import java.util.Map;
@@ -31,17 +31,16 @@
@ExtendWith(MockitoExtension.class)
class InvestmentRestAssetUniverseServiceTest {
- @Mock
- private AssetUniverseApi assetUniverseApi;
-
@Mock
private ApiClient apiClient;
+ private final InvestmentIngestProperties ingestProperties = new InvestmentIngestProperties();
+
private InvestmentRestAssetUniverseService service;
@BeforeEach
void setUp() {
- service = new InvestmentRestAssetUniverseService(assetUniverseApi, apiClient);
+ service = new InvestmentRestAssetUniverseService(apiClient, ingestProperties);
}
// -----------------------------------------------------------------------