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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [10.16.0]
### Changed
- Add externalId to the model portfolio, documents, news entries in investment

## [10.15.0]
### Changed
- Fix for Investment Portfolio products create failing due to uniques validation
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.16.0</investment-service-api.version>
<investment-service-api.version>1.16.2</investment-service-api.version>
</properties>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.backbase.stream.investment;

import com.backbase.investment.api.service.v1.model.ProductTypeEnum;
import com.fasterxml.jackson.annotation.JsonAlias;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand All @@ -12,11 +13,16 @@
public class ModelPortfolio {

private UUID uuid;
@JsonAlias("external_id")
private String externalId;
private String arrangementExternalId;
private ProductTypeEnum productTypeEnum;
private String name;
@JsonAlias("cash_weight")
private double cashWeight;
@JsonAlias("risk_level")
private int riskLevel;
@JsonAlias("allocation")
private List<Allocation> allocations;
private UUID createdFor;
private Map<String, String> extraData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import static com.backbase.investment.api.service.v1.model.PortfolioProduct.JSON_PROPERTY_PRODUCT_CATEGORY;
import static com.backbase.investment.api.service.v1.model.PortfolioProduct.JSON_PROPERTY_PRODUCT_TYPE;

import com.backbase.investment.api.service.v1.model.InvestorModelPortfolio;
import com.backbase.investment.api.service.v1.model.PortfolioProductBadge;
import com.backbase.investment.api.service.v1.model.PortfolioProductStatusEnum;
import com.backbase.investment.api.service.v1.model.ProductTypeEnum;
Expand Down Expand Up @@ -46,7 +45,7 @@ public class ProductPortfolio {
@JsonProperty(JSON_PROPERTY_ADVICE_ENGINE)
private String adviceEngine;
@JsonProperty(JSON_PROPERTY_MODEL_PORTFOLIO)
private InvestorModelPortfolio modelPortfolio;
private ModelPortfolio modelPortfolio;
@JsonProperty(JSON_PROPERTY_PRODUCT_TYPE)
private ProductTypeEnum productType;
@JsonProperty(JSON_PROPERTY_EXTRA_DATA)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
@Builder
public class ContentDocumentEntry {

@JsonProperty("external_id")
private String externalId;
private String name;
private String description;
private List<String> tags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
@AllArgsConstructor
public class MarketNewsEntry {

@JsonProperty("external_id")
private String externalId;
private String title;
private String excerpt;
private String body;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -52,22 +53,22 @@ public class InvestmentModelPortfolioService {

public Flux<OASModelPortfolioResponse> upsertModels(InvestmentData investmentData) {
return Flux.fromIterable(Objects.requireNonNullElse(investmentData.getModelPortfolios(), List.of()))
.flatMap(modelPortfolioTemplate -> upsertModelPortfolio(modelPortfolioTemplate)
.doOnSuccess(modelPortfolio -> modelPortfolioTemplate.uuid(modelPortfolio.getUuid())));
.flatMap(modelPortfolioTemplate -> upsertModelPortfolioResponse(modelPortfolioTemplate)
.doOnSuccess(response -> modelPortfolioTemplate.uuid(response.getUuid())));
}

public Mono<ModelPortfolio> upsertModelPortfolio(InvestorModelPortfolio modelPortfolio) {
public Mono<ModelPortfolio> upsertModelPortfolio(ModelPortfolio modelPortfolio) {
Objects.requireNonNull(modelPortfolio, "ModelPortfolio must not be null");

ModelPortfolio map = modelPortfolioMapper.map(modelPortfolio);
return upsertModelPortfolio(map)
.map(mp -> {
map.uuid(mp.getUuid());
return map;
return upsertModelPortfolioResponse(modelPortfolio)
.map(response -> {
modelPortfolio.uuid(response.getUuid());
return modelPortfolio;
})
.onErrorResume(WebClientResponseException.class, ex -> {
log.warn("Continuing without portfolio model: name={}, riskLevel={}, status={}",
map.getName(), map.getRiskLevel(), ex.getStatusCode());
log.warn("Continuing without portfolio model: externalId={}, name={}, riskLevel={}, status={}",
modelPortfolio.getExternalId(), modelPortfolio.getName(), modelPortfolio.getRiskLevel(),
ex.getStatusCode());
return Mono.empty();
});
}
Expand All @@ -77,7 +78,7 @@ public Mono<ModelPortfolio> upsertModelPortfolio(InvestorModelPortfolio modelPor
*
* <p>This method implements an upsert pattern:
* <ol>
* <li>Searches for existing model portfolios by name and risk level</li>
* <li>Searches for existing model portfolios by {@code external_id}, then by name</li>
* <li>If found, patches the existing model portfolio</li>
* <li>If not found, creates a new model portfolio</li>
* </ol>
Expand All @@ -86,30 +87,77 @@ public Mono<ModelPortfolio> upsertModelPortfolio(InvestorModelPortfolio modelPor
* @return Mono emitting the created or updated model portfolio response
* @throws NullPointerException if modelPortfolio is null
*/
private Mono<OASModelPortfolioResponse> upsertModelPortfolio(ModelPortfolio modelPortfolio) {
private Mono<OASModelPortfolioResponse> upsertModelPortfolioResponse(ModelPortfolio modelPortfolio) {
Objects.requireNonNull(modelPortfolio, "ModelPortfolio must not be null");

String modelName = modelPortfolio.getName();
Integer riskLevel = modelPortfolio.getRiskLevel();

log.debug("Upserting model portfolio: name={}, riskLevel={}", modelName, riskLevel);
log.debug("Upserting model portfolio: externalId={}, name={}, riskLevel={}",
modelPortfolio.getExternalId(), modelName, riskLevel);

return listExistingModelPortfolios(modelName, riskLevel)
.flatMap(pm -> {
if (isTargetAssetWeightCorrect(pm)) {
return findExistingModelPortfolio(modelPortfolio)
.flatMap(existing -> {
if (existing.expanded() != null && isTargetAssetWeightCorrect(existing.expanded())) {
InvestorModelPortfolio pm = existing.expanded();
modelPortfolio.setAllocations(modelPortfolioMapper.mapAssetModel(pm.getAllocation()));
modelPortfolio.setCashWeight(pm.getCashWeight());
} else {
} else if (existing.expanded() != null) {
log.error(
"Stored model target asset weight and cash weight are incorrect for uuid={}, name={}, riskLevel={}",
pm.getUuid(), pm.getName(), pm.getRiskLevel());
existing.uuid(), modelName, riskLevel);
}
return patchModelPortfolio(pm.getUuid(), modelPortfolio);
return patchModelPortfolio(existing.uuid(), modelPortfolio);
})
.switchIfEmpty(Mono.defer(() -> createNewModelPortfolio(modelPortfolio)))
.doOnSuccess(upserted -> log.info(
"Successfully upserted model portfolio: uuid={}, name={}, riskLevel={}",
upserted.getUuid(), upserted.getName(), upserted.getRiskLevel()));
"Successfully upserted model portfolio: uuid={}, externalId={}, name={}, riskLevel={}",
upserted.getUuid(), upserted.getExternalId(), upserted.getName(), upserted.getRiskLevel()));
}

private record ExistingModelRef(UUID uuid, InvestorModelPortfolio expanded) {
}

private Mono<ExistingModelRef> findExistingModelPortfolio(ModelPortfolio modelPortfolio) {
if (StringUtils.hasText(modelPortfolio.getExternalId())) {
return findExistingByExternalId(modelPortfolio.getExternalId())
.switchIfEmpty(Mono.defer(() -> listExistingModelPortfolios(modelPortfolio.getName(),
modelPortfolio.getRiskLevel())
.map(inv -> new ExistingModelRef(inv.getUuid(), inv))));
}
return listExistingModelPortfolios(modelPortfolio.getName(), modelPortfolio.getRiskLevel())
.map(inv -> new ExistingModelRef(inv.getUuid(), inv));
}

private Mono<ExistingModelRef> findExistingByExternalId(String externalId) {
// Do not expand allocation.asset here: expanded assets are objects in JSON but the
// generated OAS list type expects UUID references, which causes DecodingException.
return financialAdviceApi.listModelPortfolio(
null, null, null,
config.getPortfolio().getListModelPageSize(), null, null, null, null, null, null)
.doOnError(throwable -> log.error("Failed to list model portfolios for externalId={}", externalId,
throwable))
.onErrorResume(throwable -> {
log.warn("Falling back to name-based model lookup after list-by-external-id failed: externalId={}",
externalId);
return Mono.empty();
})
.flatMap(page -> {
List<OASModelPortfolioResponse> results = page != null
? Objects.requireNonNullElse(page.getResults(), List.of())
: List.of();
Optional<OASModelPortfolioResponse> match = results.stream()
.filter(model -> externalId.equals(model.getExternalId()))
.findFirst();
if (match.isEmpty()) {
log.info("No existing model portfolio found for externalId={}", externalId);
return Mono.empty();
}
OASModelPortfolioResponse model = match.get();
log.info("Found existing model portfolio by externalId: externalId={}, uuid={}, name={}",
externalId, model.getUuid(), model.getName());
return Mono.just(new ExistingModelRef(model.getUuid(), null));
});
}

private boolean isTargetAssetWeightCorrect(InvestorModelPortfolio pm) {
Expand Down Expand Up @@ -163,8 +211,8 @@ private Mono<InvestorModelPortfolio> listExistingModelPortfolios(String name, In
* @return Mono emitting the newly created model portfolio
*/
private Mono<OASModelPortfolioResponse> createNewModelPortfolio(ModelPortfolio modelPortfolio) {
log.info("Creating new model portfolio: name={}, riskLevel={}",
modelPortfolio.getName(), modelPortfolio.getRiskLevel());
log.info("Creating new model portfolio: externalId={}, name={}, riskLevel={}",
modelPortfolio.getExternalId(), modelPortfolio.getName(), modelPortfolio.getRiskLevel());
return investmentRestModelPortfolioService.createModelPortfolio(modelPortfolio)
.doOnError(throwable -> logModelPortfolioError("create",
modelPortfolio.getName(), modelPortfolio.getRiskLevel(), throwable));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.backbase.stream.investment.service;

import com.backbase.investment.api.service.v1.InvestmentProductsApi;
import com.backbase.investment.api.service.v1.model.InvestorModelPortfolio;
import com.backbase.investment.api.service.v1.model.PaginatedPortfolioProductList;
import com.backbase.investment.api.service.v1.model.PortfolioProduct;
import com.backbase.investment.api.service.v1.model.ProductTypeEnum;
Expand All @@ -11,7 +10,6 @@
import com.backbase.stream.investment.ModelPortfolio;
import com.backbase.stream.investment.ProductPortfolio;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestProductPortfolioService;
import com.backbase.stream.investment.service.resttemplate.RestTemplateModelPortfolioMapper;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
Expand All @@ -21,7 +19,6 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
Expand Down Expand Up @@ -56,8 +53,6 @@ public class InvestmentPortfolioProductService {
private final InvestmentModelPortfolioService modelPortfolioService;
private final InvestmentRestProductPortfolioService investmentRestProductPortfolioService;
private final InvestmentPortfolioProductDocumentService investmentPortfolioProductDocumentService;
private final RestTemplateModelPortfolioMapper modelPortfolioMapper =
Mappers.getMapper(RestTemplateModelPortfolioMapper.class);

public InvestmentPortfolioProductService(InvestmentProductsApi productsApi, IngestConfigProperties config,
InvestmentModelPortfolioService modelPortfolioService,
Expand Down Expand Up @@ -102,7 +97,7 @@ public Mono<List<PortfolioProduct>> upsertInvestmentProducts(InvestmentData inve
"Successfully upserted portfolio product: uuid={}, name={}, engine={}, productType={}, model={}",
product.getUuid(), product.getName(), product.getAdviceEngine(), product.getProductType(),
Optional.ofNullable(product.getModelPortfolio())
.map(InvestorModelPortfolio::getName).orElse("")))
.map(mp -> mp.getName()).orElse("")))
.doOnError(throwable -> log.error("Failed to upsert portfolio product: name={}, productType={}",
p.getName(), p.getProductType(), throwable))
.onErrorResume(throwable -> {
Expand Down Expand Up @@ -133,8 +128,7 @@ private Mono<List<ProductPortfolio>> enrichPortfolioProductsWithModels(Investmen
Mono<ModelPortfolio> modelPortfolio = upsertPortfolioModel(pp);
return modelPortfolio
.map(mp -> {
InvestorModelPortfolio mappedModelPortfolio = modelPortfolioMapper.map(mp);
pp.setModelPortfolio(mappedModelPortfolio);
pp.setModelPortfolio(mp);
return pp;
})
.switchIfEmpty(Mono.just(pp));
Expand All @@ -143,7 +137,7 @@ private Mono<List<ProductPortfolio>> enrichPortfolioProductsWithModels(Investmen
}

private Mono<ModelPortfolio> upsertPortfolioModel(ProductPortfolio pp) {
InvestorModelPortfolio modelPortfolio = pp.getModelPortfolio();
ModelPortfolio modelPortfolio = pp.getModelPortfolio();
if (modelPortfolio == null) {
return Mono.empty();
}
Expand All @@ -159,7 +153,7 @@ private void assignProductsToArrangements(List<PortfolioProduct> products,
+ "productType={}, model={}, arrangementName={}",
product.getUuid(), product.getName(), product.getAdviceEngine(), product.getProductType(),
Optional.ofNullable(product.getModelPortfolio())
.map(InvestorModelPortfolio::getName).orElse(""),
.map(mp -> mp.getName()).orElse(""),
arrangement.getName());
arrangement.setInvestmentProductId(product.getUuid());
}));
Expand Down Expand Up @@ -189,16 +183,27 @@ private Optional<PortfolioProduct> findMatchingProduct(List<PortfolioProduct> pr
return typeProducts.stream().min(BY_ORDER);
}

/**
* De-duplicates ingest templates before upsert. Uses {@code external_id} when present so rows with the same
* display name but different stable ids are all processed; otherwise de-duplicates by name (last wins).
*/
private Collection<ProductPortfolio> distinctProducts(List<ProductPortfolio> products) {
return products.stream()
.collect(Collectors.toMap(
ProductPortfolio::getName,
this::distinctProductKey,
pp -> pp,
(existing, replacement) -> replacement
))
.values();
}

private String distinctProductKey(ProductPortfolio portfolioProduct) {
if (StringUtils.hasText(portfolioProduct.getExternalId())) {
return portfolioProduct.getExternalId();
}
return portfolioProduct.getName();
}

private Mono<PortfolioProduct> listExistingPortfolioProducts(ProductPortfolio portfolioProduct) {
ProductTypeEnum productType = portfolioProduct.getProductType();

Expand Down Expand Up @@ -226,7 +231,7 @@ private Mono<PortfolioProduct> listExistingPortfolioProducts(ProductPortfolio po

private Mono<PortfolioProduct> listExistingPortfolioProductsByName(ProductPortfolio portfolioProduct) {
Integer riskLevel = Optional.ofNullable(portfolioProduct.getModelPortfolio())
.map(InvestorModelPortfolio::getRiskLevel).orElse(null);
.map(ModelPortfolio::getRiskLevel).orElse(null);

ProductTypeEnum productType = portfolioProduct.getProductType();
String productCategory = portfolioProduct.getProductCategory();
Expand Down Expand Up @@ -329,7 +334,7 @@ private Mono<PortfolioProduct> createPortfolioProductWithModel(ProductPortfolio

String productType = portfolioProduct.getProductType().getValue();
UUID modelPortfolioUuid = Optional.ofNullable(portfolioProduct.getModelPortfolio())
.map(InvestorModelPortfolio::getUuid).orElse(null);
.map(ModelPortfolio::getUuid).orElse(null);
log.info("Creating portfolio product: name={}, productType={}, modelPortfolioUuid={}",
portfolioProduct.getName(), productType, modelPortfolioUuid);

Expand Down
Loading
Loading