Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion stream-investment/investment-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<properties>
<checkstyle.disable.checks>true</checkstyle.disable.checks>
<investment-service-api.version>1.6.2</investment-service-api.version>
<investment-service-api.version>1.9.0</investment-service-api.version>
</properties>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ public class ProductPortfolio {
private ProductTypeEnum productType;
@JsonProperty(JSON_PROPERTY_EXTRA_DATA)
private Map<String, String> extraData = new HashMap<>();
private String document;

}
Original file line number Diff line number Diff line change
@@ -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<PortfolioProduct> 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<PortfolioProduct> linkProductDocuments(ProductPortfolio template, PortfolioProduct product) {
if (product == null) {
return Mono.empty();
}
List<String> 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<PortfolioProduct> createMissingDocumentLinks(PortfolioProduct product, List<String> documentNames,
List<UUID> documentUuids) {
List<UUID> resolvedUuids = documentUuids.stream().filter(Objects::nonNull).toList();
if (resolvedUuids.isEmpty()) {
return Mono.just(product);
}

return loadExistingLinkedDocumentUuids(product.getUuid())
.flatMap(existingLinkedUuids -> {
List<UUID> 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<PortfolioProductDocumentLinkRequest> 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<Set<UUID>> loadExistingLinkedDocumentUuids(UUID productUuid) {
return loadExistingLinkedDocumentUuids(productUuid, 0, new HashSet<>());
}

private Mono<Set<UUID>> loadExistingLinkedDocumentUuids(UUID productUuid, int offset, Set<UUID> 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<String> 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<UUID> 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;
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -88,6 +89,7 @@ public Mono<List<PortfolioProduct>> 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(),
Expand Down
Loading
Loading