diff --git a/CHANGELOG.md b/CHANGELOG.md
index c11125b51..7ec80d01f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,14 @@
# Changelog
All notable changes to this project will be documented in this file.
-## [10.7.1]
+## [10.9.0]
+### Changed
+- Add product-document links in investment
+
+## [10.8.0]
### Changed
- Updated stream-investment to be able to seed extra_data to investment portfolio
-- fix content entry seeding issue
+- fix content entry seeding issue in investment
## [10.7.0]
### Changed
diff --git a/stream-investment/investment-core/pom.xml b/stream-investment/investment-core/pom.xml
index 3728e1bff..a9035f09e 100644
--- a/stream-investment/investment-core/pom.xml
+++ b/stream-investment/investment-core/pom.xml
@@ -15,7 +15,7 @@
true
- 1.6.2
+ 1.9.0
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 954c55a85..b34aca44c 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
@@ -24,6 +24,7 @@
import com.backbase.stream.investment.service.InvestmentIntradayAssetPriceService;
import com.backbase.stream.investment.service.InvestmentModelPortfolioService;
import com.backbase.stream.investment.service.InvestmentPortfolioAllocationService;
+import com.backbase.stream.investment.service.InvestmentPortfolioProductDocumentService;
import com.backbase.stream.investment.service.InvestmentPortfolioProductService;
import com.backbase.stream.investment.service.InvestmentPortfolioService;
import com.backbase.stream.investment.service.InvestmentRiskAssessmentService;
@@ -34,6 +35,7 @@
import com.backbase.stream.investment.service.resttemplate.InvestmentRestNewsContentService;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestProductPortfolioService;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -71,14 +73,23 @@ public InvestmentModelPortfolioService investmentModelPortfolioService(Financial
portfolioProperties);
}
+ @Bean
+ @Primary
+ public InvestmentPortfolioProductDocumentService investmentPortfolioProductDocumentService(
+ InvestmentProductsApi investmentProductsApi,
+ @Qualifier("restContentApi") com.backbase.investment.api.service.sync.v1.ContentApi restContentApi) {
+ return new InvestmentPortfolioProductDocumentService(investmentProductsApi, restContentApi);
+ }
+
@Bean
@Primary
public InvestmentPortfolioProductService investmentPortfolioProductService(
InvestmentProductsApi investmentProductsApi, IngestConfigProperties portfolioProperties,
InvestmentModelPortfolioService modelPortfolioService,
- InvestmentRestProductPortfolioService investmentRestProductPortfolioService) {
+ InvestmentRestProductPortfolioService investmentRestProductPortfolioService,
+ InvestmentPortfolioProductDocumentService investmentPortfolioProductDocumentService) {
return new InvestmentPortfolioProductService(investmentProductsApi, portfolioProperties, modelPortfolioService,
- investmentRestProductPortfolioService);
+ investmentRestProductPortfolioService, investmentPortfolioProductDocumentService);
}
@Bean
diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ProductPortfolio.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ProductPortfolio.java
index c88a58247..b461d6620 100644
--- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ProductPortfolio.java
+++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/ProductPortfolio.java
@@ -51,5 +51,6 @@ public class ProductPortfolio {
private ProductTypeEnum productType;
@JsonProperty(JSON_PROPERTY_EXTRA_DATA)
private Map extraData = new HashMap<>();
+ private String document;
}
diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentService.java
new file mode 100644
index 000000000..93a6dd80e
--- /dev/null
+++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentService.java
@@ -0,0 +1,173 @@
+package com.backbase.stream.investment.service;
+
+import com.backbase.investment.api.service.sync.v1.ContentApi;
+import com.backbase.investment.api.service.sync.v1.model.OASDocumentResponse;
+import com.backbase.investment.api.service.sync.v1.model.PaginatedOASDocumentResponseList;
+import com.backbase.investment.api.service.v1.InvestmentProductsApi;
+import com.backbase.investment.api.service.v1.model.PortfolioProduct;
+import com.backbase.investment.api.service.v1.model.PortfolioProductDocumentLinkRequest;
+import com.backbase.investment.api.service.v1.model.PortfolioProductDocumentResponse;
+import com.backbase.stream.investment.ProductPortfolio;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Links bootstrap content documents to portfolio products via the Investment Products API.
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class InvestmentPortfolioProductDocumentService {
+
+ private static final int CONTENT_RETRIEVE_LIMIT = 100;
+
+ private final InvestmentProductsApi productsApi;
+ private final ContentApi contentApi;
+
+ public static InvestmentPortfolioProductDocumentService noOp() {
+ return new InvestmentPortfolioProductDocumentService(null, null) {
+ @Override
+ public Mono linkProductDocuments(ProductPortfolio template, PortfolioProduct product) {
+ if (product == null) {
+ return Mono.empty();
+ }
+ return Mono.just(product);
+ }
+ };
+ }
+
+ /**
+ * Links content documents to a portfolio product when missing. Existing links are left unchanged.
+ *
+ * @param template bootstrap product template containing newline-separated content document names
+ * @param product upserted portfolio product
+ * @return the unchanged product, or empty when linking is skipped
+ */
+ public Mono linkProductDocuments(ProductPortfolio template, PortfolioProduct product) {
+ if (product == null) {
+ return Mono.empty();
+ }
+ List documentNames = parseDocumentNames(template != null ? template.getDocument() : null);
+ if (documentNames.isEmpty()) {
+ return Mono.just(product);
+ }
+
+ return Flux.fromIterable(documentNames)
+ .concatMap(this::findDocumentUuidByName)
+ .collectList()
+ .flatMap(documentUuids -> createMissingDocumentLinks(product, documentNames, documentUuids))
+ .onErrorResume(error -> {
+ log.warn(
+ "Continuing without portfolio product document links: productUuid={}, name={}, reason={}",
+ product.getUuid(), product.getName(), error.getMessage());
+ return Mono.just(product);
+ });
+ }
+
+ private Mono createMissingDocumentLinks(PortfolioProduct product, List documentNames,
+ List documentUuids) {
+ List resolvedUuids = documentUuids.stream().filter(Objects::nonNull).toList();
+ if (resolvedUuids.isEmpty()) {
+ return Mono.just(product);
+ }
+
+ return loadExistingLinkedDocumentUuids(product.getUuid())
+ .flatMap(existingLinkedUuids -> {
+ List missingDocumentUuids = resolvedUuids.stream()
+ .filter(documentUuid -> !existingLinkedUuids.contains(documentUuid))
+ .toList();
+
+ if (missingDocumentUuids.isEmpty()) {
+ log.info(
+ "All content documents already linked to portfolio product: productUuid={}, name={}, "
+ + "documentUuids={}",
+ product.getUuid(), product.getName(), resolvedUuids);
+ return Mono.just(product);
+ }
+
+ List linkRequests = missingDocumentUuids.stream()
+ .map(uuid -> new PortfolioProductDocumentLinkRequest().document(uuid))
+ .toList();
+
+ log.info(
+ "Linking {} missing content document(s) to portfolio product: productUuid={}, name={}, "
+ + "documentUuids={}",
+ linkRequests.size(), product.getUuid(), product.getName(), missingDocumentUuids);
+
+ return productsApi.bulkCreatePortfolioProductDocuments(product.getUuid(), linkRequests)
+ .collectList()
+ .doOnSuccess(responses -> log.info(
+ "Linked content documents to portfolio product: productUuid={}, name={}, linkedCount={}",
+ product.getUuid(), product.getName(), responses.size()))
+ .doOnError(error -> log.error(
+ "Failed to link content documents to portfolio product: productUuid={}, name={}, "
+ + "documentNames={}",
+ product.getUuid(), product.getName(), documentNames, error))
+ .thenReturn(product);
+ });
+ }
+
+ private Mono> loadExistingLinkedDocumentUuids(UUID productUuid) {
+ return loadExistingLinkedDocumentUuids(productUuid, 0, new HashSet<>());
+ }
+
+ private Mono> loadExistingLinkedDocumentUuids(UUID productUuid, int offset, Set accumulated) {
+ return productsApi.listPortfolioProductDocuments(productUuid, CONTENT_RETRIEVE_LIMIT, offset)
+ .flatMap(page -> {
+ if (page.getResults() != null) {
+ page.getResults().stream()
+ .filter(Objects::nonNull)
+ .map(PortfolioProductDocumentResponse::getUuid)
+ .forEach(accumulated::add);
+ }
+ if (page.getNext() == null) {
+ return Mono.just(accumulated);
+ }
+ return loadExistingLinkedDocumentUuids(productUuid, offset + CONTENT_RETRIEVE_LIMIT, accumulated);
+ })
+ .defaultIfEmpty(accumulated);
+ }
+
+ private List parseDocumentNames(String documentField) {
+ if (!StringUtils.hasText(documentField)) {
+ return List.of();
+ }
+ return Arrays.stream(documentField.split("\\R"))
+ .map(String::trim)
+ .filter(StringUtils::hasText)
+ .toList();
+ }
+
+ private Mono findDocumentUuidByName(String documentName) {
+ return Mono.fromCallable(() -> lookupDocumentUuidByName(documentName))
+ .flatMap(uuid -> uuid != null ? Mono.just(uuid) : Mono.empty());
+ }
+
+ private UUID lookupDocumentUuidByName(String documentName) {
+ PaginatedOASDocumentResponseList page = contentApi.listContentDocuments(
+ null, CONTENT_RETRIEVE_LIMIT, documentName, 0, null, null);
+ if (page == null || page.getResults() == null || page.getResults().isEmpty()) {
+ log.warn("Content document not found by name: name={}", documentName);
+ return null;
+ }
+
+ return page.getResults().stream()
+ .filter(Objects::nonNull)
+ .filter(document -> documentName.equals(document.getName()))
+ .map(OASDocumentResponse::getUuid)
+ .findFirst()
+ .orElseGet(() -> {
+ log.warn("Content document name query returned no exact match: name={}", documentName);
+ return null;
+ });
+ }
+
+}
diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductService.java
index ef423af57..4f1de596d 100644
--- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductService.java
+++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentPortfolioProductService.java
@@ -58,6 +58,7 @@ public class InvestmentPortfolioProductService {
private final IngestConfigProperties config;
private final InvestmentModelPortfolioService modelPortfolioService;
private final InvestmentRestProductPortfolioService investmentRestProductPortfolioService;
+ private final InvestmentPortfolioProductDocumentService investmentPortfolioProductDocumentService;
private final RestTemplateModelPortfolioMapper modelPortfolioMapper =
Mappers.getMapper(RestTemplateModelPortfolioMapper.class);
@@ -88,6 +89,7 @@ public Mono> upsertInvestmentProducts(InvestmentData inve
.flatMap(p -> listExistingPortfolioProducts(p)
.flatMap(existingProduct -> updateExistingPortfolioProduct(existingProduct, p, investmentData))
.switchIfEmpty(Mono.defer(() -> createPortfolioProductWithModel(p, investmentData)))
+ .flatMap(product -> investmentPortfolioProductDocumentService.linkProductDocuments(p, product))
.doOnSuccess(product -> log.info(
"Successfully upserted portfolio product: uuid={}, name={}, engine={}, productType={}, model={}",
product.getUuid(), product.getName(), product.getAdviceEngine(), product.getProductType(),
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentServiceTest.java
new file mode 100644
index 000000000..8458164d9
--- /dev/null
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductDocumentServiceTest.java
@@ -0,0 +1,261 @@
+package com.backbase.stream.investment.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.backbase.investment.api.service.sync.v1.ContentApi;
+import com.backbase.investment.api.service.sync.v1.model.OASDocumentResponse;
+import com.backbase.investment.api.service.sync.v1.model.PaginatedOASDocumentResponseList;
+import com.backbase.investment.api.service.v1.InvestmentProductsApi;
+import com.backbase.investment.api.service.v1.model.PaginatedPortfolioProductDocumentList;
+import com.backbase.investment.api.service.v1.model.PortfolioProduct;
+import com.backbase.investment.api.service.v1.model.PortfolioProductDocumentLinkRequest;
+import com.backbase.investment.api.service.v1.model.PortfolioProductDocumentResponse;
+import com.backbase.investment.api.service.v1.model.ProductTypeEnum;
+import com.backbase.stream.investment.ProductPortfolio;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.AfterEach;
+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.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+class InvestmentPortfolioProductDocumentServiceTest {
+
+ private static final int CONTENT_RETRIEVE_LIMIT = 100;
+
+ @Mock
+ private InvestmentProductsApi productsApi;
+
+ @Mock
+ private ContentApi contentApi;
+
+ private InvestmentPortfolioProductDocumentService service;
+
+ private AutoCloseable mocks;
+
+ @BeforeEach
+ void setUp() {
+ mocks = MockitoAnnotations.openMocks(this);
+ service = new InvestmentPortfolioProductDocumentService(productsApi, contentApi);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ mocks.close();
+ }
+
+ @Nested
+ @DisplayName("linkProductDocuments")
+ class LinkProductDocumentsTests {
+
+ @Test
+ @DisplayName("no document names — skips linking")
+ void noDocumentNames_skipsLinking() {
+ ProductPortfolio template = new ProductPortfolio();
+ template.setName("Self Trading");
+ PortfolioProduct product = buildProduct(UUID.randomUUID(), "Self Trading");
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ verify(contentApi, never()).listContentDocuments(any(), any(), any(), any(), any(), any());
+ verify(productsApi, never()).bulkCreatePortfolioProductDocuments(any(), any());
+ }
+
+ @Test
+ @DisplayName("missing links — bulk creates only unresolved document links")
+ void missingLinks_bulkCreatesDocuments() {
+ UUID productUuid = UUID.randomUUID();
+ UUID kiidUuid = UUID.randomUUID();
+ UUID factsheetUuid = UUID.randomUUID();
+ String kiidName = "Key investor information document - Opportunity Horizon Fund";
+ String factsheetName = "Factsheet - Opportunity Horizon Fund";
+
+ ProductPortfolio template = new ProductPortfolio();
+ template.setName("Opportunity Horizon Fund");
+ template.setDocument(kiidName + "\n" + factsheetName);
+
+ PortfolioProduct product = buildProduct(productUuid, "Opportunity Horizon Fund");
+
+ stubContentDocumentLookup(kiidName, kiidUuid);
+ stubContentDocumentLookup(factsheetName, factsheetUuid);
+ when(productsApi.listPortfolioProductDocuments(eq(productUuid), eq(CONTENT_RETRIEVE_LIMIT), eq(0)))
+ .thenReturn(Mono.just(new PaginatedPortfolioProductDocumentList().count(0).results(List.of())));
+
+ when(productsApi.bulkCreatePortfolioProductDocuments(eq(productUuid), any()))
+ .thenReturn(Flux.just(
+ new PortfolioProductDocumentResponse(kiidUuid),
+ new PortfolioProductDocumentResponse(factsheetUuid)));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ verify(productsApi).bulkCreatePortfolioProductDocuments(eq(productUuid), captor.capture());
+ assertThat(captor.getValue()).extracting(PortfolioProductDocumentLinkRequest::getDocument)
+ .containsExactly(kiidUuid, factsheetUuid);
+ }
+
+ @Test
+ @DisplayName("all links already exist — skips bulk create")
+ void allLinksExist_skipsBulkCreate() {
+ UUID productUuid = UUID.randomUUID();
+ UUID kiidUuid = UUID.randomUUID();
+ UUID factsheetUuid = UUID.randomUUID();
+ String kiidName = "Key investor information document - Opportunity Horizon Fund";
+ String factsheetName = "Factsheet - Opportunity Horizon Fund";
+
+ ProductPortfolio template = new ProductPortfolio();
+ template.setDocument(kiidName + "\n" + factsheetName);
+ PortfolioProduct product = buildProduct(productUuid, "Opportunity Horizon Fund");
+
+ stubContentDocumentLookup(kiidName, kiidUuid);
+ stubContentDocumentLookup(factsheetName, factsheetUuid);
+ when(productsApi.listPortfolioProductDocuments(eq(productUuid), eq(CONTENT_RETRIEVE_LIMIT), eq(0)))
+ .thenReturn(Mono.just(new PaginatedPortfolioProductDocumentList()
+ .count(2)
+ .results(List.of(
+ new PortfolioProductDocumentResponse(kiidUuid),
+ new PortfolioProductDocumentResponse(factsheetUuid)))));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ verify(productsApi, never()).bulkCreatePortfolioProductDocuments(any(), any());
+ }
+
+ @Test
+ @DisplayName("partial existing links — bulk creates only missing documents")
+ void partialExistingLinks_bulkCreatesMissingOnly() {
+ UUID productUuid = UUID.randomUUID();
+ UUID kiidUuid = UUID.randomUUID();
+ UUID factsheetUuid = UUID.randomUUID();
+ String kiidName = "Key investor information document - Opportunity Horizon Fund";
+ String factsheetName = "Factsheet - Opportunity Horizon Fund";
+
+ ProductPortfolio template = new ProductPortfolio();
+ template.setDocument(kiidName + "\n" + factsheetName);
+ PortfolioProduct product = buildProduct(productUuid, "Opportunity Horizon Fund");
+
+ stubContentDocumentLookup(kiidName, kiidUuid);
+ stubContentDocumentLookup(factsheetName, factsheetUuid);
+ when(productsApi.listPortfolioProductDocuments(eq(productUuid), eq(CONTENT_RETRIEVE_LIMIT), eq(0)))
+ .thenReturn(Mono.just(new PaginatedPortfolioProductDocumentList()
+ .count(1)
+ .results(List.of(new PortfolioProductDocumentResponse(kiidUuid)))));
+
+ when(productsApi.bulkCreatePortfolioProductDocuments(eq(productUuid), any()))
+ .thenReturn(Flux.just(new PortfolioProductDocumentResponse(factsheetUuid)));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ verify(productsApi).bulkCreatePortfolioProductDocuments(eq(productUuid), captor.capture());
+ assertThat(captor.getValue()).extracting(PortfolioProductDocumentLinkRequest::getDocument)
+ .containsExactly(factsheetUuid);
+ }
+
+ @Test
+ @DisplayName("unresolved document name — skips bulk create")
+ void unresolvedDocumentName_skipsBulkCreate() {
+ ProductPortfolio template = new ProductPortfolio();
+ template.setDocument("UNKNOWN DOC");
+ PortfolioProduct product = buildProduct(UUID.randomUUID(), "Opportunity Horizon Fund");
+
+ when(contentApi.listContentDocuments(isNull(), eq(CONTENT_RETRIEVE_LIMIT), eq("UNKNOWN DOC"), eq(0),
+ isNull(), isNull()))
+ .thenReturn(new PaginatedOASDocumentResponseList().count(0).results(List.of()));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ verify(productsApi, never()).listPortfolioProductDocuments(any(), any(), any());
+ verify(productsApi, never()).bulkCreatePortfolioProductDocuments(any(), any());
+ }
+
+ @Test
+ @DisplayName("partial document resolution — bulk creates only resolved documents")
+ void partialDocumentResolution_bulkCreatesResolvedOnly() {
+ UUID productUuid = UUID.randomUUID();
+ UUID kiidUuid = UUID.randomUUID();
+ String kiidName = "Key investor information document - Opportunity Horizon Fund";
+ String factsheetName = "Factsheet - Opportunity Horizon Fund";
+
+ ProductPortfolio template = new ProductPortfolio();
+ template.setDocument(kiidName + "\n" + factsheetName);
+ PortfolioProduct product = buildProduct(productUuid, "Opportunity Horizon Fund");
+
+ stubContentDocumentLookup(kiidName, kiidUuid);
+ when(contentApi.listContentDocuments(isNull(), eq(CONTENT_RETRIEVE_LIMIT), eq(factsheetName), eq(0),
+ isNull(), isNull()))
+ .thenReturn(new PaginatedOASDocumentResponseList().count(0).results(List.of()));
+ when(productsApi.listPortfolioProductDocuments(eq(productUuid), eq(CONTENT_RETRIEVE_LIMIT), eq(0)))
+ .thenReturn(Mono.just(new PaginatedPortfolioProductDocumentList().count(0).results(List.of())));
+ when(productsApi.bulkCreatePortfolioProductDocuments(eq(productUuid), any()))
+ .thenReturn(Flux.just(new PortfolioProductDocumentResponse(kiidUuid)));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ verify(productsApi).bulkCreatePortfolioProductDocuments(eq(productUuid), captor.capture());
+ assertThat(captor.getValue()).extracting(PortfolioProductDocumentLinkRequest::getDocument)
+ .containsExactly(kiidUuid);
+ }
+
+ @Test
+ @DisplayName("bulk create failure — product upsert still completes")
+ void bulkCreateFailure_productStillReturned() {
+ UUID productUuid = UUID.randomUUID();
+ String documentName = "Factsheet - Opportunity Horizon Fund";
+ UUID documentUuid = UUID.randomUUID();
+
+ ProductPortfolio template = new ProductPortfolio();
+ template.setDocument(documentName);
+ PortfolioProduct product = buildProduct(productUuid, "Opportunity Horizon Fund");
+
+ stubContentDocumentLookup(documentName, documentUuid);
+ when(productsApi.listPortfolioProductDocuments(eq(productUuid), eq(CONTENT_RETRIEVE_LIMIT), eq(0)))
+ .thenReturn(Mono.just(new PaginatedPortfolioProductDocumentList().count(0).results(List.of())));
+ when(productsApi.bulkCreatePortfolioProductDocuments(eq(productUuid), any()))
+ .thenReturn(Flux.error(new RuntimeException("link failed")));
+
+ StepVerifier.create(service.linkProductDocuments(template, product))
+ .expectNext(product)
+ .verifyComplete();
+ }
+ }
+
+ private void stubContentDocumentLookup(String documentName, UUID documentUuid) {
+ when(contentApi.listContentDocuments(isNull(), eq(CONTENT_RETRIEVE_LIMIT), eq(documentName), eq(0), isNull(),
+ isNull()))
+ .thenReturn(new PaginatedOASDocumentResponseList()
+ .count(1)
+ .results(List.of(new OASDocumentResponse(documentUuid).name(documentName))));
+ }
+
+ private PortfolioProduct buildProduct(UUID uuid, String name) {
+ return new PortfolioProduct(name, null, null, 1, null, "savings-plan", uuid, null, null,
+ ProductTypeEnum.SAVINGS_PLAN);
+ }
+}
diff --git a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductServiceTest.java b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductServiceTest.java
index 11a1a1faa..a27637a65 100644
--- a/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductServiceTest.java
+++ b/stream-investment/investment-core/src/test/java/com/backbase/stream/investment/service/InvestmentPortfolioProductServiceTest.java
@@ -54,6 +54,9 @@ class InvestmentPortfolioProductServiceTest {
@Mock
private InvestmentRestProductPortfolioService investmentRestProductPortfolioService;
+ @Mock
+ private InvestmentPortfolioProductDocumentService investmentPortfolioProductDocumentService;
+
private final IngestConfigProperties ingestConfigProperties = new IngestConfigProperties();
private InvestmentPortfolioProductService service;
@@ -67,7 +70,10 @@ void setUp() {
productsApi,
ingestConfigProperties,
modelPortfolioService,
- investmentRestProductPortfolioService);
+ investmentRestProductPortfolioService,
+ investmentPortfolioProductDocumentService);
+ when(investmentPortfolioProductDocumentService.linkProductDocuments(any(), any()))
+ .thenAnswer(invocation -> Mono.just(invocation.getArgument(1)));
}
@AfterEach