diff --git a/CHANGELOG.md b/CHANGELOG.md index edde86609..ee8047827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Restructured the response of the portfolio report endpoint (_X-ray_) - Refactored the create or update access dialog component to standalone - Upgraded `envalid` from version `8.0.0` to `8.1.0` - Upgraded `prisma` from version `6.14.0` to `6.15.0` diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 748cdceb8..d703cf604 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -655,8 +655,8 @@ export class PortfolioController { this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - for (const rule in report.xRay.rules) { - report.xRay.rules[rule] = null; + for (const category of report.xRay.categories) { + category.rules = null; } report.xRay.statistics = { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 1d010bcf7..f5b4ab1c6 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -50,6 +50,7 @@ import { PortfolioPerformanceResponse, PortfolioPosition, PortfolioReportResponse, + PortfolioReportRule, PortfolioSummary, UserSettings } from '@ghostfolio/common/interfaces'; @@ -1231,176 +1232,236 @@ export class PortfolioService { }) ).toNumber(); - const rules: PortfolioReportResponse['xRay']['rules'] = { - accountClusterRisk: - summary.activityCount > 0 - ? await this.rulesService.evaluate( - [ - new AccountClusterRiskCurrentInvestment( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - accounts - ), - new AccountClusterRiskSingleAccount( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - accounts - ) - ], - userSettings + const categories: PortfolioReportResponse['xRay']['categories'] = [ + { + key: 'liquidity', + name: this.i18nService.getTranslation({ + id: 'rule.liquidity.category', + languageCode: userSettings.language + }), + rules: await this.rulesService.evaluate( + [ + new BuyingPower( + this.exchangeRateDataService, + this.i18nService, + summary.cash, + userSettings.language ) - : undefined, - assetClassClusterRisk: - summary.activityCount > 0 - ? await this.rulesService.evaluate( - [ - new AssetClassClusterRiskEquity( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - Object.values(holdings) - ), - new AssetClassClusterRiskFixedIncome( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - Object.values(holdings) - ) - ], - userSettings - ) - : undefined, - currencyClusterRisk: - summary.activityCount > 0 - ? await this.rulesService.evaluate( - [ - new CurrencyClusterRiskBaseCurrencyCurrentInvestment( - this.exchangeRateDataService, - this.i18nService, - Object.values(holdings), - userSettings.language - ), - new CurrencyClusterRiskCurrentInvestment( - this.exchangeRateDataService, - this.i18nService, - Object.values(holdings), - userSettings.language - ) - ], - userSettings - ) - : undefined, - economicMarketClusterRisk: - summary.activityCount > 0 - ? await this.rulesService.evaluate( - [ - new EconomicMarketClusterRiskDevelopedMarkets( - this.exchangeRateDataService, - this.i18nService, - marketsTotalInBaseCurrency, - markets.developedMarkets.valueInBaseCurrency, - userSettings.language - ), - new EconomicMarketClusterRiskEmergingMarkets( - this.exchangeRateDataService, - this.i18nService, - marketsTotalInBaseCurrency, - markets.emergingMarkets.valueInBaseCurrency, - userSettings.language - ) - ], - userSettings + ], + userSettings + ) + }, + { + key: 'emergencyFund', + name: this.i18nService.getTranslation({ + id: 'rule.emergencyFund.category', + languageCode: userSettings.language + }), + rules: await this.rulesService.evaluate( + [ + new EmergencyFundSetup( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + this.getTotalEmergencyFund({ + userSettings, + emergencyFundHoldingsValueInBaseCurrency: + this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings }) + }).toNumber() ) - : undefined, - emergencyFund: await this.rulesService.evaluate( - [ - new EmergencyFundSetup( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - this.getTotalEmergencyFund({ - userSettings, - emergencyFundHoldingsValueInBaseCurrency: - this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings }) - }).toNumber() - ) - ], - userSettings - ), - fees: await this.rulesService.evaluate( - [ - new FeeRatioInitialInvestment( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - summary.committedFunds, - summary.fees - ) - ], - userSettings - ), - liquidity: await this.rulesService.evaluate( - [ - new BuyingPower( - this.exchangeRateDataService, - this.i18nService, - summary.cash, - userSettings.language - ) - ], - userSettings - ), - regionalMarketClusterRisk: - summary.activityCount > 0 - ? await this.rulesService.evaluate( - [ - new RegionalMarketClusterRiskAsiaPacific( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - marketsAdvancedTotalInBaseCurrency, - marketsAdvanced.asiaPacific.valueInBaseCurrency - ), - new RegionalMarketClusterRiskEmergingMarkets( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - marketsAdvancedTotalInBaseCurrency, - marketsAdvanced.emergingMarkets.valueInBaseCurrency - ), - new RegionalMarketClusterRiskEurope( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - marketsAdvancedTotalInBaseCurrency, - marketsAdvanced.europe.valueInBaseCurrency - ), - new RegionalMarketClusterRiskJapan( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - marketsAdvancedTotalInBaseCurrency, - marketsAdvanced.japan.valueInBaseCurrency - ), - new RegionalMarketClusterRiskNorthAmerica( - this.exchangeRateDataService, - this.i18nService, - userSettings.language, - marketsAdvancedTotalInBaseCurrency, - marketsAdvanced.northAmerica.valueInBaseCurrency - ) - ], - userSettings + ], + userSettings + ) + }, + { + key: 'currencyClusterRisk', + name: this.i18nService.getTranslation({ + id: 'rule.currencyClusterRisk.category', + languageCode: userSettings.language + }), + rules: + summary.activityCount > 0 + ? await this.rulesService.evaluate( + [ + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + this.exchangeRateDataService, + this.i18nService, + Object.values(holdings), + userSettings.language + ), + new CurrencyClusterRiskCurrentInvestment( + this.exchangeRateDataService, + this.i18nService, + Object.values(holdings), + userSettings.language + ) + ], + userSettings + ) + : undefined + }, + { + key: 'assetClassClusterRisk', + name: this.i18nService.getTranslation({ + id: 'rule.assetClassClusterRisk.category', + languageCode: userSettings.language + }), + rules: + summary.activityCount > 0 + ? await this.rulesService.evaluate( + [ + new AssetClassClusterRiskEquity( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + Object.values(holdings) + ), + new AssetClassClusterRiskFixedIncome( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + Object.values(holdings) + ) + ], + userSettings + ) + : undefined + }, + { + key: 'accountClusterRisk', + name: this.i18nService.getTranslation({ + id: 'rule.accountClusterRisk.category', + languageCode: userSettings.language + }), + rules: + summary.activityCount > 0 + ? await this.rulesService.evaluate( + [ + new AccountClusterRiskCurrentInvestment( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + accounts + ), + new AccountClusterRiskSingleAccount( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + accounts + ) + ], + userSettings + ) + : undefined + }, + { + key: 'economicMarketClusterRisk', + name: this.i18nService.getTranslation({ + id: 'rule.economicMarketClusterRisk.category', + languageCode: userSettings.language + }), + rules: + summary.activityCount > 0 + ? await this.rulesService.evaluate( + [ + new EconomicMarketClusterRiskDevelopedMarkets( + this.exchangeRateDataService, + this.i18nService, + marketsTotalInBaseCurrency, + markets.developedMarkets.valueInBaseCurrency, + userSettings.language + ), + new EconomicMarketClusterRiskEmergingMarkets( + this.exchangeRateDataService, + this.i18nService, + marketsTotalInBaseCurrency, + markets.emergingMarkets.valueInBaseCurrency, + userSettings.language + ) + ], + userSettings + ) + : undefined + }, + { + key: 'regionalMarketClusterRisk', + name: this.i18nService.getTranslation({ + id: 'rule.regionalMarketClusterRisk.category', + languageCode: userSettings.language + }), + rules: + summary.activityCount > 0 + ? await this.rulesService.evaluate( + [ + new RegionalMarketClusterRiskAsiaPacific( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.asiaPacific.valueInBaseCurrency + ), + new RegionalMarketClusterRiskEmergingMarkets( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.emergingMarkets.valueInBaseCurrency + ), + new RegionalMarketClusterRiskEurope( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.europe.valueInBaseCurrency + ), + new RegionalMarketClusterRiskJapan( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.japan.valueInBaseCurrency + ), + new RegionalMarketClusterRiskNorthAmerica( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.northAmerica.valueInBaseCurrency + ) + ], + userSettings + ) + : undefined + }, + { + key: 'fees', + name: this.i18nService.getTranslation({ + id: 'rule.fees.category', + languageCode: userSettings.language + }), + rules: await this.rulesService.evaluate( + [ + new FeeRatioInitialInvestment( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + summary.committedFunds, + summary.fees ) - : undefined - }; + ], + userSettings + ) + } + ]; return { xRay: { - rules, - statistics: this.getReportStatistics(rules) + categories, + statistics: this.getReportStatistics( + categories.flatMap(({ rules }) => { + return rules ?? []; + }) + ) } }; } @@ -1822,7 +1883,7 @@ export class PortfolioService { } private getReportStatistics( - evaluatedRules: PortfolioReportResponse['xRay']['rules'] + evaluatedRules: PortfolioReportRule[] ): PortfolioReportResponse['xRay']['statistics'] { const rulesActiveCount = Object.values(evaluatedRules) .flat() diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 7f6d964f5..48d1658aa 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -22,7 +22,6 @@ export class RulesService { return { evaluation, value, - categoryName: rule.getCategoryName(), configuration: rule.getConfiguration(), isActive: true, key: rule.getKey(), @@ -30,7 +29,6 @@ export class RulesService { }; } else { return { - categoryName: rule.getCategoryName(), isActive: false, key: rule.getKey(), name: rule.getName() diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts index 6fcbd2d38..9811a6564 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts @@ -4,6 +4,7 @@ import { } from '@ghostfolio/common/interfaces'; export interface IRuleSettingsDialogParams { + categoryName: string; rule: PortfolioReportRule; settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index f5903e6d5..9fdc0cd57 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -1,4 +1,4 @@ -
{{ data.rule.categoryName }} › {{ data.rule.name }}
+
{{ data.categoryName }} › {{ data.rule.name }}
@if ( diff --git a/apps/client/src/app/components/rule/rule.component.ts b/apps/client/src/app/components/rule/rule.component.ts index 624338915..f9d7c8664 100644 --- a/apps/client/src/app/components/rule/rule.component.ts +++ b/apps/client/src/app/components/rule/rule.component.ts @@ -37,6 +37,7 @@ import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-setti standalone: false }) export class RuleComponent implements OnInit { + @Input() categoryName: string; @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() rule: PortfolioReportRule; @@ -69,6 +70,7 @@ export class RuleComponent implements OnInit { const dialogRef = this.dialog.open(GfRuleSettingsDialogComponent, { data: { rule, + categoryName: this.categoryName, settings: this.settings } as IRuleSettingsDialogParams, width: this.deviceType === 'mobile' ? '100vw' : '50rem' diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index 28343673d..d0cf7ece5 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -8,6 +8,7 @@ @if (rules !== null && rules !== undefined) { @for (rule of rules; track rule.key) { }
-
-

- Liquidity - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Emergency Fund - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Currency Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Asset Class Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Account Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Economic Market Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Regional Market Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Fees - @if (user?.subscription?.type === 'Basic') { - - } -

- -
+ @for (category of categories; track category.key) { +
+

+ {{ category.name }} + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+ } @if (inactiveRules?.length > 0) {

Inactive

diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts index 0c8637b05..c0578b5d7 100644 --- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts @@ -36,18 +36,15 @@ import { Subject, takeUntil } from 'rxjs'; templateUrl: './x-ray-page.component.html' }) export class GfXRayPageComponent { - public accountClusterRiskRules: PortfolioReportRule[]; - public assetClassClusterRiskRules: PortfolioReportRule[]; - public currencyClusterRiskRules: PortfolioReportRule[]; - public economicMarketClusterRiskRules: PortfolioReportRule[]; - public emergencyFundRules: PortfolioReportRule[]; - public feeRules: PortfolioReportRule[]; + public categories: { + key: string; + name: string; + rules: PortfolioReportRule[]; + }[]; public hasImpersonationId: boolean; public hasPermissionToUpdateUserSettings: boolean; public inactiveRules: PortfolioReportRule[]; public isLoading = false; - public liquidityRules: PortfolioReportRule[]; - public regionalMarketClusterRiskRules: PortfolioReportRule[]; public statistics: PortfolioReportResponse['xRay']['statistics']; public user: User; @@ -116,50 +113,11 @@ export class GfXRayPageComponent { this.dataService .fetchPortfolioReport() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ xRay: { rules, statistics } }) => { - this.inactiveRules = this.mergeInactiveRules(rules); + .subscribe(({ xRay: { categories, statistics } }) => { + this.categories = categories; + this.inactiveRules = this.mergeInactiveRules(categories); this.statistics = statistics; - this.accountClusterRiskRules = - rules['accountClusterRisk']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.assetClassClusterRiskRules = - rules['assetClassClusterRisk']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.currencyClusterRiskRules = - rules['currencyClusterRisk']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.economicMarketClusterRiskRules = - rules['economicMarketClusterRisk']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.emergencyFundRules = - rules['emergencyFund']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.feeRules = - rules['fees']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.liquidityRules = - rules['liquidity']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - - this.regionalMarketClusterRiskRules = - rules['regionalMarketClusterRisk']?.filter(({ isActive }) => { - return isActive; - }) ?? null; - this.isLoading = false; this.changeDetectorRef.markForCheck(); @@ -167,20 +125,14 @@ export class GfXRayPageComponent { } private mergeInactiveRules( - rules: PortfolioReportResponse['xRay']['rules'] + categories: PortfolioReportResponse['xRay']['categories'] ): PortfolioReportRule[] { - let inactiveRules: PortfolioReportRule[] = []; - - for (const category in rules) { - const rulesArray = rules[category] || []; - - inactiveRules = inactiveRules.concat( - rulesArray.filter(({ isActive }) => { + return categories.flatMap(({ rules }) => { + return ( + rules?.filter(({ isActive }) => { return !isActive; - }) + }) ?? [] ); - } - - return inactiveRules; + }); } } diff --git a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts index 4df7a8eac..0296606b8 100644 --- a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts @@ -1,5 +1,4 @@ export interface PortfolioReportRule { - categoryName: string; configuration?: { threshold?: { max: number; diff --git a/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts index 985a42311..8146b6086 100644 --- a/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts @@ -2,7 +2,11 @@ import { PortfolioReportRule } from '../portfolio-report-rule.interface'; export interface PortfolioReportResponse { xRay: { - rules: { [group: string]: PortfolioReportRule[] }; + categories: { + key: string; + name: string; + rules: PortfolioReportRule[]; + }[]; statistics: { rulesActiveCount: number; rulesFulfilledCount: number;