From e19d6dd4e85130178202d69f446250f7b510ec49 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Wed, 28 Jan 2026 11:31:10 +0200 Subject: [PATCH 1/7] generate used API's --- .../INVESTMENT_API_ENDPOINTS_USED.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 stream-investment/INVESTMENT_API_ENDPOINTS_USED.md diff --git a/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md b/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md new file mode 100644 index 000000000..76e1ac6c9 --- /dev/null +++ b/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md @@ -0,0 +1,156 @@ +# Investment Package - Used API Endpoints + +This document lists all Investment Service API endpoints that are actively used in the `investment-core` package. + +## Asset Universe API (AssetUniverseApi) + +### Markets +- `GET /service-api/v2/asset/markets/{code}` - Get market by code +- `POST /service-api/v2/asset/markets` - Create market +- `PUT /service-api/v2/asset/markets/{code}` - Update market + +### Assets +- `GET /service-api/v2/asset/assets/{assetIdentifier}` - Get asset by identifier (ISIN_Market_Currency) +- `POST /service-api/v2/asset/assets` - Create asset (via CustomIntegrationApiService) +- `PATCH /service-api/v2/asset/assets/{uuid}` - Patch asset (via InvestmentRestAssetUniverseService.patchAsset - for logo upload) +- `GET /service-api/v2/asset/assets` - List assets with response spec (for intraday prices) + +### Asset Categories +- `GET /service-api/v2/asset/asset-categories` - List asset categories +- `POST /service-api/v2/asset/asset-categories` - Create asset category +- `PUT /service-api/v2/asset/asset-categories/{uuid}` - Update asset category +- `PATCH /service-api/v2/asset/asset-categories/{uuid}/` - Partial update asset category (for image upload) + +### Asset Category Types +- `GET /service-api/v2/asset/asset-category-types` - List asset category types +- `POST /service-api/v2/asset/asset-category-types` - Create asset category type +- `PUT /service-api/v2/asset/asset-category-types/{uuid}` - Update asset category type + +### Market Special Days +- `GET /service-api/v2/asset/market-special-days` - List market special days +- `POST /service-api/v2/asset/market-special-days` - Create market special day +- `PUT /service-api/v2/asset/market-special-days/{uuid}` - Update market special day + +### Asset Prices +- `GET /service-api/v2/asset/assets/{assetIdentifier}/prices/close` - List asset close prices +- `POST /service-api/v2/asset/assets/{assetIdentifier}/prices/close` - Create asset close prices (batch) +- `POST /service-api/v2/asset/assets/{assetIdentifier}/prices/intraday` - Create intraday asset prices + +## Client API (ClientApi) + +- `POST /service-api/v2/client/clients` - Create client +- `GET /service-api/v2/client/clients` - List clients (with filters: internalUserId) +- `GET /service-api/v2/client/clients/{uuid}` - Get client by UUID +- `PATCH /service-api/v2/client/clients/{uuid}` - Patch client (partial update) +- `PUT /service-api/v2/client/clients/{uuid}` - Update client (full update) + +## Investment Products API (InvestmentProductsApi) + +- `GET /service-api/v2/investment-product/portfolio-products` - List portfolio products +- `POST /service-api/v2/investment-product/portfolio-products` - Create portfolio product +- `PATCH /service-api/v2/investment-product/portfolio-products/{uuid}` - Patch portfolio product + +## Portfolio API (PortfolioApi) + +- `GET /service-api/v2/portfolio/portfolios` - List portfolios (with filters: externalId) +- `POST /service-api/v2/portfolio/portfolios` - Create portfolio +- `PATCH /service-api/v2/portfolio/portfolios/{uuid}` - Patch portfolio + +## Financial Advice API (FinancialAdviceApi) + +### Model Portfolios +- `GET /service-api/v2/financial-advice/model-portfolios` - List model portfolios (with filters: name, riskLevel) +- `POST /service-api/v2/financial-advice/model-portfolios` - Create model portfolio (via CustomIntegrationApiService) +- `PATCH /service-api/v2/financial-advice/model-portfolios/{uuid}` - Patch model portfolio (via CustomIntegrationApiService) + +## Allocations API (AllocationsApi) + +- `GET /service-api/v2/portfolio/portfolios/{portfolioUuid}/allocations` - List portfolio allocations +- `DELETE /service-api/v2/portfolio/portfolios/{portfolioUuid}/allocations/{valuationDate}` - Delete portfolio allocation by valuation date +- `POST /service-api/v2/portfolio/portfolios/{portfolioUuid}/allocations` - Create portfolio allocation (via CustomIntegrationApiService) + +## Payments API (PaymentsApi) + +- `GET /service-api/v2/payment/deposits` - List deposits (with filters: portfolio UUID) +- `POST /service-api/v2/payment/deposits` - Create deposit + +## Investment API (InvestmentApi) + +- `GET /service-api/v2/investment/orders` - List orders (with filters: assetKey) +- `POST /service-api/v2/investment/orders` - Create order + +## Content API (ContentApi) - News/Content Management + +- `GET /service-api/v2/content/entries` - List content entries +- `POST /service-api/v2/content/entries` - Create content entry +- `PATCH /service-api/v2/content/entries/{uuid}/` - Patch content entry (for thumbnail upload) + +## Async Bulk Groups API (AsyncBulkGroupsApi) + +- `GET /service-api/v2/async/bulk-groups/{uuid}` - Get bulk group status + +--- + +## Summary by Service + +### InvestmentAssetUniverseService +- Uses: AssetUniverseApi (markets, assets, categories, category types, special days, prices) +- Uses: InvestmentRestAssetUniverseService (multipart uploads for logos) +- Uses: CustomIntegrationApiService (asset creation with custom logic) + +### InvestmentClientService +- Uses: ClientApi (create, list, get, patch, update clients) + +### InvestmentPortfolioService +- Uses: InvestmentProductsApi (portfolio products) +- Uses: PortfolioApi (portfolios) +- Uses: PaymentsApi (deposits) + +### InvestmentModelPortfolioService +- Uses: FinancialAdviceApi (model portfolios) +- Uses: CustomIntegrationApiService (model portfolio creation/patching) + +### InvestmentPortfolioAllocationService +- Uses: AllocationsApi (portfolio allocations) +- Uses: InvestmentApi (orders) +- Uses: AssetUniverseApi (asset prices) + +### InvestmentAssetPriceService +- Uses: AssetUniverseApi (asset close prices) + +### InvestmentIntradayAssetPriceService +- Uses: AssetUniverseApi (list assets, intraday prices) + +### InvestmentRestNewsContentService +- Uses: ContentApi (news/content entries) + +### AsyncTaskService +- Uses: AsyncBulkGroupsApi (bulk operation status) + +### WorkDayService +- No external API calls (utility service) + +--- + +## Notes + +1. **Custom Integration API Service**: This is a custom wrapper service (marked as deprecated since 8.6.0) that handles multipart/form-data requests for: + - Asset creation (POST /service-api/v2/asset/assets) + - Model portfolio creation/patching with image uploads + - Portfolio allocation creation + +2. **REST Template Services**: Two services handle multipart uploads that generated API clients can't handle properly: + - `InvestmentRestAssetUniverseService` - For asset and asset category logo uploads + - `InvestmentRestNewsContentService` - For content entry thumbnail uploads + +3. **Upsert Pattern**: Most services implement an upsert pattern: + - Try to GET/LIST existing entity + - If found (or 200 OK), PATCH/PUT to update + - If not found (404), POST to create + +4. **Endpoint Patterns**: + - All endpoints follow the pattern: `/service-api/v2/{domain}/{resource}` + - Multipart upload endpoints use PATCH with `multipart/form-data` content type + - Most list endpoints support pagination and filtering + +5. **Error Handling**: All services handle WebClientResponseException with special logic for 404 (Not Found) responses to implement upsert patterns. From c20cb7fbacffa8a3d9fef3fa3492e71844c08f7c Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Wed, 28 Jan 2026 11:59:31 +0200 Subject: [PATCH 2/7] update used API's --- .../INVESTMENT_API_ENDPOINTS_USED.md | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md b/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md index 76e1ac6c9..92de2d673 100644 --- a/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md +++ b/stream-investment/INVESTMENT_API_ENDPOINTS_USED.md @@ -91,6 +91,22 @@ This document lists all Investment Service API endpoints that are actively used --- +## Integration API (CustomIntegrationApiService) + +**Note:** This service is deprecated since 8.6.0 and uses integration-api endpoints instead of service-api. + +### Assets +- `POST /integration-api/v2/asset/assets/` - Create asset (custom implementation) + +### Model Portfolios +- `POST /integration-api/v2/advice-engines/model-portfolio/model_portfolios/` - Create model portfolio (with optional image upload via multipart/form-data) +- `PATCH /integration-api/v2/advice-engines/model-portfolio/model_portfolios/{uuid}/` - Patch model portfolio (with optional image upload via multipart/form-data) + +### Portfolio Allocations +- `POST /integration-api/v2/portfolios/{portfolio_uuid}/allocations/` - Create portfolio allocation + +--- + ## Summary by Service ### InvestmentAssetUniverseService @@ -134,9 +150,9 @@ This document lists all Investment Service API endpoints that are actively used ## Notes -1. **Custom Integration API Service**: This is a custom wrapper service (marked as deprecated since 8.6.0) that handles multipart/form-data requests for: - - Asset creation (POST /service-api/v2/asset/assets) - - Model portfolio creation/patching with image uploads +1. **Custom Integration API Service**: This is a custom wrapper service (marked as deprecated since 8.6.0) that uses `/integration-api/v2/` endpoints instead of `/service-api/v2/` endpoints. It handles: + - Asset creation via POST `/integration-api/v2/asset/assets/` + - Model portfolio creation/patching with optional image uploads via multipart/form-data - Portfolio allocation creation 2. **REST Template Services**: Two services handle multipart uploads that generated API clients can't handle properly: @@ -149,8 +165,39 @@ This document lists all Investment Service API endpoints that are actively used - If not found (404), POST to create 4. **Endpoint Patterns**: - - All endpoints follow the pattern: `/service-api/v2/{domain}/{resource}` - - Multipart upload endpoints use PATCH with `multipart/form-data` content type + - Service API endpoints follow: `/service-api/v2/{domain}/{resource}` + - Integration API endpoints follow: `/integration-api/v2/{domain}/{resource}` (used by CustomIntegrationApiService) + - Multipart upload endpoints use PATCH or POST with `multipart/form-data` content type - Most list endpoints support pagination and filtering 5. **Error Handling**: All services handle WebClientResponseException with special logic for 404 (Not Found) responses to implement upsert patterns. + +--- + +## Total Endpoint Count + +### Service API Endpoints: 42 +- Asset Universe API: 17 endpoints +- Client API: 5 endpoints +- Investment Products API: 3 endpoints +- Portfolio API: 3 endpoints +- Financial Advice API: 3 endpoints +- Allocations API: 3 endpoints +- Content API: 3 endpoints +- Payments API: 2 endpoints +- Investment API: 2 endpoints +- Async Bulk Groups API: 1 endpoint + +### Integration API Endpoints: 4 +- Asset creation: 1 endpoint +- Model portfolios: 2 endpoints +- Portfolio allocations: 1 endpoint + +### Grand Total: 46 unique API endpoints + +By HTTP Method: +- GET: 17 endpoints +- POST: 19 endpoints (15 service-api + 4 integration-api) +- PATCH: 8 endpoints (7 service-api + 1 integration-api) +- PUT: 5 endpoints +- DELETE: 1 endpoint From 8d719461a0fec3bdd832b0984ffb85901b1d6801 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Fri, 30 Jan 2026 13:44:02 +0200 Subject: [PATCH 3/7] NOJIRA: fix file load and request send avoid `File`, use only spring `Resource`; --- .../com/backbase/stream/investment/Asset.java | 5 +- .../InvestmentAssetUniverseService.java | 5 +- .../InvestmentRestAssetUniverseService.java | 82 +++++++++++++++---- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java index 5d9cf597f..8e8ce4d58 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java @@ -3,7 +3,6 @@ import com.backbase.investment.api.service.v1.model.AssetTypeEnum; import com.backbase.investment.api.service.v1.model.StatusA10Enum; import com.fasterxml.jackson.annotation.JsonProperty; -import java.io.File; import java.util.List; import java.util.Map; import java.util.UUID; @@ -11,6 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.core.io.Resource; /** * Lightweight projection of {@link com.backbase.investment.api.service.v1.model.Asset} that keeps the DTO immutable @@ -35,7 +35,8 @@ public class Asset implements AssetKey { private AssetTypeEnum assetType; private List categories; private String externalId; - private File logo; + private String logo; + private Resource logoFile; private String description; private Double defaultPrice; diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java index eddaa88e4..58a195743 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java @@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.mapstruct.factory.Mappers; +import org.springframework.core.io.Resource; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; @@ -90,7 +91,7 @@ public Mono upsertMarket(MarketRequest marketRequest) { * @return Mono representing the existing or newly created asset * @throws IOException if an I/O error occurs */ - public Mono getOrCreateAsset(OASAssetRequestDataRequest assetRequest, File logo) { + public Mono getOrCreateAsset(OASAssetRequestDataRequest assetRequest, Resource logo) { log.debug("Creating asset: {}", assetRequest); // Build a unique asset identifier using ISIN, market, and currency @@ -213,7 +214,7 @@ public Flux createAssets(List { OASAssetRequestDataRequest assetRequest = assetMapper.map(asset, categoryIdByCode); - return this.getOrCreateAsset(assetRequest, asset.getLogo()).map(assetMapper::map); + return this.getOrCreateAsset(assetRequest, asset.getLogoFile()).map(assetMapper::map); }); }); } 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 112c4eecf..396b484b9 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 @@ -3,6 +3,7 @@ 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.OASAssetRequestDataRequest; import com.backbase.investment.api.service.v1.model.Asset; import java.io.File; import java.util.HashMap; @@ -13,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -29,7 +31,7 @@ public class InvestmentRestAssetUniverseService { private final AssetUniverseApi assetUniverseApi; private final ApiClient apiClient; - public Mono setAssetLogo(Asset asset, File logo) { + public Mono setAssetLogo(Asset asset, Resource logo) { String assetUuid = asset.getUuid().toString(); if (logo == null) { @@ -38,22 +40,23 @@ public Mono setAssetLogo(Asset asset, File logo) { } log.info( - "Starting logo attachment for asset: assetUuid={}, assetName='{}', logoFile='{}', logoSize={}", - assetUuid, asset.getName(), logo.getName(), logo.length()); - - return Mono.defer(() -> Mono.just(assetUniverseApi.patchAsset(assetUuid, null, logo))).map(patchedAsset -> { - log.info( - "Logo attached successfully to asset:assetUuid={}, assetName='{}', logoFile='{}'", assetUuid, - asset.getName(), logo.getName()); - return asset; - }).onErrorResume(throwable -> { - log.error( - "Logo attachment failed for asset:assetUuid={}, assetName='{}', logoFile='{}', errorType={}, errorMessage={}", - assetUuid, asset.getName(), logo.getName(), throwable.getClass().getSimpleName(), - throwable.getMessage(), throwable); - log.warn("Asset processing continuing without logo:assetUuid={}", assetUuid); - return Mono.just(asset); - }); + "Starting logo attachment for asset: assetUuid={}, assetName='{}', logoFile='{}'", + assetUuid, asset.getName(), logo.getFilename()); + + return Mono.defer(() -> Mono.just(patchAsset(assetUuid, null, logo))) + .map(patchedAsset -> { + log.info( + "Logo attached successfully to asset:assetUuid={}, assetName='{}', logoFile='{}'", assetUuid, + asset.getName(), logo.getFilename()); + return asset; + }).onErrorResume(throwable -> { + log.error( + "Logo attachment failed for asset:assetUuid={}, assetName='{}', logoFile='{}', errorType={}, errorMessage={}", + assetUuid, asset.getName(), logo.getFilename(), throwable.getClass().getSimpleName(), + throwable.getMessage(), throwable); + log.warn("Asset processing continuing without logo:assetUuid={}", assetUuid); + return Mono.just(asset); + }); } public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { @@ -117,4 +120,49 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { }); } + public com.backbase.investment.api.service.sync.v1.model.Asset patchAsset(String assetIdentifier, + OASAssetRequestDataRequest data, Resource logo) { + Object localVarPostBody = null; + + // verify the required parameter 'assetIdentifier' is set + if (assetIdentifier == null) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, + "Missing the required parameter 'assetIdentifier' when calling patchAsset"); + } + + // create path and map variables + final Map uriVariables = new HashMap(); + uriVariables.put("asset_identifier", assetIdentifier); + + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + if (data != null) { + localVarFormParams.add("data", data); + } + if (logo != null) { + localVarFormParams.add("logo", logo); + } + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "multipart/form-data" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[]{}; + + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { + }; + return apiClient.invokeAPI("/service-api/v2/asset/assets/{asset_identifier}/", HttpMethod.PATCH, uriVariables, + localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, + localVarAccept, localVarContentType, localVarAuthNames, localReturnType) + .getBody(); + } + } \ No newline at end of file From 77d934f6f3ebc7c9733769c591f917ac99a91803 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Mon, 2 Feb 2026 14:30:35 +0200 Subject: [PATCH 4/7] NOJIRA: fix file load and request send avoid `File`, use only spring `Resource`; --- .../InvestmentServiceConfiguration.java | 6 +- .../investment/InvestmentAssetData.java | 6 +- .../investment/InvestmentAssetsTask.java | 5 - .../investment/InvestmentContentData.java | 4 +- .../investment/model/AssetCategoryEntry.java | 26 ++ .../investment/model/MarketNewsEntry.java | 36 +++ .../saga/InvestmentAssetUniverseSaga.java | 1 - .../investment/service/AssetMapper.java | 19 -- .../service/CustomIntegrationApiService.java | 38 --- .../InvestmentAssetUniverseService.java | 179 +++++------ .../service/resttemplate/ContentMapper.java | 14 + .../InvestmentRestAssetUniverseService.java | 295 ++++++++++++++---- .../InvestmentRestNewsContentService.java | 59 ++-- .../resttemplate/RestTemplateAssetMapper.java | 62 ++++ 14 files changed, 504 insertions(+), 246 deletions(-) create mode 100644 stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/AssetCategoryEntry.java create mode 100644 stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/MarketNewsEntry.java create mode 100644 stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/ContentMapper.java create mode 100644 stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/RestTemplateAssetMapper.java 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 d7756091f..60172735b 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 @@ -63,10 +63,8 @@ public InvestmentPortfolioService investmentPortfolioService(PortfolioApi portfo @Bean public InvestmentAssetUniverseService investmentAssetUniverseService(AssetUniverseApi assetUniverseApi, - InvestmentRestAssetUniverseService investmentRestAssetUniverseService, - CustomIntegrationApiService customIntegrationApiService) { - return new InvestmentAssetUniverseService(assetUniverseApi, investmentRestAssetUniverseService, - customIntegrationApiService); + InvestmentRestAssetUniverseService investmentRestAssetUniverseService) { + return new InvestmentAssetUniverseService(assetUniverseApi, investmentRestAssetUniverseService); } @Bean diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetData.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetData.java index 049fc3f60..78a8c2630 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetData.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetData.java @@ -1,11 +1,10 @@ package com.backbase.stream.investment; -import com.backbase.investment.api.service.v1.model.AssetCategory; -import com.backbase.investment.api.service.v1.model.AssetCategoryRequest; import com.backbase.investment.api.service.v1.model.AssetCategoryType; 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.investment.model.AssetCategoryEntry; import java.util.List; import java.util.Map; import java.util.Objects; @@ -24,10 +23,9 @@ public class InvestmentAssetData { private List markets; private List marketSpecialDays; private List assetCategoryTypes; - private List assetCategories; + private List assetCategories; private List assets; private List assetPrices; - private List insertedAssetCategories; private List priceAsyncTasks; private List intradayPriceAsyncTasks; diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetsTask.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetsTask.java index 8c43a913a..2c3151676 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetsTask.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentAssetsTask.java @@ -1,6 +1,5 @@ package com.backbase.stream.investment; -import com.backbase.investment.api.service.v1.model.AssetCategory; import com.backbase.investment.api.service.v1.model.AssetCategoryType; import com.backbase.investment.api.service.v1.model.GroupResult; import com.backbase.investment.api.service.v1.model.Market; @@ -38,10 +37,6 @@ public void setAssetCategoryTypes(List assetCategoryTypes) { data.setAssetCategoryTypes(assetCategoryTypes); } - public void setInsertedAssetCategories(List assetCategories) { - data.setInsertedAssetCategories(assetCategories); - } - public void setAssets(List assets) { data.setAssets(assets); } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentContentData.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentContentData.java index eb05cbfc1..6739a3d31 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentContentData.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/InvestmentContentData.java @@ -1,6 +1,6 @@ package com.backbase.stream.investment; -import com.backbase.investment.api.service.sync.v1.model.EntryCreateUpdateRequest; +import com.backbase.stream.investment.model.MarketNewsEntry; import java.util.List; import lombok.Builder; import lombok.Data; @@ -11,6 +11,6 @@ @Builder public class InvestmentContentData { - private List marketNews; + private List marketNews; } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/AssetCategoryEntry.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/AssetCategoryEntry.java new file mode 100644 index 000000000..3cba0158b --- /dev/null +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/AssetCategoryEntry.java @@ -0,0 +1,26 @@ +package com.backbase.stream.investment.model; + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.core.io.Resource; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AssetCategoryEntry { + + private UUID uuid; + private String name; + private String code; + private Integer order; + private String type; + private String excerpt; + private String description; + private String image; + private Resource imageResource; + +} diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/MarketNewsEntry.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/MarketNewsEntry.java new file mode 100644 index 000000000..155b7903d --- /dev/null +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/model/MarketNewsEntry.java @@ -0,0 +1,36 @@ +package com.backbase.stream.investment.model; + +import com.backbase.stream.investment.ModelAsset; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.core.io.Resource; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MarketNewsEntry { + + private String title; + private String excerpt; + private String body; + @JsonProperty("video_url") + private URI videoUrl; + @JsonProperty("external_url") + private URI externalUrl; + private String thumbnail; + private Resource thumbnailResource; + private List tags = new ArrayList<>(); + @JsonProperty("published_on") + private OffsetDateTime publishedOn; + private Integer order; + private List assets = new ArrayList<>(); + +} diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java index 6ad33ace7..800f5d2ad 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java @@ -255,7 +255,6 @@ public Mono upsertAssetCategories(InvestmentAssetsTask inv .flatMap(assetUniverseService::upsertAssetCategory) .collectList() .map(assetCategories -> { - investmentTask.setInsertedAssetCategories(assetCategories); investmentTask.info(INVESTMENT, OP_CREATE, RESULT_CREATED, investmentTask.getName(), investmentTask.getId(), RESULT_CREATED + " " + assetCategories.size() + " Investment Asset Categories"); diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AssetMapper.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AssetMapper.java index 807e77c6e..5f5da106e 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AssetMapper.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/AssetMapper.java @@ -1,40 +1,21 @@ package com.backbase.stream.investment.service; import com.backbase.investment.api.service.v1.model.AssetCategory; -import com.backbase.investment.api.service.v1.model.OASAssetRequestDataRequest; import com.backbase.stream.investment.Asset; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.UUID; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; import org.mapstruct.Named; @Mapper public interface AssetMapper { - @Mapping(target = "categories", ignore = true) - OASAssetRequestDataRequest map(Asset asset, Map categoryIdByCode); - @Mapping(target = "categories", source = "categories", qualifiedByName = "mapCategories") @Mapping(target = "logo", ignore = true) Asset map(com.backbase.investment.api.service.v1.model.Asset asset); - @AfterMapping - default void postMap(@MappingTarget OASAssetRequestDataRequest requestDataRequest, Asset asset, - Map categoryIdByCode) { - if (requestDataRequest == null) { - return; - } - requestDataRequest.setCategories(Objects.requireNonNullElse(asset.getCategories(), new ArrayList()) - .stream().filter(Objects::nonNull).map(categoryIdByCode::get) - .filter(Objects::nonNull).toList()); - } - @Named("mapCategories") default List mapCategories(List categories) { return Objects.requireNonNullElse(categories, new ArrayList()) diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/CustomIntegrationApiService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/CustomIntegrationApiService.java index 203e87440..b334ae3f2 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/CustomIntegrationApiService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/CustomIntegrationApiService.java @@ -1,9 +1,7 @@ package com.backbase.stream.investment.service; import com.backbase.investment.api.service.ApiClient; -import com.backbase.investment.api.service.v1.model.Asset; import com.backbase.investment.api.service.v1.model.OASAllocationCreateRequest; -import com.backbase.investment.api.service.v1.model.OASAssetRequestDataRequest; import com.backbase.investment.api.service.v1.model.OASModelPortfolioRequestDataRequest; import com.backbase.investment.api.service.v1.model.OASModelPortfolioResponse; import com.backbase.investment.api.service.v1.model.OASPortfolioAllocation; @@ -32,42 +30,6 @@ public class CustomIntegrationApiService { private final ApiClient apiClient; - /** - * Creates a new asset by sending a POST request to the asset API. - * - * @param assetRequest the asset request payload - * @return Mono representing the created asset - * @throws WebClientResponseException if the API call fails - */ - public Mono createAsset(OASAssetRequestDataRequest assetRequest) throws WebClientResponseException { - // create path and map variables - final Map pathParams = new HashMap(); - - final MultiValueMap queryParams = new LinkedMultiValueMap(); - final HttpHeaders headerParams = new HttpHeaders(); - final MultiValueMap cookieParams = new LinkedMultiValueMap(); - final MultiValueMap formParams = new LinkedMultiValueMap(); - - final String[] localVarAccepts = { - "application/json" - }; - final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); - final String[] localVarContentTypes = { - "application/json" - }; - final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); - - String[] localVarAuthNames = new String[]{}; - - ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() { - }; - return apiClient.invokeAPI("/service-api/v2/asset/assets/", HttpMethod.POST, pathParams, queryParams, - assetRequest, - headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, - localVarReturnType) - .bodyToMono(localVarReturnType); - } - public Mono patchModelPortfolioRequestCreation(String uuid, List expand, String fields, String omit, OASModelPortfolioRequestDataRequest data, File image) throws WebClientResponseException { diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java index 58a195743..73a40726b 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java @@ -1,31 +1,27 @@ package com.backbase.stream.investment.service; +import com.backbase.investment.api.service.sync.v1.model.AssetCategory; import com.backbase.investment.api.service.v1.AssetUniverseApi; -import com.backbase.investment.api.service.v1.model.Asset; -import com.backbase.investment.api.service.v1.model.AssetCategory; -import com.backbase.investment.api.service.v1.model.AssetCategoryRequest; import com.backbase.investment.api.service.v1.model.AssetCategoryType; import com.backbase.investment.api.service.v1.model.AssetCategoryTypeRequest; import com.backbase.investment.api.service.v1.model.Market; import com.backbase.investment.api.service.v1.model.MarketRequest; import com.backbase.investment.api.service.v1.model.MarketSpecialDay; import com.backbase.investment.api.service.v1.model.MarketSpecialDayRequest; -import com.backbase.investment.api.service.v1.model.OASAssetRequestDataRequest; import com.backbase.investment.api.service.v1.model.PaginatedAssetCategoryList; +import com.backbase.stream.investment.model.AssetCategoryEntry; import com.backbase.stream.investment.service.resttemplate.InvestmentRestAssetUniverseService; -import java.io.File; -import java.io.IOException; import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.mapstruct.factory.Mappers; -import org.springframework.core.io.Resource; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; @@ -37,7 +33,6 @@ public class InvestmentAssetUniverseService { private final AssetUniverseApi assetUniverseApi; private final InvestmentRestAssetUniverseService investmentRestAssetUniverseService; - private final CustomIntegrationApiService customIntegrationApiService; private final AssetMapper assetMapper = Mappers.getMapper(AssetMapper.class); /** @@ -83,20 +78,17 @@ public Mono upsertMarket(MarketRequest marketRequest) { } /** - * Gets an existing asset by its identifier, or creates it if not found (404). Handles 404 NOT_FOUND from - * getAsset by returning Mono.empty(), which triggers asset creation via switchIfEmpty. + * Gets an existing asset by its identifier, or creates it if not found (404). Handles 404 NOT_FOUND from getAsset + * by returning Mono.empty(), which triggers asset creation via switchIfEmpty. * - * @param assetRequest the asset request details - * @param logo the thumbnail image * @return Mono representing the existing or newly created asset - * @throws IOException if an I/O error occurs */ - public Mono getOrCreateAsset(OASAssetRequestDataRequest assetRequest, Resource logo) { - log.debug("Creating asset: {}", assetRequest); + public Mono getOrCreateAsset(com.backbase.stream.investment.Asset asset, + Map categoryIdByCode) { + log.debug("Creating asset: {}", asset); // Build a unique asset identifier using ISIN, market, and currency - final String assetIdentifier = - assetRequest.getIsin() + "_" + assetRequest.getMarket() + "_" + assetRequest.getCurrency(); + String assetIdentifier = asset.getKeyString(); // Try to fetch the asset by its identifier return assetUniverseApi.getAsset(assetIdentifier, null, null, null) @@ -110,16 +102,13 @@ public Mono getOrCreateAsset(OASAssetRequestDataRequest assetRequest, Res return Mono.error(error); }) // If asset exists, log and return it - .map(existingAsset -> { + .flatMap(a -> { log.info("Asset already exists with Asset Identifier : {}", assetIdentifier); - return existingAsset; + return investmentRestAssetUniverseService.patchAsset(a, asset).thenReturn(a); }) - .flatMap(a -> investmentRestAssetUniverseService.setAssetLogo(a, logo) - .thenReturn(a)) + .map(assetMapper::map) // If Mono is empty (asset not found), create the asset - .switchIfEmpty(customIntegrationApiService.createAsset(assetRequest) - .flatMap(a -> investmentRestAssetUniverseService.setAssetLogo(a, logo) - .thenReturn(a)) + .switchIfEmpty(investmentRestAssetUniverseService.createAsset(asset, categoryIdByCode) .doOnSuccess(createdAsset -> log.info("Created asset with assetIdentifier: {}", assetIdentifier)) .doOnError(error -> { if (error instanceof WebClientResponseException w) { @@ -134,8 +123,8 @@ public Mono getOrCreateAsset(OASAssetRequestDataRequest assetRequest, Res } /** - * Gets an existing market special day by date and market, or creates it if not found. Handles 404 or empty - * results by creating the market special day. + * Gets an existing market special day by date and market, or creates it if not found. Handles 404 or empty results + * by creating the market special day. * * @param marketSpecialDayRequest the request containing market and date details * @return Mono\ representing the existing or newly created market special day @@ -186,8 +175,7 @@ public Mono upsertMarketSpecialDay(MarketSpecialDayRequest mar .doOnSuccess( createdMarketSpecialDay -> log.info("Created market special day: {}", createdMarketSpecialDay)) .doOnError(error -> { - if (error instanceof WebClientResponseException) { - WebClientResponseException w = (WebClientResponseException) error; + if (error instanceof WebClientResponseException w) { log.error("Error creating market special day : {} : HTTP {} -> {}", marketSpecialDayRequest, w.getStatusCode(), w.getResponseBodyAsString()); } else { @@ -210,78 +198,79 @@ public Flux createAssets(List { Map categoryIdByCode = categories.stream() - .collect(Collectors.toMap(AssetCategory::getCode, AssetCategory::getUuid)); + .collect(Collectors.toMap(com.backbase.investment.api.service.v1.model.AssetCategory::getCode, + com.backbase.investment.api.service.v1.model.AssetCategory::getUuid)); return Flux.fromIterable(assets) - .flatMap(asset -> { - OASAssetRequestDataRequest assetRequest = assetMapper.map(asset, categoryIdByCode); - return this.getOrCreateAsset(assetRequest, asset.getLogoFile()).map(assetMapper::map); - }); + .flatMap(asset -> this.getOrCreateAsset(asset, categoryIdByCode)); }); } /** - * Gets an existing asset category by its code, or creates it if not found. Handles empty results by creating - * the asset category. + * Gets an existing asset category by its code, or creates it if not found. Handles empty results by creating the + * asset category. * - * @param assetCategoryRequest the request containing asset category details + * @param assetCategoryEntry the request containing asset category details * @return Mono representing the existing or newly created asset category */ - public Mono upsertAssetCategory(AssetCategoryRequest assetCategoryRequest) { - if (assetCategoryRequest == null) { + public Mono upsertAssetCategory(AssetCategoryEntry assetCategoryEntry) { + if (assetCategoryEntry == null) { return Mono.empty(); } - File logo = assetCategoryRequest.getImage(); - // Post request cannot insert file directly, so set to null for the initial creation call - assetCategoryRequest.setImage(null); - return assetUniverseApi.listAssetCategories(assetCategoryRequest.getCode(), 100, - assetCategoryRequest.getName(), 0, assetCategoryRequest.getOrder(), assetCategoryRequest.getType()) - .flatMap(paginatedAssetCategoryList -> { - List assetCategoryList = paginatedAssetCategoryList.getResults(); - if (assetCategoryList == null || assetCategoryList.isEmpty()) { - log.debug("No asset category exists for code: {}", assetCategoryRequest.getCode()); + return assetUniverseApi.listAssetCategories(assetCategoryEntry.getCode(), 100, + assetCategoryEntry.getName(), 0, assetCategoryEntry.getOrder(), + assetCategoryEntry.getType()) + .flatMap(paginatedAssetCategoryList -> Optional.ofNullable(paginatedAssetCategoryList) + .map(PaginatedAssetCategoryList::getResults) + .filter(Predicate.not(List::isEmpty)) + .flatMap(l -> l.stream() + .filter(ac -> assetCategoryEntry.getCode().equals(ac.getCode())) + .findAny()) + .map(c -> { + log.info("Asset category already exists for code: {}", assetCategoryEntry.getCode()); + return investmentRestAssetUniverseService.patchAssetCategory( + c.getUuid(), + assetCategoryEntry, assetCategoryEntry.getImageResource()) + .doOnSuccess(updatedCategory -> { + assetCategoryEntry.setUuid(updatedCategory.getUuid()); + log.info("Updated asset category: {}", updatedCategory); + }) + .doOnError(error -> { + if (error instanceof WebClientResponseException w) { + log.error("Error updating asset category: {} : HTTP {} -> {}", + assetCategoryEntry.getCode(), + w.getStatusCode(), w.getResponseBodyAsString()); + } else { + log.error("Error updating asset category: {} : {}", + assetCategoryEntry.getCode(), + error.getMessage(), error); + } + }) + .onErrorResume(e -> Mono.empty()); + }) + .orElseGet(() -> { + log.debug("No asset category exists for code: {}", assetCategoryEntry.getCode()); return Mono.empty(); - } else { - Optional matchingCategory = assetCategoryList.stream() - .filter(ac -> assetCategoryRequest.getCode().equals(ac.getCode())) - .findFirst(); - if (matchingCategory.isPresent()) { - log.info("Asset category already exists for code: {}", assetCategoryRequest.getCode()); - return assetUniverseApi.updateAssetCategory(matchingCategory.get().getUuid().toString(), assetCategoryRequest) - .doOnSuccess(updatedCategory -> log.info("Updated asset category: {}", updatedCategory)) - .doOnError(error -> { - if (error instanceof WebClientResponseException w) { - log.error("Error updating asset category: {} : HTTP {} -> {}", assetCategoryRequest.getCode(), - w.getStatusCode(), w.getResponseBodyAsString()); - } else { - log.error("Error updating asset category: {} : {}", assetCategoryRequest.getCode(), - error.getMessage(), error); - } - }) - .onErrorResume(e -> Mono.empty()); - } else { - log.debug("No asset category exists for code: {}", assetCategoryRequest.getCode()); - return Mono.empty(); - } - } - }) - .switchIfEmpty( - assetUniverseApi.createAssetCategory(assetCategoryRequest) - .doOnSuccess(createdCategory -> log.info("Created asset category : {}", createdCategory)) - .doOnError(error -> { - if (error instanceof WebClientResponseException w) { - log.error("Error creating asset category: {} : HTTP {} -> {}", - assetCategoryRequest.getCode(), - w.getStatusCode(), w.getResponseBodyAsString()); - } else { - log.error("Error creating asset category: {} : {}", assetCategoryRequest.getCode(), - error.getMessage(), error); - } - }) - ) - .flatMap(ac -> investmentRestAssetUniverseService.setAssetCategoryLogo(ac.getUuid(), logo) - .thenReturn(ac) - ) - .onErrorResume(e -> Mono.empty()); + }) + .switchIfEmpty( + investmentRestAssetUniverseService + .createAssetCategory(assetCategoryEntry, assetCategoryEntry.getImageResource()) + .doOnSuccess(createdCategory -> { + assetCategoryEntry.setUuid(createdCategory.getUuid()); + log.info("Created asset category : {}", createdCategory); + }) + .doOnError(error -> { + if (error instanceof WebClientResponseException w) { + log.error("Error creating asset category: {} : HTTP {} -> {}", + assetCategoryEntry.getCode(), + w.getStatusCode(), w.getResponseBodyAsString()); + } else { + log.error("Error creating asset category: {} : {}", assetCategoryEntry.getCode(), + error.getMessage(), error); + } + }) + ) + .onErrorResume(e -> Mono.empty()) + ); } /** @@ -307,13 +296,16 @@ public Mono upsertAssetCategoryType(AssetCategoryTypeRequest .filter(act -> assetCategoryTypeRequest.getCode().equals(act.getCode())) .findFirst(); if (matchingType.isPresent()) { - log.info("Asset category type already exists for code: {}", assetCategoryTypeRequest.getCode()); - return assetUniverseApi.updateAssetCategoryType(matchingType.get().getUuid().toString(), assetCategoryTypeRequest) + log.info("Asset category type already exists for code: {}", + assetCategoryTypeRequest.getCode()); + return assetUniverseApi.updateAssetCategoryType(matchingType.get().getUuid().toString(), + assetCategoryTypeRequest) .doOnSuccess(updatedType -> log.info("Updated asset category type: {}", updatedType)) .doOnError(error -> { if (error instanceof WebClientResponseException w) { log.error("Error updating asset category type: {} : HTTP {} -> {}", - assetCategoryTypeRequest.getCode(), w.getStatusCode(), w.getResponseBodyAsString()); + assetCategoryTypeRequest.getCode(), w.getStatusCode(), + w.getResponseBodyAsString()); } else { log.error("Error updating asset category type: {} : {}", assetCategoryTypeRequest.getCode(), error.getMessage(), error); @@ -335,7 +327,8 @@ public Mono upsertAssetCategoryType(AssetCategoryTypeRequest assetCategoryTypeRequest.getCode(), w.getStatusCode(), w.getResponseBodyAsString()); } else { - log.error("Error creating asset category type: {} : {}", assetCategoryTypeRequest.getCode(), + log.error("Error creating asset category type: {} : {}", + assetCategoryTypeRequest.getCode(), error.getMessage(), error); } }) diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/ContentMapper.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/ContentMapper.java new file mode 100644 index 000000000..9e1957e1a --- /dev/null +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/ContentMapper.java @@ -0,0 +1,14 @@ +package com.backbase.stream.investment.service.resttemplate; + +import com.backbase.investment.api.service.sync.v1.model.EntryCreateUpdateRequest; +import com.backbase.stream.investment.model.MarketNewsEntry; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface ContentMapper { + + @Mapping(target = "thumbnail", ignore = true) + EntryCreateUpdateRequest map(MarketNewsEntry entry); + +} 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 396b484b9..fafadc3e3 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 @@ -3,17 +3,22 @@ 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 java.io.File; +import com.backbase.stream.investment.model.AssetCategoryEntry; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.mapstruct.factory.Mappers; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -30,36 +35,140 @@ public class InvestmentRestAssetUniverseService { private final AssetUniverseApi assetUniverseApi; private final ApiClient apiClient; + private final RestTemplateAssetMapper assetMapper = Mappers.getMapper(RestTemplateAssetMapper.class); - public Mono setAssetLogo(Asset asset, Resource logo) { - String assetUuid = asset.getUuid().toString(); + public Mono createAsset(com.backbase.stream.investment.Asset asset, + Map categoryIdByCode) { - if (logo == null) { - log.debug("Skipping logo attachment: assetUuid={}", assetUuid); - return Mono.just(asset); - } + OASAssetRequestDataRequest assetRequestDataRequest = assetMapper.mapAsset(asset, categoryIdByCode); + + log.info( + "Start created asset with assetIdentifier:: assetUuid={}, assetName='{}', logoFile='{}'", + asset.getKeyString(), asset.getName(), asset.getLogo()); + + return Mono.defer(() -> Mono.just(createAsset(assetRequestDataRequest, asset.getLogoFile()))) + .map(createdAsset -> { + log.info( + "Asset created successfully: assetUuid={}, assetName='{}'", createdAsset.getUuid(), + createdAsset.getName()); + return assetMapper.mapFromSyncAsset(createdAsset); + }) + .onErrorResume(throwable -> { + log.error( + "Asset creation failed for asset:asset={}, assetName='{}', logoFile='{}', errorType={}, errorMessage={}", + asset.getKeyString(), asset.getName(), asset.getLogo(), throwable.getClass().getSimpleName(), + throwable.getMessage(), throwable); + log.warn("Asset processing continuing without asset={}", asset.getKeyString()); + return Mono.just(asset); + }); + } + + public Mono patchAsset(Asset existAsset, + com.backbase.stream.investment.Asset asset) { + String assetUuid = existAsset.getUuid().toString(); log.info( - "Starting logo attachment for asset: assetUuid={}, assetName='{}', logoFile='{}'", - assetUuid, asset.getName(), logo.getFilename()); + "Starting asset update: assetUuid={}, assetName='{}', logoFile='{}'", + assetUuid, asset.getName(), asset.getLogo()); - return Mono.defer(() -> Mono.just(patchAsset(assetUuid, null, logo))) + return Mono.defer(() -> Mono.just(patchAsset(assetUuid, null, asset.getLogoFile()))) .map(patchedAsset -> { log.info( "Logo attached successfully to asset:assetUuid={}, assetName='{}', logoFile='{}'", assetUuid, - asset.getName(), logo.getFilename()); + asset.getName(), asset.getLogo()); return asset; }).onErrorResume(throwable -> { log.error( "Logo attachment failed for asset:assetUuid={}, assetName='{}', logoFile='{}', errorType={}, errorMessage={}", - assetUuid, asset.getName(), logo.getFilename(), throwable.getClass().getSimpleName(), + assetUuid, asset.getName(), asset.getLogo(), throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); log.warn("Asset processing continuing without logo:assetUuid={}", assetUuid); return Mono.just(asset); }); } - public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { + private com.backbase.investment.api.service.sync.v1.model.Asset createAsset(OASAssetRequestDataRequest data, + Resource logo) { + Object localVarPostBody = null; + + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + if (data != null) { + localVarFormParams.add("data", data); + } + if (logo != null) { + localVarFormParams.add("logo", logo); + } + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "multipart/form-data" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[]{}; + + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { + }; + return apiClient.invokeAPI("/service-api/v2/asset/assets/", HttpMethod.POST, + Collections.emptyMap(), localVarQueryParams, localVarPostBody, localVarHeaderParams, + localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, + localReturnType) + .getBody(); + } + + public com.backbase.investment.api.service.sync.v1.model.Asset patchAsset(String assetIdentifier, + OASAssetRequestDataRequest data, Resource logo) { + Object localVarPostBody = null; + + // verify the required parameter 'assetIdentifier' is set + if (assetIdentifier == null) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, + "Missing the required parameter 'assetIdentifier' when calling patchAsset"); + } + + // create path and map variables + final Map uriVariables = new HashMap(); + uriVariables.put("asset_identifier", assetIdentifier); + + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + if (data != null) { + localVarFormParams.add("data", data); + } + if (logo != null) { + localVarFormParams.add("logo", logo); + } + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "multipart/form-data" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[]{}; + + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { + }; + return apiClient.invokeAPI("/service-api/v2/asset/assets/{asset_identifier}/", HttpMethod.PATCH, uriVariables, + localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, + localVarAccept, localVarContentType, localVarAuthNames, localReturnType) + .getBody(); + } + + public Mono setAssetCategoryLogo(UUID assetCategoryId, Resource logo) { String assetCategoryUuid = assetCategoryId.toString(); if (logo == null) { @@ -69,8 +178,8 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { } log.info( - "Starting logo attachment for asset category: operation=setLogo, assetCategoryUuid={}, logoFile='{}', logoSize={}, action=start", - assetCategoryUuid, logo.getName(), logo.length()); + "Starting logo attachment for asset category: operation=setLogo, assetCategoryUuid={}, logoFile='{}'", + assetCategoryUuid, getFileNameForLog(logo)); return Mono.defer(() -> { // verify the required parameter 'uuid' is set @@ -88,7 +197,7 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); - localVarFormParams.add("image", new FileSystemResource(logo)); + localVarFormParams.add("image", logo); final String[] localVarAccepts = {"application/json"}; final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); @@ -107,12 +216,12 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { }).map(patchedAssetCategoryUuId -> { log.info( "Logo attached successfully to asset category: assetCategoryUuid={}, logoFile='{}'", - patchedAssetCategoryUuId, logo.getName()); + patchedAssetCategoryUuId, getFileNameForLog(logo)); return patchedAssetCategoryUuId; }).onErrorResume(throwable -> { log.error( "Logo attachment failed for asset category: assetCategoryUuid={}, logoFile='{}', errorType={}, errorMessage={}", - assetCategoryId, logo.getName(), throwable.getClass().getSimpleName(), throwable.getMessage(), + assetCategoryId, getFileNameForLog(logo), throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); log.warn( "Asset processing continuing without logo: assetCategoryUuid={}", assetCategoryId); @@ -120,49 +229,129 @@ public Mono setAssetCategoryLogo(UUID assetCategoryId, File logo) { }); } - public com.backbase.investment.api.service.sync.v1.model.Asset patchAsset(String assetIdentifier, - OASAssetRequestDataRequest data, Resource logo) { - Object localVarPostBody = null; + public Mono patchAssetCategory(UUID assetCategoryId, + AssetCategoryEntry assetCategoryEntry, Resource image) { - // verify the required parameter 'assetIdentifier' is set - if (assetIdentifier == null) { + PatchedAssetCategoryRequest assetCategoryPatch = assetMapper.mapPatchAssetCategory( + assetCategoryEntry); + + String uuid = assetCategoryId.toString(); + + log.info( + "Starting asset category patch: assetCategoryUuid={}, logoFile='{}'", + uuid, getFileNameForLog(image)); + + return Mono.defer(() -> { + if (uuid == null) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, + "Missing the required parameter 'uuid' when calling partialUpdateAssetCategory"); + } + + // create path and map variables + final Map uriVariables = new HashMap(); + uriVariables.put("uuid", uuid); + + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + Optional.ofNullable(assetCategoryPatch.getName()) + .ifPresent(v -> localVarFormParams.add("name", v)); + Optional.ofNullable(assetCategoryPatch.getCode()) + .ifPresent(v -> localVarFormParams.add("code", v)); + Optional.ofNullable(assetCategoryPatch.getOrder()) + .ifPresent(v -> localVarFormParams.add("order", v)); + Optional.ofNullable(assetCategoryPatch.getType()) + .ifPresent(v -> localVarFormParams.add("type", v)); + Optional.ofNullable(assetCategoryPatch.getExcerpt()) + .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)); + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "multipart/form-data" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[]{}; + + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { + }; + return Mono.just( + apiClient.invokeAPI("/service-api/v2/asset/asset-categories/{uuid}/", HttpMethod.PATCH, uriVariables, + localVarQueryParams, null, localVarHeaderParams, localVarCookieParams, localVarFormParams, + localVarAccept, localVarContentType, localVarAuthNames, localReturnType) + .getBody()); + }); + } + + @Nonnull + public static String getFileNameForLog(Resource image) { + return Optional.ofNullable(image).map(Resource::getFilename).orElse("null"); + } + + public Mono createAssetCategory(AssetCategoryEntry assetCategoryEntry, Resource image) { + + AssetCategoryRequest assetCategoryRequest = assetMapper.mapAssetCategory(assetCategoryEntry); + + log.info( + "Starting create asset category : assetCategory={}, logoFile='{}'", + assetCategoryRequest.getName(), getFileNameForLog(image)); + + // verify the required parameter 'assetCategoryRequest' is set + if (assetCategoryRequest == null) { throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, - "Missing the required parameter 'assetIdentifier' when calling patchAsset"); + "Missing the required parameter 'assetCategoryRequest' when calling createAssetCategory"); } - // create path and map variables - final Map uriVariables = new HashMap(); - uriVariables.put("asset_identifier", assetIdentifier); + return Mono.defer(() -> { + // create path and map variables + final Map uriVariables = new HashMap(); - final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); - final HttpHeaders localVarHeaderParams = new HttpHeaders(); - final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); - final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); - if (data != null) { - localVarFormParams.add("data", data); - } - if (logo != null) { - localVarFormParams.add("logo", logo); - } + Optional.ofNullable(assetCategoryRequest.getName()) + .ifPresent(v -> localVarFormParams.add("name", v)); + Optional.ofNullable(assetCategoryRequest.getCode()) + .ifPresent(v -> localVarFormParams.add("code", v)); + Optional.ofNullable(assetCategoryRequest.getOrder()) + .ifPresent(v -> localVarFormParams.add("order", v)); + Optional.ofNullable(assetCategoryRequest.getType()) + .ifPresent(v -> localVarFormParams.add("type", v)); + Optional.ofNullable(assetCategoryRequest.getExcerpt()) + .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)); - final String[] localVarAccepts = { - "application/json" - }; - final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); - final String[] localVarContentTypes = { - "multipart/form-data" - }; - final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "multipart/form-data" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); - String[] localVarAuthNames = new String[]{}; + String[] localVarAuthNames = new String[]{}; - ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { - }; - return apiClient.invokeAPI("/service-api/v2/asset/assets/{asset_identifier}/", HttpMethod.PATCH, uriVariables, - localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, - localVarAccept, localVarContentType, localVarAuthNames, localReturnType) - .getBody(); + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() { + }; + return Mono.just(apiClient.invokeAPI("/service-api/v2/asset/asset-categories/", HttpMethod.POST, + uriVariables, + localVarQueryParams, null, localVarHeaderParams, localVarCookieParams, localVarFormParams, + localVarAccept, localVarContentType, localVarAuthNames, localReturnType) + .getBody()); + }); } } \ No newline at end of file diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java index c1a732a46..7effa2a65 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java @@ -1,11 +1,13 @@ package com.backbase.stream.investment.service.resttemplate; +import static com.backbase.stream.investment.service.resttemplate.InvestmentRestAssetUniverseService.getFileNameForLog; + import com.backbase.investment.api.service.sync.ApiClient; import com.backbase.investment.api.service.sync.v1.ContentApi; import com.backbase.investment.api.service.sync.v1.model.Entry; import com.backbase.investment.api.service.sync.v1.model.EntryCreateUpdate; import com.backbase.investment.api.service.sync.v1.model.EntryCreateUpdateRequest; -import java.io.File; +import com.backbase.stream.investment.model.MarketNewsEntry; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,8 +18,9 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.mapstruct.factory.Mappers; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -43,6 +46,7 @@ public class InvestmentRestNewsContentService { public static final int CONTENT_RETRIEVE_LIMIT = 100; private final ContentApi contentApi; private final ApiClient apiClient; + private final ContentMapper contentMapper = Mappers.getMapper(ContentMapper.class); /** * Upserts a list of content entries. For each entry, checks if content with the same title exists. If exists, @@ -52,7 +56,7 @@ public class InvestmentRestNewsContentService { * @param contentEntries List of content entries to upsert * @return Mono that completes when all entries have been processed */ - public Mono upsertContent(List contentEntries) { + public Mono upsertContent(List contentEntries) { log.info("Starting content upsert batch operation:, totalEntries={}", contentEntries.size()); log.debug("Content upsert batch details: entries={}", contentEntries); @@ -70,9 +74,9 @@ public Mono upsertContent(List contentEntries) { * @param request The content entry to upsert * @return Mono that completes when the entry has been processed */ - private Mono upsertSingleEntry(EntryCreateUpdateRequest request) { + private Mono upsertSingleEntry(MarketNewsEntry request) { log.debug("Processing content entry: title='{}', hasThumbnail={}", request.getTitle(), - request.getThumbnail() != null); + request.getThumbnailResource() != null); return createNewEntry(request) .doOnSuccess( @@ -97,9 +101,9 @@ private Mono upsertSingleEntry(EntryCreateUpdateRequest reque }); } - private Flux findEntriesNewContent(List contentEntries) { - Map entryByTitle = contentEntries.stream() - .collect(Collectors.toMap(EntryCreateUpdateRequest::getTitle, Function.identity())); + private Flux findEntriesNewContent(List contentEntries) { + Map entryByTitle = contentEntries.stream() + .collect(Collectors.toMap(MarketNewsEntry::getTitle, Function.identity())); log.debug("Filtering content entries: requestedTitles={}", entryByTitle.keySet()); List existsNews = contentApi.listContentEntries(null, CONTENT_RETRIEVE_LIMIT, 0, null, null, null, null) @@ -112,14 +116,14 @@ private Flux findEntriesNewContent(List existTitles = existsNews.stream().map(Entry::getTitle).collect(Collectors.toSet()); - List newEntries = contentEntries.stream() + List newEntries = contentEntries.stream() .filter(c -> existTitles.stream().noneMatch(e -> c.getTitle().contains(e))).toList(); log.info( "Content filtering completed: requestedEntries={}, existingEntriesFound={}, newEntriesToCreate={}, duplicatesSkipped={}", entryByTitle.size(), existsNews.size(), newEntries.size(), entryByTitle.size() - newEntries.size()); log.debug("Filtered new content titles: newTitles={}", - newEntries.stream().map(EntryCreateUpdateRequest::getTitle).collect(Collectors.toList())); + newEntries.stream().map(MarketNewsEntry::getTitle).collect(Collectors.toList())); return Flux.fromIterable(newEntries); } @@ -130,23 +134,23 @@ private Flux findEntriesNewContent(List createNewEntry(EntryCreateUpdateRequest request) { + private Mono createNewEntry(MarketNewsEntry request) { log.debug("Creating new content entry: title='{}', hasThumbnail={}", request.getTitle(), - request.getThumbnail() != null); - File thumbnail = request.getThumbnail(); - request.setThumbnail(null); - return Mono.defer(() -> Mono.just(contentApi.createContentEntry(request))) - .flatMap(e -> addThumbnail(e, thumbnail)) + request.getThumbnailResource() != null); + EntryCreateUpdateRequest createUpdateRequest = contentMapper.map(request); + log.debug("Content entry processing : {}", createUpdateRequest); + return Mono.defer(() -> Mono.just(contentApi.createContentEntry(createUpdateRequest))) + .flatMap(e -> addThumbnail(e, request.getThumbnailResource())) .doOnSuccess( created -> log.info("Content entry created successfully: title='{}', uuid={}, thumbnailAttached={}", - request.getTitle(), created.getUuid(), thumbnail != null)) + request.getTitle(), created.getUuid(), request.getThumbnailResource() != null)) .doOnError( error -> log.error("Content entry creation failed: title='{}', errorType={}, errorMessage={}", request.getTitle(), error.getClass().getSimpleName(), error.getMessage(), error)) .onErrorResume(error -> Mono.empty()); } - private Mono addThumbnail(EntryCreateUpdate entry, File thumbnail) { + private Mono addThumbnail(EntryCreateUpdate entry, Resource thumbnail) { UUID uuid = entry.getUuid(); if (thumbnail == null) { @@ -154,21 +158,20 @@ private Mono addThumbnail(EntryCreateUpdate entry, File thumb return Mono.just(entry); } - log.debug("Attaching thumbnail to content entry: uuid={}, thumbnailFile='{}', thumbnailSize={}", uuid, - thumbnail.getName(), thumbnail.length()); + log.debug("Attaching thumbnail to content entry: uuid={}, thumbnailFile='{}'", uuid, + getFileNameForLog(thumbnail)); return Mono.defer(() -> { - // create path and map variables - Map uriVariables = new HashMap<>(); - uriVariables.put("uuid", uuid); + // create path and map variables + Map uriVariables = new HashMap<>(); + uriVariables.put("uuid", uuid); MultiValueMap localVarQueryParams = new LinkedMultiValueMap<>(); HttpHeaders localVarHeaderParams = new HttpHeaders(); MultiValueMap localVarCookieParams = new LinkedMultiValueMap<>(); MultiValueMap localVarFormParams = new LinkedMultiValueMap<>(); - FileSystemResource value = new FileSystemResource(thumbnail); - localVarFormParams.add("thumbnail", value); + localVarFormParams.add("thumbnail", thumbnail); final String[] localVarAccepts = {"application/json"}; final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); @@ -183,11 +186,13 @@ private Mono addThumbnail(EntryCreateUpdate entry, File thumb localVarQueryParams, null, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType); - log.info("Thumbnail attached successfully: uuid={}, thumbnailFile='{}'", uuid, thumbnail.getName()); + log.info("Thumbnail attached successfully: uuid={}, thumbnailFile='{}'", uuid, + getFileNameForLog(thumbnail)); return Mono.just(entry); }).doOnError(error -> log.error( "Thumbnail attachment failed: uuid={}, thumbnailFile='{}', errorType={}, errorMessage={}", uuid, - thumbnail.getName(), error.getClass().getSimpleName(), error.getMessage(), error)).onErrorResume(error -> { + getFileNameForLog(thumbnail), error.getClass().getSimpleName(), error.getMessage(), error)) + .onErrorResume(error -> { log.warn("Content entry created without thumbnail: uuid={}, reason={}", uuid, error.getMessage()); return Mono.just(entry); }); diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/RestTemplateAssetMapper.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/RestTemplateAssetMapper.java new file mode 100644 index 000000000..af592eb37 --- /dev/null +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/RestTemplateAssetMapper.java @@ -0,0 +1,62 @@ +package com.backbase.stream.investment.service.resttemplate; + +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.stream.investment.Asset; +import com.backbase.stream.investment.model.AssetCategoryEntry; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Named; + +@Mapper +public interface RestTemplateAssetMapper { + + @Mapping(target = "categories", ignore = true) + OASAssetRequestDataRequest mapAsset(Asset asset, + Map categoryIdByCode); + + @AfterMapping + default void postMap( + @MappingTarget OASAssetRequestDataRequest requestDataRequest, + Asset asset, + Map categoryIdByCode) { + if (requestDataRequest == null) { + return; + } + requestDataRequest.setCategories( + Objects.requireNonNullElse(asset.getCategories(), new ArrayList()) + .stream().filter(Objects::nonNull).map(categoryIdByCode::get) + .filter(Objects::nonNull).toList()); + } + + @Mapping(target = "categories", source = "categories", qualifiedByName = "mapSyncCategories") + @Mapping(target = "logo", ignore = true) + com.backbase.stream.investment.Asset mapFromSyncAsset( + com.backbase.investment.api.service.sync.v1.model.Asset asset); + + // Post request cannot insert file directly, so set to null for the initial creation call + @Mapping(target = "image", ignore = true) + AssetCategoryRequest mapAssetCategory(AssetCategoryEntry entry); + + // Post request cannot insert file directly, so set to null for the initial creation call + @Mapping(target = "image", ignore = true) + PatchedAssetCategoryRequest mapPatchAssetCategory(AssetCategoryEntry entry); + + @Named("mapSyncCategories") + default List mapSyncCategories( + List categories) { + return Objects.requireNonNullElse(categories, + new ArrayList()) + .stream().map(com.backbase.investment.api.service.sync.v1.model.AssetCategory::getCode).toList(); + } + +} From a7acb0d5ffda084975ecfca99e9c55fa632657c7 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Mon, 2 Feb 2026 14:30:53 +0200 Subject: [PATCH 5/7] NOJIRA: fix file load and request send avoid `File`, use only spring `Resource`; --- .../stream/investment/saga/InvestmentAssetUniverseSagaTest.java | 2 ++ 1 file changed, 2 insertions(+) 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 46848b480..c1d05b4b7 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 @@ -383,6 +383,7 @@ private InvestmentAssetsTask createTestTask() { List.of("Technology"), "AAPL-001", null, + null, "Apple Inc. Stock", 150.0 ); @@ -400,6 +401,7 @@ private InvestmentAssetsTask createTestTask() { List.of("Technology"), "MSFT-001", null, + null, "Microsoft Corp. Stock", 200.0 ); From 34469fed29a369677145c5cffb52fb9cf4c45719 Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Mon, 2 Feb 2026 14:37:30 +0200 Subject: [PATCH 6/7] NOJIRA: fix file load and request send fix text compile --- .../InvestmentAssetUniverseServiceTest.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) 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 24627ab2c..d6ea7c960 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 @@ -5,15 +5,15 @@ import com.backbase.investment.api.service.v1.model.Asset; import com.backbase.investment.api.service.v1.model.Market; import com.backbase.investment.api.service.v1.model.MarketRequest; -import com.backbase.investment.api.service.v1.model.OASAssetRequestDataRequest; import com.backbase.stream.investment.service.resttemplate.InvestmentRestAssetUniverseService; -import java.io.File; import java.nio.charset.StandardCharsets; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -32,16 +32,14 @@ class InvestmentAssetUniverseServiceTest { AssetUniverseApi assetUniverseApi; ApiClient apiClient; InvestmentRestAssetUniverseService investmentRestAssetUniverseService; - CustomIntegrationApiService customIntegrationApiService; @BeforeEach void setUp() { assetUniverseApi = Mockito.mock(AssetUniverseApi.class); apiClient = Mockito.mock(ApiClient.class); investmentRestAssetUniverseService = Mockito.mock(InvestmentRestAssetUniverseService.class); - customIntegrationApiService = Mockito.mock(CustomIntegrationApiService.class); service = new InvestmentAssetUniverseService(assetUniverseApi, - investmentRestAssetUniverseService, customIntegrationApiService); + investmentRestAssetUniverseService); } @Test @@ -51,7 +49,7 @@ void upsertMarket_marketExists() { Market marketUpdated = new Market().code("US").name("Usa Market Updated"); Mockito.when(assetUniverseApi.getMarket("US")).thenReturn(Mono.just(market)); Mockito.when(assetUniverseApi.createMarket(request)).thenReturn(Mono.just(market)); - Mockito.when(assetUniverseApi.updateMarket("US",request)).thenReturn(Mono.just(marketUpdated)); + Mockito.when(assetUniverseApi.updateMarket("US", request)).thenReturn(Mono.just(marketUpdated)); StepVerifier.create(service.upsertMarket(request)) .expectNext(marketUpdated) @@ -91,11 +89,11 @@ void upsertMarket_otherError_propagates() { @Test void getOrCreateAsset_assetExists() { - OASAssetRequestDataRequest req = new OASAssetRequestDataRequest() - .isin("ABC123").market("US").currency("USD"); - Asset asset = new Asset().isin("ABC123"); + com.backbase.stream.investment.Asset req = createAsset(); + com.backbase.stream.investment.Asset asset = createAsset(); String assetId = "ABC123_US_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)).thenReturn(Mono.just(asset)); + Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) + .thenReturn(Mono.just(new Asset().isin("ABC123"))); WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); Mockito.when(apiClient.invokeAPI( @@ -120,13 +118,20 @@ void getOrCreateAsset_assetExists() { .verifyComplete(); } + private static com.backbase.stream.investment.Asset createAsset() { + com.backbase.stream.investment.Asset req = new com.backbase.stream.investment.Asset(); + req.setIsin("ABC123"); + req.setMarket("market"); + req.setCurrency("USD"); + return req; + } + @Test void getOrCreateAsset_assetNotFound_createsAsset() { - OASAssetRequestDataRequest req = new OASAssetRequestDataRequest() - .isin("ABC123").market("US").currency("USD"); - Asset createdAsset = new Asset().isin("ABC123"); + com.backbase.stream.investment.Asset req = createAsset(); + com.backbase.stream.investment.Asset createdAsset = createAsset(); String assetId = "ABC123_US_USD"; - File logo = null; + Resource logo = null; Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) .thenReturn(Mono.error(WebClientResponseException.create( @@ -154,15 +159,14 @@ void getOrCreateAsset_assetNotFound_createsAsset() { Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) .thenReturn(Mono.just(createdAsset)); - StepVerifier.create(service.getOrCreateAsset(req, logo)) + StepVerifier.create(service.getOrCreateAsset(req, Map.of())) .expectNext(createdAsset) .verifyComplete(); } @Test void getOrCreateAsset_otherError_propagates() { - OASAssetRequestDataRequest req = new OASAssetRequestDataRequest() - .isin("ABC123").market("US").currency("USD"); + com.backbase.stream.investment.Asset req = createAsset(); String assetId = "ABC123_US_USD"; Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) .thenReturn(Mono.error(new RuntimeException("API error"))); @@ -192,8 +196,7 @@ void getOrCreateAsset_otherError_propagates() { @Test void getOrCreateAsset_createAssetFails_propagates() { - OASAssetRequestDataRequest req = new OASAssetRequestDataRequest() - .isin("ABC123").market("US").currency("USD"); + com.backbase.stream.investment.Asset req = createAsset(); String assetId = "ABC123_US_USD"; Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) .thenReturn(Mono.error(WebClientResponseException.create( @@ -235,8 +238,7 @@ void getOrCreateAsset_nullRequest_returnsError() { @Test void getOrCreateAsset_emptyMonoFromCreateAsset() { - OASAssetRequestDataRequest req = new OASAssetRequestDataRequest() - .isin("ABC123").market("US").currency("USD"); + com.backbase.stream.investment.Asset req = createAsset(); String assetId = "ABC123_US_USD"; Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) .thenReturn(Mono.error(WebClientResponseException.create( From 9c3890adb7a711f63facec41b90780a46ab60aba Mon Sep 17 00:00:00 2001 From: Roman Kniazevych Date: Mon, 2 Feb 2026 16:00:50 +0200 Subject: [PATCH 7/7] NOJIRA: fix file load and request send fix logging; intraday price start processing; asset error process; --- .../InvestmentServiceConfiguration.java | 3 +- .../com/backbase/stream/investment/Asset.java | 2 + .../saga/InvestmentAssetUniverseSaga.java | 7 +- .../service/InvestmentAssetPriceService.java | 4 +- .../InvestmentAssetUniverseService.java | 49 ++---- .../InvestmentIntradayAssetPriceService.java | 40 +++-- .../InvestmentRestNewsContentService.java | 67 ++----- .../saga/InvestmentAssetUniverseSagaTest.java | 3 + .../InvestmentAssetUniverseServiceTest.java | 164 +++++++----------- 9 files changed, 134 insertions(+), 205 deletions(-) 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 60172735b..b36469f97 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 @@ -112,9 +112,10 @@ public InvestmentAssetUniverseSaga investmentStaticDataSaga( InvestmentAssetUniverseService investmentAssetUniverseService, InvestmentAssetPriceService investmentAssetPriceService, InvestmentIntradayAssetPriceService investmentIntradayAssetPriceService, + AsyncTaskService asyncTaskService, InvestmentIngestionConfigurationProperties coreConfigurationProperties) { return new InvestmentAssetUniverseSaga(investmentAssetUniverseService, investmentAssetPriceService, - investmentIntradayAssetPriceService, coreConfigurationProperties); + investmentIntradayAssetPriceService, asyncTaskService, coreConfigurationProperties); } @Bean diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java index 8e8ce4d58..fe678c5af 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/Asset.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.UUID; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -20,6 +21,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode public class Asset implements AssetKey { private UUID uuid; diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java index 800f5d2ad..d562cbb99 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/saga/InvestmentAssetUniverseSaga.java @@ -6,6 +6,7 @@ import com.backbase.stream.configuration.InvestmentIngestionConfigurationProperties; import com.backbase.stream.investment.InvestmentAssetData; import com.backbase.stream.investment.InvestmentAssetsTask; +import com.backbase.stream.investment.service.AsyncTaskService; import com.backbase.stream.investment.service.InvestmentAssetPriceService; import com.backbase.stream.investment.service.InvestmentAssetUniverseService; import com.backbase.stream.investment.service.InvestmentClientService; @@ -60,6 +61,7 @@ public class InvestmentAssetUniverseSaga implements StreamTaskExecutor upsertPrices(InvestmentAssetsTask investmentT } private Mono createIntradayPrices(InvestmentAssetsTask investmentTask) { - return investmentIntradayAssetPriceService.ingestIntradayPrices() - .map(investmentTask::setIntradayPriceTasks); + return asyncTaskService.checkPriceAsyncTasksFinished(investmentTask.getData().getPriceAsyncTasks()) + .then(investmentIntradayAssetPriceService.ingestIntradayPrices() + .map(investmentTask::setIntradayPriceTasks)); } /** diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetPriceService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetPriceService.java index 6972625d5..7301c0c55 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetPriceService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetPriceService.java @@ -71,7 +71,7 @@ private Mono>> generatePrices(List assets, Map>> generatePrices(List assets, Map { diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java index 73a40726b..989bcb76a 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentAssetUniverseService.java @@ -228,24 +228,8 @@ public Mono upsertAssetCategory(AssetCategoryEntry assetCategoryE .map(c -> { log.info("Asset category already exists for code: {}", assetCategoryEntry.getCode()); return investmentRestAssetUniverseService.patchAssetCategory( - c.getUuid(), - assetCategoryEntry, assetCategoryEntry.getImageResource()) - .doOnSuccess(updatedCategory -> { - assetCategoryEntry.setUuid(updatedCategory.getUuid()); - log.info("Updated asset category: {}", updatedCategory); - }) - .doOnError(error -> { - if (error instanceof WebClientResponseException w) { - log.error("Error updating asset category: {} : HTTP {} -> {}", - assetCategoryEntry.getCode(), - w.getStatusCode(), w.getResponseBodyAsString()); - } else { - log.error("Error updating asset category: {} : {}", - assetCategoryEntry.getCode(), - error.getMessage(), error); - } - }) - .onErrorResume(e -> Mono.empty()); + c.getUuid(), + assetCategoryEntry, assetCategoryEntry.getImageResource()); }) .orElseGet(() -> { log.debug("No asset category exists for code: {}", assetCategoryEntry.getCode()); @@ -254,21 +238,22 @@ public Mono upsertAssetCategory(AssetCategoryEntry assetCategoryE .switchIfEmpty( investmentRestAssetUniverseService .createAssetCategory(assetCategoryEntry, assetCategoryEntry.getImageResource()) - .doOnSuccess(createdCategory -> { - assetCategoryEntry.setUuid(createdCategory.getUuid()); - log.info("Created asset category : {}", createdCategory); - }) - .doOnError(error -> { - if (error instanceof WebClientResponseException w) { - log.error("Error creating asset category: {} : HTTP {} -> {}", - assetCategoryEntry.getCode(), - w.getStatusCode(), w.getResponseBodyAsString()); - } else { - log.error("Error creating asset category: {} : {}", assetCategoryEntry.getCode(), - error.getMessage(), error); - } - }) ) + .doOnSuccess(updatedCategory -> { + assetCategoryEntry.setUuid(updatedCategory.getUuid()); + log.info("Updated asset category: {}", updatedCategory); + }) + .doOnError(error -> { + if (error instanceof WebClientResponseException w) { + log.error("Error updating asset category: {} : HTTP {} -> {}", + assetCategoryEntry.getCode(), + w.getStatusCode(), w.getResponseBodyAsString()); + } else { + log.error("Error updating asset category: {} : {}", + assetCategoryEntry.getCode(), + error.getMessage(), error); + } + }) .onErrorResume(e -> Mono.empty()) ); } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentIntradayAssetPriceService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentIntradayAssetPriceService.java index 51be1511d..cb05e17f6 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentIntradayAssetPriceService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/InvestmentIntradayAssetPriceService.java @@ -5,6 +5,7 @@ import com.backbase.investment.api.service.v1.model.OASCreatePriceRequest; import com.backbase.investment.api.service.v1.model.TypeEnum; import com.backbase.stream.investment.model.AssetWithMarketAndLatestPrice; +import com.backbase.stream.investment.model.ExpandedLatestPrice; import com.backbase.stream.investment.model.PaginatedExpandedAssetList; import java.math.BigDecimal; import java.math.RoundingMode; @@ -14,9 +15,9 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; @@ -29,14 +30,12 @@ * Service responsible for generating and ingesting intraday asset prices. * *

Responsibilities: - * - Read assets with latest prices via {@link AssetUniverseApi}. - * - Generate a series of intraday OHLC price points per asset. - * - Submit intraday prices back to the Asset API using bulk create. + * - Read assets with latest prices via {@link AssetUniverseApi}. - Generate a series of intraday OHLC price points per + * asset. - Submit intraday prices back to the Asset API using bulk create. * *

Notes: - * - The generator uses a randomised model constrained to realistic percentage ranges. - * - The service is reactive and non-blocking: ingestion returns a {@link Mono} that completes - * after all async submissions are triggered. + * - The generator uses a randomised model constrained to realistic percentage ranges. - The service is reactive and + * non-blocking: ingestion returns a {@link Mono} that completes after all async submissions are triggered. * */ @Slf4j @@ -65,7 +64,7 @@ private Mono>> generateIntradayPrices() { log.info("Generating Intraday Prices for Assets"); return assetUniverseApi.listAssetsWithResponseSpec( null, null, null, null, - List.of("market","latest_price"), + List.of("market", "latest_price"), null, null, "uuid,market,latest_price", null, null, null, null, null, @@ -84,7 +83,8 @@ private Mono>> generateIntradayPrices() { List requests = generateIntradayPricesForAsset(assetWithMarketAndLatestPrice); - log.debug("Generated intraday price requests: {}", requests); + log.debug("Generated intraday price requests: {}", requests.size()); + log.trace("Generated intraday price requests: {}", requests); if (requests.isEmpty()) { return Mono.empty(); @@ -142,8 +142,15 @@ private List generateIntradayPricesForAsset( AssetWithMarketAndLatestPrice assetWithMarketAndLatestPrice) { List requests = new ArrayList<>(); + // null check // Base previous close - Double previousClose = assetWithMarketAndLatestPrice.expandedLatestPrice().previousClosePrice(); + Double previousClose = Optional.ofNullable(assetWithMarketAndLatestPrice.expandedLatestPrice()) + .map(ExpandedLatestPrice::previousClosePrice).orElse(null); + if (previousClose == null) { + log.warn("The previous close price is null for asset {}, skipping intraday generation", + assetWithMarketAndLatestPrice.uuid()); + return requests; + } // Today OffsetDateTime todaySessionStarts = assetWithMarketAndLatestPrice.expandedMarket().todaySessionStarts(); @@ -171,7 +178,8 @@ private LocalTime intradayStartTime(OffsetDateTime offsetDateTime) { return offsetDateTime.toLocalTime().plusMinutes(ThreadLocalRandom.current().nextInt(1, 15)); } - private OASCreatePriceRequest createIntradayRequest(AssetWithMarketAndLatestPrice asset, Ohlc ohlc, Double previousClose, + private OASCreatePriceRequest createIntradayRequest(AssetWithMarketAndLatestPrice asset, Ohlc ohlc, + Double previousClose, OffsetDateTime dateTime) { return new OASCreatePriceRequest() @@ -189,10 +197,8 @@ private OASCreatePriceRequest createIntradayRequest(AssetWithMarketAndLatestPric * Deterministic randomised OHLC generator based on a previous close. * *

Algorithm constraints: - * - Total intraday range: 2–5% (implemented as 4–9 per mille in code). - * - Opening gap: ±0.1–0.3%. - * - Candle body: 0.2–1.2%. - * - Wicks are larger than body and distributed to respect direction. + * - Total intraday range: 2–5% (implemented as 4–9 per mille in code). - Opening gap: ±0.1–0.3%. - Candle body: + * 0.2–1.2%. - Wicks are larger than body and distributed to respect direction. * * @param previousClose the prior close price (must be positive) * @return a map with keys \"open\", \"high\", \"low\", \"close\" rounded to 6 decimal places @@ -248,6 +254,8 @@ private static double round6(double value) { .doubleValue(); } - public record Ohlc(double open, double high, double low, double close) {} + public record Ohlc(double open, double high, double low, double close) { + + } } diff --git a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java index 7effa2a65..52146eed3 100644 --- a/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java +++ b/stream-investment/investment-core/src/main/java/com/backbase/stream/investment/service/resttemplate/InvestmentRestNewsContentService.java @@ -26,7 +26,6 @@ import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -61,9 +60,9 @@ public Mono upsertContent(List contentEntries) { log.debug("Content upsert batch details: entries={}", contentEntries); return findEntriesNewContent(contentEntries).flatMap(this::upsertSingleEntry).doOnComplete( - () -> log.info("Content upsert batch completed successfully: totalEntriesProcessed={}", - contentEntries.size())).doOnError( - error -> log.error("Content upsert batch failed: totalEntries={}, errorType={}, errorMessage={}", + () -> log.info("Content upsert batch completed successfully: totalEntriesProcessed={}", + contentEntries.size())) + .doOnError(error -> log.error("Content upsert batch failed: totalEntries={}, errorType={}, errorMessage={}", contentEntries.size(), error.getClass().getSimpleName(), error.getMessage(), error)).then(); } @@ -78,27 +77,19 @@ private Mono upsertSingleEntry(MarketNewsEntry request) { log.debug("Processing content entry: title='{}', hasThumbnail={}", request.getTitle(), request.getThumbnailResource() != null); - return createNewEntry(request) + log.debug("Creating new content entry: title='{}', hasThumbnail={}", request.getTitle(), + request.getThumbnailResource() != null); + EntryCreateUpdateRequest createUpdateRequest = contentMapper.map(request); + log.debug("Content entry processing : {}", createUpdateRequest); + return Mono.defer(() -> Mono.just(contentApi.createContentEntry(createUpdateRequest))) + .flatMap(e -> addThumbnail(e, request.getThumbnailResource())) .doOnSuccess( - result -> log.info("Content entry processed successfully: title='{}', uuid={}", request.getTitle(), - result.getUuid()) - ).doOnError(throwable -> { - if (throwable instanceof WebClientResponseException ex) { - log.error( - "Content entry processing failed with API error: title='{}', httpStatus={}, errorResponse={}", - request.getTitle(), ex.getStatusCode(), ex.getResponseBodyAsString(), ex); - } else { - log.error( - "Content entry processing failed with unexpected error: title='{}', errorType={}, errorMessage={}", - request.getTitle(), throwable.getClass().getSimpleName(), throwable.getMessage(), throwable); - } - }) - .onErrorResume(error -> { - log.warn("Skipping failed content entry in batch: title='{}', decision=skip, reason={}", - request.getTitle(), - error.getMessage()); - return Mono.empty(); - }); + created -> log.info("Content entry created successfully: title='{}', uuid={}, thumbnailAttached={}", + request.getTitle(), created.getUuid(), request.getThumbnailResource() != null)) + .doOnError( + error -> log.error("Content entry creation failed: title='{}', errorType={}, errorMessage={}", + request.getTitle(), error.getClass().getSimpleName(), error.getMessage(), error)) + .onErrorResume(error -> Mono.empty()); } private Flux findEntriesNewContent(List contentEntries) { @@ -128,28 +119,6 @@ private Flux findEntriesNewContent(List conten return Flux.fromIterable(newEntries); } - /** - * Creates a new content entry. - * - * @param request The content data for the new entry - * @return Mono containing the created entry - */ - private Mono createNewEntry(MarketNewsEntry request) { - log.debug("Creating new content entry: title='{}', hasThumbnail={}", request.getTitle(), - request.getThumbnailResource() != null); - EntryCreateUpdateRequest createUpdateRequest = contentMapper.map(request); - log.debug("Content entry processing : {}", createUpdateRequest); - return Mono.defer(() -> Mono.just(contentApi.createContentEntry(createUpdateRequest))) - .flatMap(e -> addThumbnail(e, request.getThumbnailResource())) - .doOnSuccess( - created -> log.info("Content entry created successfully: title='{}', uuid={}, thumbnailAttached={}", - request.getTitle(), created.getUuid(), request.getThumbnailResource() != null)) - .doOnError( - error -> log.error("Content entry creation failed: title='{}', errorType={}, errorMessage={}", - request.getTitle(), error.getClass().getSimpleName(), error.getMessage(), error)) - .onErrorResume(error -> Mono.empty()); - } - private Mono addThumbnail(EntryCreateUpdate entry, Resource thumbnail) { UUID uuid = entry.getUuid(); @@ -162,9 +131,9 @@ private Mono addThumbnail(EntryCreateUpdate entry, Resource t getFileNameForLog(thumbnail)); return Mono.defer(() -> { - // create path and map variables - Map uriVariables = new HashMap<>(); - uriVariables.put("uuid", uuid); + // create path and map variables + Map uriVariables = new HashMap<>(); + uriVariables.put("uuid", uuid); MultiValueMap localVarQueryParams = new LinkedMultiValueMap<>(); HttpHeaders localVarHeaderParams = new HttpHeaders(); 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 c1d05b4b7..3825365e9 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 @@ -18,6 +18,7 @@ import com.backbase.stream.investment.InvestmentAssetData; import com.backbase.stream.investment.InvestmentAssetsTask; import com.backbase.stream.investment.RandomParam; +import com.backbase.stream.investment.service.AsyncTaskService; import com.backbase.stream.investment.service.InvestmentAssetPriceService; import com.backbase.stream.investment.service.InvestmentAssetUniverseService; import com.backbase.stream.investment.service.InvestmentIntradayAssetPriceService; @@ -55,6 +56,7 @@ class InvestmentAssetUniverseSagaTest { @Mock private InvestmentIntradayAssetPriceService investmentIntradayAssetPriceService; + private AsyncTaskService asyncTaskService; @Mock private InvestmentIngestionConfigurationProperties configurationProperties; @@ -68,6 +70,7 @@ void setUp() { assetUniverseService, investmentAssetPriceService, investmentIntradayAssetPriceService, + asyncTaskService, configurationProperties ); // Enable asset universe by default 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 d6ea7c960..ca16528a1 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 @@ -1,6 +1,5 @@ package com.backbase.stream.investment.service; -import com.backbase.investment.api.service.ApiClient; import com.backbase.investment.api.service.v1.AssetUniverseApi; import com.backbase.investment.api.service.v1.model.Asset; import com.backbase.investment.api.service.v1.model.Market; @@ -12,12 +11,8 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -30,13 +25,11 @@ class InvestmentAssetUniverseServiceTest { InvestmentAssetUniverseService service; AssetUniverseApi assetUniverseApi; - ApiClient apiClient; InvestmentRestAssetUniverseService investmentRestAssetUniverseService; @BeforeEach void setUp() { assetUniverseApi = Mockito.mock(AssetUniverseApi.class); - apiClient = Mockito.mock(ApiClient.class); investmentRestAssetUniverseService = Mockito.mock(InvestmentRestAssetUniverseService.class); service = new InvestmentAssetUniverseService(assetUniverseApi, investmentRestAssetUniverseService); @@ -91,30 +84,33 @@ void upsertMarket_otherError_propagates() { void getOrCreateAsset_assetExists() { com.backbase.stream.investment.Asset req = createAsset(); com.backbase.stream.investment.Asset asset = createAsset(); - String assetId = "ABC123_US_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) - .thenReturn(Mono.just(new Asset().isin("ABC123"))); - - WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); - Mockito.when(apiClient.invokeAPI( - ArgumentMatchers.anyString(), - ArgumentMatchers.eq(HttpMethod.POST), - ArgumentMatchers.anyMap(), - ArgumentMatchers.any(), - ArgumentMatchers.eq(req), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(ParameterizedTypeReference.class) - )).thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) + Asset existingAsset = new Asset() + .isin("ABC123") + .market("market") + .currency("USD"); + +// req.setIsin("ABC123"); +// req.setMarket("market"); +// req.setCurrency("USD"); + + Mockito.when(assetUniverseApi.getAsset( + ArgumentMatchers.anyString(), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any())) + .thenReturn(Mono.just(existingAsset)); + Mockito.when(investmentRestAssetUniverseService.patchAsset(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(Mono.just(asset)); + Mockito.when(investmentRestAssetUniverseService.createAsset( + ArgumentMatchers.any(), + ArgumentMatchers.any())) + .thenReturn(Mono.just(asset)); // This won't be called, but needed for switchIfEmpty evaluation StepVerifier.create(service.getOrCreateAsset(req, null)) - .expectNext(asset) + .expectNextMatches( + asset1 -> asset1.getIsin().equals(req.getIsin()) && asset1.getMarket().equals(req.getMarket()) + && asset1.getCurrency() + .equals(req.getCurrency())) .verifyComplete(); } @@ -130,10 +126,13 @@ private static com.backbase.stream.investment.Asset createAsset() { void getOrCreateAsset_assetNotFound_createsAsset() { com.backbase.stream.investment.Asset req = createAsset(); com.backbase.stream.investment.Asset createdAsset = createAsset(); - String assetId = "ABC123_US_USD"; - Resource logo = null; + String assetId = "ABC123_market_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) + Mockito.when(assetUniverseApi.getAsset( + ArgumentMatchers.eq(assetId), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull())) .thenReturn(Mono.error(WebClientResponseException.create( HttpStatus.NOT_FOUND.value(), "Not Found", @@ -141,22 +140,9 @@ void getOrCreateAsset_assetNotFound_createsAsset() { null, StandardCharsets.UTF_8 ))); - WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); - Mockito.when(apiClient.invokeAPI( - ArgumentMatchers.anyString(), - ArgumentMatchers.eq(HttpMethod.POST), - ArgumentMatchers.anyMap(), - ArgumentMatchers.any(), - ArgumentMatchers.eq(req), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(ParameterizedTypeReference.class) - )).thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) + Mockito.when(investmentRestAssetUniverseService.createAsset( + ArgumentMatchers.eq(req), + ArgumentMatchers.eq(Map.of()))) .thenReturn(Mono.just(createdAsset)); StepVerifier.create(service.getOrCreateAsset(req, Map.of())) @@ -167,27 +153,17 @@ void getOrCreateAsset_assetNotFound_createsAsset() { @Test void getOrCreateAsset_otherError_propagates() { com.backbase.stream.investment.Asset req = createAsset(); - String assetId = "ABC123_US_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) - .thenReturn(Mono.error(new RuntimeException("API error"))); - - WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); - Mockito.when(apiClient.invokeAPI( - ArgumentMatchers.anyString(), - ArgumentMatchers.eq(HttpMethod.POST), - ArgumentMatchers.anyMap(), - ArgumentMatchers.any(), - ArgumentMatchers.eq(req), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(ParameterizedTypeReference.class) - )).thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) + String assetId = "ABC123_market_USD"; + Mockito.when(assetUniverseApi.getAsset( + ArgumentMatchers.eq(assetId), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull())) .thenReturn(Mono.error(new RuntimeException("API error"))); + Mockito.when(investmentRestAssetUniverseService.createAsset( + ArgumentMatchers.eq(req), + ArgumentMatchers.isNull())) + .thenReturn(Mono.empty()); // This won't be called, but needed for switchIfEmpty evaluation StepVerifier.create(service.getOrCreateAsset(req, null)) .expectErrorMatches(e -> e instanceof RuntimeException && e.getMessage().equals("API error")) @@ -197,8 +173,12 @@ void getOrCreateAsset_otherError_propagates() { @Test void getOrCreateAsset_createAssetFails_propagates() { com.backbase.stream.investment.Asset req = createAsset(); - String assetId = "ABC123_US_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) + String assetId = "ABC123_market_USD"; + Mockito.when(assetUniverseApi.getAsset( + ArgumentMatchers.eq(assetId), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull())) .thenReturn(Mono.error(WebClientResponseException.create( HttpStatus.NOT_FOUND.value(), "Not Found", @@ -206,22 +186,9 @@ void getOrCreateAsset_createAssetFails_propagates() { null, StandardCharsets.UTF_8 ))); - WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); - Mockito.when(apiClient.invokeAPI( - ArgumentMatchers.anyString(), - ArgumentMatchers.eq(HttpMethod.POST), - ArgumentMatchers.anyMap(), - ArgumentMatchers.any(), - ArgumentMatchers.eq(req), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(ParameterizedTypeReference.class) - )).thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) + Mockito.when(investmentRestAssetUniverseService.createAsset( + ArgumentMatchers.eq(req), + ArgumentMatchers.isNull())) .thenReturn(Mono.error(new RuntimeException("Create asset failed"))); StepVerifier.create(service.getOrCreateAsset(req, null)) @@ -239,8 +206,12 @@ void getOrCreateAsset_nullRequest_returnsError() { @Test void getOrCreateAsset_emptyMonoFromCreateAsset() { com.backbase.stream.investment.Asset req = createAsset(); - String assetId = "ABC123_US_USD"; - Mockito.when(assetUniverseApi.getAsset(assetId, null, null, null)) + String assetId = "ABC123_market_USD"; + Mockito.when(assetUniverseApi.getAsset( + ArgumentMatchers.eq(assetId), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull(), + ArgumentMatchers.isNull())) .thenReturn(Mono.error(WebClientResponseException.create( HttpStatus.NOT_FOUND.value(), "Not Found", @@ -248,22 +219,9 @@ void getOrCreateAsset_emptyMonoFromCreateAsset() { null, StandardCharsets.UTF_8 ))); - WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); - Mockito.when(apiClient.invokeAPI( - ArgumentMatchers.anyString(), - ArgumentMatchers.eq(HttpMethod.POST), - ArgumentMatchers.anyMap(), - ArgumentMatchers.any(), - ArgumentMatchers.eq(req), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(), - ArgumentMatchers.any(ParameterizedTypeReference.class) - )).thenReturn(responseSpec); - Mockito.when(responseSpec.bodyToMono(ArgumentMatchers.any(ParameterizedTypeReference.class))) + Mockito.when(investmentRestAssetUniverseService.createAsset( + ArgumentMatchers.eq(req), + ArgumentMatchers.isNull())) .thenReturn(Mono.empty()); StepVerifier.create(service.getOrCreateAsset(req, null))