diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ce8700b..043680fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Included data provider errors in API response + ### Fixed - Improved the account calculations diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index 29550b43a..48e6038f3 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -1,8 +1,7 @@ -import { TimelinePosition } from '@ghostfolio/common/interfaces'; +import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import Big from 'big.js'; -export interface CurrentPositions { - hasErrors: boolean; +export interface CurrentPositions extends ResponseError { positions: TimelinePosition[]; grossPerformance: Big; grossPerformancePercentage: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts index 8906431fb..5dddc53fd 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts @@ -66,6 +66,7 @@ describe('PortfolioCalculatorNew', () => { expect(currentPositions).toEqual({ currentValue: new Big('0'), + errors: [], grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), hasErrors: false, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts index 230fb04ab..de0f1f0bf 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts @@ -55,6 +55,7 @@ describe('PortfolioCalculatorNew', () => { expect(currentPositions).toEqual({ currentValue: new Big('297.8'), + errors: [], grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), hasErrors: false, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 8df16f785..972d4db3c 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -1,7 +1,11 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; -import { TimelinePosition } from '@ghostfolio/common/interfaces'; +import { + ResponseError, + TimelinePosition, + UniqueAsset +} from '@ghostfolio/common/interfaces'; import { Logger } from '@nestjs/common'; import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; @@ -232,6 +236,8 @@ export class PortfolioCalculatorNew { const positions: TimelinePosition[] = []; let hasAnySymbolMetricsErrors = false; + const errors: ResponseError['errors'] = []; + for (const item of lastTransactionPoint.items) { const marketValue = marketSymbolMap[todayString]?.[item.symbol]; @@ -272,12 +278,17 @@ export class PortfolioCalculatorNew { symbol: item.symbol, transactionCount: item.transactionCount }); + + if (hasErrors) { + errors.push({ dataSource: item.dataSource, symbol: item.symbol }); + } } const overall = this.calculateOverallPerformance(positions, initialValues); return { ...overall, + errors, positions, hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 5d15aa423..fd11334d9 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -14,7 +14,7 @@ import { PortfolioChart, PortfolioDetails, PortfolioInvestments, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, PortfolioSummary @@ -204,10 +204,11 @@ export class PortfolioController { @Get('performance') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPerformance( @Headers('impersonation-id') impersonationId: string, @Query('range') range - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const performanceInformation = await this.portfolioServiceStrategy .get() .getPerformance(impersonationId, range); diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index d90182c39..e078c5410 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -24,7 +24,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, PortfolioDetails, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioReport, PortfolioSummary, Position, @@ -730,7 +730,7 @@ export class PortfolioServiceNew { public async getPerformance( aImpersonationId: string, aDateRange: DateRange = 'max' - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const { portfolioOrders, transactionPoints } = @@ -776,6 +776,7 @@ export class PortfolioServiceNew { currentPositions.netPerformancePercentage.toNumber(); return { + errors: currentPositions.errors, hasErrors: currentPositions.hasErrors || hasErrors, performance: { currentGrossPerformance, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index eb1a463ee..108c55d27 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -25,7 +25,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, PortfolioDetails, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioReport, PortfolioSummary, Position, @@ -712,7 +712,7 @@ export class PortfolioService { public async getPerformance( aImpersonationId: string, aDateRange: DateRange = 'max' - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( diff --git a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts index 720f02b67..2aeb895fe 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -41,6 +41,14 @@ export class TransformDataSourceInResponseInterceptor data.dataSource = encodeDataSource(data.dataSource); } + if (data.errors) { + for (const error of data.errors) { + if (error.dataSource) { + error.dataSource = encodeDataSource(error.dataSource); + } + } + } + if (data.holdings) { for (const symbol of Object.keys(data.holdings)) { if (data.holdings[symbol].dataSource) { diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index d6ae7e2b0..f959fca77 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -7,7 +7,11 @@ import { } from '@ghostfolio/client/services/settings-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { defaultDateRangeOptions } from '@ghostfolio/common/config'; -import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces'; +import { + PortfolioPerformance, + UniqueAsset, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; @@ -24,6 +28,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { public dateRange: DateRange; public dateRangeOptions = defaultDateRangeOptions; public deviceType: string; + public errors: UniqueAsset[]; public hasError: boolean; public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; @@ -126,6 +131,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { .fetchPortfolioPerformance({ range: this.dateRange }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { + this.errors = response.errors; this.hasError = response.hasErrors; this.performance = response.performance; this.isLoadingPerformance = false; diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index 82f45ed57..7f804d990 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -28,6 +28,7 @@ class="pb-4" [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" + [errors]="errors" [hasError]="hasError" [isAllTimeHigh]="isAllTimeHigh" [isAllTimeLow]="isAllTimeLow" diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index cd61f901e..5601e42cc 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -7,6 +7,7 @@ ? 'Sorry! Our data provider partner is experiencing the hiccups.' : '' " + (click)="errors?.length > 0 && onShowErrors()" > { + return `${error.symbol} (${error.dataSource})`; + }); + + alert(errorMessageParts.join('\n')); + } } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index fac56a1f2..c61730d3c 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -24,9 +24,11 @@ import { PortfolioDetails, PortfolioInvestments, PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, PortfolioSummary, + UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -188,13 +190,13 @@ export class DataService { }); } - public fetchPortfolioPerformance(aParams: { [param: string]: any }) { - return this.http.get<{ - hasErrors: boolean; - performance: PortfolioPerformance; - }>('/api/portfolio/performance', { - params: aParams - }); + public fetchPortfolioPerformance(params: { [param: string]: any }) { + return this.http.get( + '/api/portfolio/performance', + { + params + } + ); } public fetchPortfolioPublic(aId: string) { diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index feeaaabd4..d2ad50742 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -21,6 +21,8 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface'; import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioSummary } from './portfolio-summary.interface'; import { Position } from './position.interface'; +import { ResponseError } from './responses/errors.interface'; +import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import { TimelinePosition } from './timeline-position.interface'; import { UniqueAsset } from './unique-asset.interface'; import { UserSettings } from './user-settings.interface'; @@ -43,12 +45,14 @@ export { PortfolioItem, PortfolioOverview, PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPosition, PortfolioPublicDetails, PortfolioReport, PortfolioReportRule, PortfolioSummary, Position, + ResponseError, TimelinePosition, UniqueAsset, User, diff --git a/libs/common/src/lib/interfaces/responses/errors.interface.ts b/libs/common/src/lib/interfaces/responses/errors.interface.ts new file mode 100644 index 000000000..0b43592be --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/errors.interface.ts @@ -0,0 +1,6 @@ +import { UniqueAsset } from '../unique-asset.interface'; + +export interface ResponseError { + errors?: UniqueAsset[]; + hasErrors: boolean; +} diff --git a/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts new file mode 100644 index 000000000..3db6d3af4 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts @@ -0,0 +1,6 @@ +import { PortfolioPerformance } from '../portfolio-performance.interface'; +import { ResponseError } from './errors.interface'; + +export interface PortfolioPerformanceResponse extends ResponseError { + performance: PortfolioPerformance; +}