From 0388a977b3158d6205e6bc3fa3a7df07b2178e75 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:44:37 +0100 Subject: [PATCH] Support date range in activities endpoint --- .../src/app/benchmark/benchmark.controller.ts | 5 +- .../api/src/app/benchmark/benchmark.module.ts | 2 - apps/api/src/app/order/order.controller.ts | 8 +- .../src/app/portfolio/portfolio.controller.ts | 3 +- .../src/app/portfolio/portfolio.service.ts | 73 +++---------------- apps/api/src/helper/portfolio.helper.ts | 59 +++++++++++++++ .../activities/activities-page.component.ts | 1 + apps/client/src/app/services/data.service.ts | 11 ++- 8 files changed, 92 insertions(+), 70 deletions(-) diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index 833e2b9e8..184214384 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -1,6 +1,6 @@ -import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { getInterval } from '@ghostfolio/api/helper/portfolio.helper'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import type { @@ -35,7 +35,6 @@ import { BenchmarkService } from './benchmark.service'; export class BenchmarkController { public constructor( private readonly benchmarkService: BenchmarkService, - private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -112,7 +111,7 @@ export class BenchmarkController { @Param('symbol') symbol: string, @Query('range') dateRange: DateRange = 'max' ): Promise { - const { endDate, startDate } = this.portfolioService.getInterval( + const { endDate, startDate } = getInterval( dateRange, new Date(startDateString) ); diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/benchmark/benchmark.module.ts index 9e7b8fe75..7371588d1 100644 --- a/apps/api/src/app/benchmark/benchmark.module.ts +++ b/apps/api/src/app/benchmark/benchmark.module.ts @@ -1,4 +1,3 @@ -import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -22,7 +21,6 @@ import { BenchmarkService } from './benchmark.service'; DataProviderModule, ExchangeRateDataModule, MarketDataModule, - PortfolioModule, PrismaModule, PropertyModule, RedisCacheModule, diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index dbcf6dedb..2f9825d6b 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { getInterval } from '@ghostfolio/api/helper/portfolio.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; @@ -8,7 +9,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -84,6 +85,7 @@ export class OrderController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, + @Query('range') dateRange: DateRange = 'max', @Query('skip') skip?: number, @Query('sortColumn') sortColumn?: string, @Query('sortDirection') sortDirection?: Prisma.SortOrder, @@ -96,14 +98,18 @@ export class OrderController { filterByTags }); + const { endDate, startDate } = getInterval(dateRange); + const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); const userCurrency = this.request.user.Settings.settings.baseCurrency; const { activities, count } = await this.orderService.getOrders({ + endDate, filters, sortColumn, sortDirection, + startDate, userCurrency, includeDrafts: true, skip: isNaN(skip) ? undefined : skip, diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 5f62b0969..6047b7abd 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -6,6 +6,7 @@ import { hasNotDefinedValuesInObject, nullifyValuesInObject } from '@ghostfolio/api/helper/object.helper'; +import { getInterval } from '@ghostfolio/api/helper/portfolio.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; @@ -236,7 +237,7 @@ export class PortfolioController { await this.impersonationService.validateImpersonationId(impersonationId); const userCurrency = this.request.user.Settings.settings.baseCurrency; - const { endDate, startDate } = this.portfolioService.getInterval(dateRange); + const { endDate, startDate } = getInterval(dateRange); const { activities } = await this.orderService.getOrders({ endDate, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fca5e1e0d..8384427c3 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -5,7 +5,10 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; +import { + getFactor, + getInterval +} from '@ghostfolio/api/helper/portfolio.helper'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; @@ -26,8 +29,7 @@ import { DATE_FORMAT, getAllActivityTypes, getSum, - parseDate, - resetHours + parseDate } from '@ghostfolio/common/helper'; import { Accounts, @@ -74,20 +76,10 @@ import { isBefore, isSameMonth, isSameYear, - isValid, - max, - min, parseISO, - set, - startOfWeek, - startOfMonth, - startOfYear, - subDays, - subYears, - endOfDay, - endOfYear + set } from 'date-fns'; -import { first, isEmpty, last, uniq, uniqBy } from 'lodash'; +import { isEmpty, last, uniq, uniqBy } from 'lodash'; import { PortfolioCalculator } from './calculator/twr/portfolio-calculator'; import { @@ -243,49 +235,6 @@ export class PortfolioService { return dividends; } - public getInterval(aDateRange: DateRange, portfolioStart = new Date(0)) { - let endDate = endOfDay(new Date()); - let startDate = portfolioStart; - - switch (aDateRange) { - case '1d': - startDate = max([startDate, subDays(resetHours(new Date()), 1)]); - break; - case 'mtd': - startDate = max([ - startDate, - subDays(startOfMonth(resetHours(new Date())), 1) - ]); - break; - case 'wtd': - startDate = max([ - startDate, - subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1) - ]); - break; - case 'ytd': - startDate = max([ - startDate, - subDays(startOfYear(resetHours(new Date())), 1) - ]); - break; - case '1y': - startDate = max([startDate, subYears(resetHours(new Date()), 1)]); - break; - case '5y': - startDate = max([startDate, subYears(resetHours(new Date()), 5)]); - break; - case 'max': - break; - default: - // '2024', '2023', '2022', etc. - endDate = endOfYear(new Date(aDateRange)); - startDate = max([startDate, new Date(aDateRange)]); - } - - return { endDate, startDate }; - } - public async getInvestments({ dateRange, filters, @@ -412,7 +361,7 @@ export class PortfolioService { exchangeRateDataService: this.exchangeRateDataService }); - const { startDate } = this.getInterval( + const { startDate } = getInterval( dateRange, portfolioCalculator.getStartDate() ); @@ -997,7 +946,7 @@ export class PortfolioService { const userId = await this.getUserId(impersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); - const { endDate, startDate } = this.getInterval(dateRange); + const { endDate, startDate } = getInterval(dateRange); const { activities } = await this.orderService.getOrders({ endDate, @@ -1171,7 +1120,7 @@ export class PortfolioService { ) ); - const { endDate, startDate } = this.getInterval(dateRange); + const { endDate, startDate } = getInterval(dateRange); const { activities } = await this.orderService.getOrders({ endDate, @@ -1479,7 +1428,7 @@ export class PortfolioService { userId = await this.getUserId(impersonationId, userId); - const { endDate, startDate } = this.getInterval( + const { endDate, startDate } = getInterval( dateRange, portfolioCalculator.getStartDate() ); diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index 01b532cbf..730f34bde 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -1,4 +1,17 @@ +import { resetHours } from '@ghostfolio/common/helper'; +import { DateRange } from '@ghostfolio/common/types'; + import { Type as ActivityType } from '@prisma/client'; +import { + endOfDay, + max, + subDays, + startOfMonth, + startOfWeek, + startOfYear, + subYears, + endOfYear +} from 'date-fns'; export function getFactor(activityType: ActivityType) { let factor: number; @@ -19,3 +32,49 @@ export function getFactor(activityType: ActivityType) { return factor; } + +export function getInterval( + aDateRange: DateRange, + portfolioStart = new Date(0) +) { + let endDate = endOfDay(new Date()); + let startDate = portfolioStart; + + switch (aDateRange) { + case '1d': + startDate = max([startDate, subDays(resetHours(new Date()), 1)]); + break; + case 'mtd': + startDate = max([ + startDate, + subDays(startOfMonth(resetHours(new Date())), 1) + ]); + break; + case 'wtd': + startDate = max([ + startDate, + subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1) + ]); + break; + case 'ytd': + startDate = max([ + startDate, + subDays(startOfYear(resetHours(new Date())), 1) + ]); + break; + case '1y': + startDate = max([startDate, subYears(resetHours(new Date()), 1)]); + break; + case '5y': + startDate = max([startDate, subYears(resetHours(new Date()), 5)]); + break; + case 'max': + break; + default: + // '2024', '2023', '2022', etc. + endDate = endOfYear(new Date(aDateRange)); + startDate = max([startDate, new Date(aDateRange)]); + } + + return { endDate, startDate }; +} diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 190fc673e..5b02585c3 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -124,6 +124,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { this.dataService .fetchActivities({ filters: this.userService.getFilters(), + range: this.user?.settings?.dateRange, skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn, sortDirection: this.sortDirection, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index ae1d04004..088512cec 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -159,12 +159,14 @@ export class DataService { public fetchActivities({ filters, + range, skip, sortColumn, sortDirection, take }: { filters?: Filter[]; + range?: DateRange; skip?: number; sortColumn?: string; sortDirection?: SortDirection; @@ -172,6 +174,10 @@ export class DataService { }): Observable { let params = this.buildFiltersAsQueryParams({ filters }); + if (range) { + params = params.append('range', range); + } + if (skip) { params = params.append('skip', skip); } @@ -277,7 +283,10 @@ export class DataService { startDate: Date; } & UniqueAsset): Observable { let params = new HttpParams(); - params = params.append('range', range); + + if (range) { + params = params.append('range', range); + } return this.http.get( `/api/v1/benchmark/${dataSource}/${symbol}/${format(