Skip to content
Open
129 changes: 129 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copilot Instructions — Backbase Stream Services

## Project Basics

- **Java 21 / Spring Boot** multi-module Maven mono-repo.
- **Fully reactive** — use `Mono`/`Flux` (Project Reactor) for all service calls. Never use blocking calls in production code.
- Parent POM: `com.backbase.buildingblocks:backbase-parent:20.0.0`. BOM: `backbase-bom`.

## Core Architecture Pattern

Every ingestion domain uses the **Saga + StreamTask** pattern:

1. Create a `StreamTask` subclass wrapping the aggregate data with a `getData()` accessor.
2. Create a Saga class implementing `StreamTaskExecutor<T>` with `executeTask(T)` returning `Mono<T>`.
3. Chain saga steps via `.flatMap()`. Each step receives and returns the task.
4. Track progress with `task.info(entity, operation, result, externalId, internalId, message)` and failures with `task.error(...)`.

```java
// Canonical saga pattern — see LegalEntitySaga.executeTask()
@Override
public Mono<MyTask> executeTask(MyTask streamTask) {
return stepOne(streamTask)
.flatMap(this::stepTwo)
.flatMap(this::stepThree);
}
```

## Code Style & Conventions

- **Lombok everywhere**: annotate classes with `@Slf4j`, `@RequiredArgsConstructor`, `@Data`. Do not write manual getters/setters/constructors when Lombok suffices. See `lombok.config` for project settings (`addConstructorProperties=true`).
- **MapStruct for model mapping**: define `@Mapper` interfaces to convert between DBS API models, stream models, and composition API models. Access via `Mappers.getMapper(MyMapper.class)` or Spring injection with `componentModel = "spring"`.
- **No component scanning**: modules wire beans via `@Configuration` + `@Import(...)` chains. When adding a new service bean, register it in the module's `*Configuration` class and `@Import` it where needed.
- **DBS clients extend `CompositeApiClientConfig`** with a `@ConfigurationProperties("backbase.communication.services.<name>")` prefix. All are assembled in `DbsApiClientsAutoConfiguration`.
- **Error handling in sagas**: catch `WebClientResponseException` specifically, log the response body, record in task history, then wrap in `StreamTaskException`.

```java
.onErrorResume(throwable -> {
if (throwable instanceof WebClientResponseException wce) {
task.error(ENTITY, OP, ERROR, extId, null, wce, wce.getResponseBodyAsString(), MESSAGE);
} else {
task.error(ENTITY, OP, ERROR, extId, null, throwable, throwable.getMessage(), MESSAGE);
}
return Mono.error(new StreamTaskException(task, throwable, MESSAGE));
})
```

## Generated Code — Do Not Edit

- `stream-dbs-clients` generates reactive WebClient wrappers from OpenAPI specs via `boat-maven-plugin`. **Never hand-edit files under `target/generated-sources/`.**
- Stream API models in `api/` and `stream-compositions/api/` are also generated. Edit the `.yaml` spec, not the Java output.

## Build Commands

```bash
# Fast full build (no tests)
mvn clean install -DskipTest -Dmaven.test.skip=true

# Single module with upstream dependencies
mvn clean install -pl stream-legal-entity/legal-entity-core -am

# Run a composition service locally
cd stream-compositions/services/legal-entity-composition-service
mvn spring-boot:run -Dspring-boot.run.profiles=local
```

## Testing Patterns

- **JUnit 5 + Mockito + reactor-test**. Use `@ExtendWith(MockitoExtension.class)`.
- Mock DBS API clients (`@Mock LimitsServiceApi limitsApi`), stub with `when(...).thenReturn(Mono.just(...))`.
- Verify reactive chains with `StepVerifier` or `.block()` in tests.
- **Configuration tests** use `ApplicationContextRunner` to validate Spring wiring without a full app context:

```java
@SpringJUnitConfig
class MyConfigTest {
ApplicationContextRunner contextRunner = new ApplicationContextRunner();

@Test
void configurationTest() {
contextRunner
.withBean(WebClientAutoConfiguration.class)
.withBean(DbsApiClientsAutoConfiguration.class)
.withBean(InterServiceWebClientConfiguration.class)
.withUserConfiguration(MyConfiguration.class)
.run(context -> assertThat(context).hasSingleBean(MyService.class));
}
}
```

## Key Directories

| What | Where |
|---|---|
| SDK framework (StreamTask, UnitOfWork) | `stream-sdk/stream-parent/stream-worker/` |
| DBS client configs | `stream-dbs-clients/src/main/java/.../clients/config/` |
| Legal Entity Saga (reference saga) | `stream-legal-entity/legal-entity-core/src/.../LegalEntitySaga.java` |
| Shared ingestion models | `stream-models/legal-entity-model/` |
| Composition services (deployable apps) | `stream-compositions/services/` |
| OpenAPI specs | `api/`, `stream-compositions/api/` |

## Skills Reference

Before generating or modifying code, **read the relevant skill file** from `.github/skills/` and follow its rules exactly. Each skill is authoritative for its domain.

| Task / Domain | Skill File to Read |
|---|---|
| Build commands, adding modules, POM hierarchy, versioning | `.github/skills/stream-build-and-modules/SKILL.md` |
| Creating or modifying a Saga / StreamTask | `.github/skills/stream-saga-development/SKILL.md` |
| Reactive `Mono`/`Flux` pipelines, error handling, parallel ops | `.github/skills/stream-reactive-patterns/SKILL.md` |
| Spring `@Configuration` / `@Import` / `@Bean` wiring | `.github/skills/stream-spring-wiring/SKILL.md` |
| Adding or configuring a DBS API client (`CompositeApiClientConfig`) | `.github/skills/stream-dbs-client-config/SKILL.md` |
| MapStruct mappers (`@Mapper`, `@Mapping`, model conversion) | `.github/skills/stream-mapstruct-mapping/SKILL.md` |
| Composition services (REST endpoints, integration APIs, events) | `.github/skills/stream-composition-services/SKILL.md` |
| Writing unit / integration tests (JUnit 5, Mockito, StepVerifier) | `.github/skills/stream-testing-patterns/SKILL.md` |

**Rules for code generation:**

1. **Identify** which skill(s) apply to the task being performed.
2. **Read** the matching `SKILL.md` file(s) in full before writing any code.
3. **Follow** every rule listed in the `## Rules` section of each skill — they override general conventions when they conflict.
4. When multiple skills apply (e.g., new saga + new DBS client + tests), read all relevant skill files.
5. Do **not** skip skill files for "simple" changes — even small edits must conform to the skill's patterns.

## PR Checklist

- Update `CHANGELOG.md` under the next version heading.
- Use `hotfix/` branch prefix for patch-level changes; standard branches bump MINOR.
- Support branches use `.x` suffix (e.g., `support/2.45.x`).

2 changes: 1 addition & 1 deletion stream-investment/investment-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<properties>
<checkstyle.disable.checks>true</checkstyle.disable.checks>
<investment-service-api.version>1.3.0</investment-service-api.version>
<investment-service-api.version>1.4.1</investment-service-api.version>
</properties>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

Check warning on line 5 in stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/IngestConfigProperties.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'org.springframework.context.annotation.Configuration'.

See more on https://sonarcloud.io/project/issues?id=com.backbase.stream%3Astream-services&issues=AZ1S_-6ZFUyP9qXTK4Si&open=AZ1S_-6ZFUyP9qXTK4Si&pullRequest=590

/**
* Fine-grained configuration properties for {@code InvestmentPortfolioService}.
Expand Down Expand Up @@ -33,15 +34,16 @@
* </pre>
*/
@Data
@ConfigurationProperties(prefix = "backbase.bootstrap.ingestions.investment.service")
public class InvestmentIngestProperties {
@ConfigurationProperties(prefix = "backbase.bootstrap.ingestions.investment.config")
public class IngestConfigProperties {

public static final double DEFAULT_INIT_CASH = 10_000d;

private PortfolioConfig portfolio = new PortfolioConfig();
private AllocationConfig allocation = new AllocationConfig();
private DepositConfig deposit = new DepositConfig();
private AssetConfig asset = new AssetConfig();
private AssessmentConfig assessment = new AssessmentConfig();

// -------------------------------------------------------------------------
// Portfolio
Expand Down Expand Up @@ -126,5 +128,12 @@

}

@Data
public static class AssessmentConfig {

private int riskQuestionsPageSize = 100;

}

}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.backbase.investment.api.service.v1.PaymentsApi;
import com.backbase.investment.api.service.v1.PortfolioApi;
import com.backbase.investment.api.service.v1.PortfolioTradingAccountsApi;
import com.backbase.investment.api.service.v1.RiskAssessmentApi;
import com.backbase.stream.clients.config.CompositeApiClientConfig;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -146,6 +147,12 @@ public CurrencyApi currencyApi(ApiClient investmentApiClient) {
return new CurrencyApi(investmentApiClient);
}

@Bean
@ConditionalOnMissingBean
public RiskAssessmentApi riskAssessmentApi(ApiClient investmentApiClient) {
return new RiskAssessmentApi(investmentApiClient);
}

@Bean
@ConditionalOnMissingBean
public AsyncBulkGroupsApi asyncBulkGroupsApi(ApiClient investmentApiClient) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* <p>Controls which high-level ingestion flows are enabled. Service-level tuning
* (portfolio currencies, deposit provider, allocation assets, etc.) lives in
* {@link InvestmentIngestProperties}.
* {@link IngestConfigProperties}.
*/
@Data
@ConditionalOnBean(InvestmentServiceConfiguration.class)
Expand All @@ -20,6 +20,7 @@ public class InvestmentIngestionConfigurationProperties {
private boolean contentEnabled = true;
private boolean assetUniverseEnabled = true;
private boolean wealthEnabled = true;
private IngestConfigProperties config = new IngestConfigProperties();


}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.backbase.stream.configuration;

import com.backbase.investment.api.service.sync.v1.AssetUniverseApi;
import com.backbase.investment.api.service.sync.v1.ContentApi;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestAssetUniverseService;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestDocumentContentService;
Expand Down Expand Up @@ -92,7 +91,7 @@ public InvestmentRestDocumentContentService investmentRestContentDocumentService
@Bean
public InvestmentRestAssetUniverseService investmentRestAssetUniverseService(
com.backbase.investment.api.service.sync.ApiClient restInvestmentApiClient,
InvestmentIngestProperties portfolioProperties) {
IngestConfigProperties portfolioProperties) {
return new InvestmentRestAssetUniverseService(restInvestmentApiClient, portfolioProperties);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.backbase.investment.api.service.v1.PaymentsApi;
import com.backbase.investment.api.service.v1.PortfolioApi;
import com.backbase.investment.api.service.v1.PortfolioTradingAccountsApi;
import com.backbase.investment.api.service.v1.RiskAssessmentApi;
import com.backbase.stream.clients.autoconfigure.DbsApiClientsAutoConfiguration;
import com.backbase.stream.investment.saga.InvestmentAssetUniverseSaga;
import com.backbase.stream.investment.saga.InvestmentContentSaga;
Expand All @@ -26,6 +27,8 @@
import com.backbase.stream.investment.service.InvestmentModelPortfolioService;
import com.backbase.stream.investment.service.InvestmentPortfolioAllocationService;
import com.backbase.stream.investment.service.InvestmentPortfolioService;
import com.backbase.stream.investment.service.InvestmentRiskAssessmentService;
import com.backbase.stream.investment.service.InvestmentRiskQuestionaryService;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestAssetUniverseService;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestDocumentContentService;
import com.backbase.stream.investment.service.resttemplate.InvestmentRestNewsContentService;
Expand All @@ -42,7 +45,7 @@
})
@EnableConfigurationProperties({
InvestmentIngestionConfigurationProperties.class,
InvestmentIngestProperties.class
IngestConfigProperties.class
})
@RequiredArgsConstructor
@Configuration
Expand All @@ -56,13 +59,14 @@

@Bean
public CustomIntegrationApiService customIntegrationApiService(ApiClient apiClient) {
return new CustomIntegrationApiService(apiClient);

Check warning on line 62 in stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated class, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=com.backbase.stream%3Astream-services&issues=AZ1S_-4GFUyP9qXTK4Sg&open=AZ1S_-4GFUyP9qXTK4Sg&pullRequest=590
}

@Bean
public InvestmentPortfolioService investmentPortfolioService(PortfolioApi portfolioApi,
InvestmentProductsApi investmentProductsApi, PaymentsApi paymentsApi, PortfolioTradingAccountsApi portfolioTradingAccountsApi,
InvestmentIngestProperties portfolioProperties) {
InvestmentProductsApi investmentProductsApi, PaymentsApi paymentsApi,
PortfolioTradingAccountsApi portfolioTradingAccountsApi,
IngestConfigProperties portfolioProperties) {
return new InvestmentPortfolioService(investmentProductsApi, portfolioApi, paymentsApi,
portfolioTradingAccountsApi, portfolioProperties);
}
Expand Down Expand Up @@ -97,7 +101,7 @@
@Bean
public InvestmentPortfolioAllocationService investmentPortfolioAllocationService(AllocationsApi allocationsApi,
AssetUniverseApi assetUniverseApi, InvestmentApi investmentApi,
CustomIntegrationApiService customIntegrationApiService, InvestmentIngestProperties portfolioProperties) {
CustomIntegrationApiService customIntegrationApiService, IngestConfigProperties portfolioProperties) {

Check warning on line 104 in stream-investment/investment-core/src/main/java/com/backbase/stream/configuration/InvestmentServiceConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated class, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=com.backbase.stream%3Astream-services&issues=AZ1S_-4GFUyP9qXTK4Sh&open=AZ1S_-4GFUyP9qXTK4Sh&pullRequest=590
return new InvestmentPortfolioAllocationService(allocationsApi, assetUniverseApi, investmentApi,
customIntegrationApiService, portfolioProperties);
}
Expand All @@ -107,15 +111,28 @@
return new InvestmentCurrencyService(currencyApi);
}

@Bean
public InvestmentRiskAssessmentService investmentRiskAssessmentService(RiskAssessmentApi riskAssessmentApi) {
return new InvestmentRiskAssessmentService(riskAssessmentApi);
}

@Bean
public InvestmentRiskQuestionaryService investmentRiskQuestionaryService(RiskAssessmentApi riskAssessmentApi,
IngestConfigProperties portfolioProperties) {
return new InvestmentRiskQuestionaryService(riskAssessmentApi, portfolioProperties);
}

@Bean
public InvestmentSaga investmentSaga(InvestmentClientService investmentClientService,
InvestmentRiskAssessmentService investmentRiskAssessmentService,
InvestmentRiskQuestionaryService investmentRiskQuestionaryService,
InvestmentPortfolioService investmentPortfolioService,
InvestmentModelPortfolioService investmentModelPortfolioService,
InvestmentPortfolioAllocationService investmentPortfolioAllocationService, AsyncTaskService asyncTaskService,
InvestmentIngestionConfigurationProperties coreConfigurationProperties) {
return new InvestmentSaga(investmentClientService, investmentPortfolioService,
investmentPortfolioAllocationService, investmentModelPortfolioService, asyncTaskService,
coreConfigurationProperties);
return new InvestmentSaga(investmentClientService, investmentRiskAssessmentService,
investmentRiskQuestionaryService, investmentPortfolioService, investmentPortfolioAllocationService,
investmentModelPortfolioService, asyncTaskService, coreConfigurationProperties);
}

@Bean
Expand All @@ -126,7 +143,7 @@
InvestmentCurrencyService investmentCurrencyService,
AsyncTaskService asyncTaskService,
InvestmentIngestionConfigurationProperties coreConfigurationProperties,
InvestmentIngestProperties portfolioProperties) {
IngestConfigProperties portfolioProperties) {
return new InvestmentAssetUniverseSaga(investmentAssetUniverseService, investmentAssetPriceService,
investmentIntradayAssetPriceService, investmentCurrencyService, asyncTaskService,
coreConfigurationProperties, portfolioProperties);
Expand All @@ -137,7 +154,8 @@
InvestmentRestNewsContentService investmentRestNewsContentService,
InvestmentRestDocumentContentService investmentRestDocumentContentService,
InvestmentIngestionConfigurationProperties coreConfigurationProperties) {
return new InvestmentContentSaga(investmentRestNewsContentService, investmentRestDocumentContentService, coreConfigurationProperties);
return new InvestmentContentSaga(investmentRestNewsContentService, investmentRestDocumentContentService,
coreConfigurationProperties);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class Asset implements AssetKey {
private String market;
private String currency;
@JsonProperty("extra_data")
private Map<String, Object> extraData;
private Map<String, String> extraData;
@JsonProperty("asset_type")
private AssetTypeEnum assetType;
private List<String> categories;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ClientUser {
private String internalUserId;
private String externalUserId;
private String legalEntityId;
private String userName;


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.backbase.investment.api.service.v1.model.PortfolioProduct;
import com.backbase.stream.investment.model.InvestmentPortfolio;
import com.backbase.stream.investment.model.InvestmentPortfolioTradingAccount;
import com.backbase.stream.investment.model.RiskQuestion;
import com.backbase.stream.investment.model.UserRiskAssessment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand All @@ -28,6 +30,8 @@ public class InvestmentData implements InvestmentDataValue {
private InvestmentAssetData investmentAssetData;
private List<InvestmentPortfolio> portfolios;
private List<InvestmentPortfolioTradingAccount> investmentPortfolioTradingAccounts;
private List<RiskQuestion> riskQuestions;
private List<UserRiskAssessment> riskAssessments;

public Map<String, List<UUID>> getClientsByLeExternalId() {
Map<String, List<UUID>> clientsByLeExternalId = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class ContentDocumentEntry {
private List<String> tags;
private List<ModelAsset> assets;
@JsonProperty("extra_data")
private Map<String, Object> extraData;
private Map<String, String> extraData;
private String document;
private Resource documentResource;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.backbase.stream.investment.model;

import java.util.UUID;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class QuestionChoice {

private UUID uuid;
private String code;
private String description;
private Double score;
private Boolean suitable;
private Integer order;

}
Loading
Loading