diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 3fcbcff12..c7cb51fa4 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -89,7 +89,7 @@ export class AccountService { orderBy?: Prisma.AccountOrderByWithRelationInput; }): Promise< (AccountWithBalance & { - activities?: (Order & { SymbolProfile?: SymbolProfile })[]; + activities?: (Order & { SymbolProfile?: SymbolProfile; tags?: Tag[] })[]; balances?: AccountBalance[]; platform?: Platform; tags?: Tag[]; @@ -237,15 +237,10 @@ export class AccountService { }); return accounts.map((account) => { - let activitiesCount = 0; - - for (const { isDraft } of account.activities) { - if (!isDraft) { - activitiesCount += 1; - } - } - - const result = { ...account, activitiesCount }; + const result = { + ...account, + activitiesCount: account.activities.length + }; delete result.activities; diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index a1b559c84..b95a33c36 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.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 { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; @@ -287,9 +288,9 @@ export class ActivitiesController { userId: this.request.user.id }); - if (dataSource && !activity.isDraft) { + if (dataSource && !isActivityInFuture({ date: activity.date })) { // Gather symbol data in the background, if data source is set - // (not MANUAL) and not draft + // (not MANUAL) and the date is not in the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { @@ -369,6 +370,7 @@ export class ActivitiesController { }), user: { connect: { id: this.request.user.id } } }, + storedDate: originalActivity.date, userId: this.request.user.id, where: { id diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 0d0f56fbe..19c57d155 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -7,7 +7,11 @@ import { isAccountBalanceInFuture, WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; -import { isDraftTagToBeAssigned } from '@ghostfolio/api/helper/activity.helper'; +import { + isActivityInFuture, + isDraftTagToBeAssigned, + WHERE_ACTIVITY_NOT_DRAFT +} from '@ghostfolio/api/helper/activity.helper'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; @@ -28,6 +32,7 @@ import { import { canDeleteAssetProfile, getAssetProfileIdentifier, + isDraftActivity, isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; import { @@ -51,7 +56,7 @@ import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; -import { endOfToday, isAfter } from 'date-fns'; +import { endOfToday } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; @@ -263,10 +268,6 @@ export class ActivitiesService { const orderData: Prisma.OrderCreateInput = data; - const isDraft = NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) - ? false - : isAfter(data.date as Date, endOfToday()); - const tagsToConnect = isDraftTagToBeAssigned({ date: data.date as Date, type: data.type @@ -278,7 +279,8 @@ export class ActivitiesService { data: { ...orderData, account, - isDraft, + // @deprecated Mirrors the "Draft" tag until the attribute is removed + isDraft: isDraftActivity({ tags: tagsToConnect }), tags: { connect: tagsToConnect } @@ -647,7 +649,7 @@ export class ActivitiesService { } if (includeDrafts === false) { - where.isDraft = false; + andConditions.push(WHERE_ACTIVITY_NOT_DRAFT); } if (filtersByAssetClass.length > 0) { @@ -961,6 +963,7 @@ export class ActivitiesService { public async updateActivity({ data, + storedDate, userId, where }: { @@ -972,6 +975,7 @@ export class ActivitiesService { tags?: { id: string }[]; type?: ActivityType; }; + storedDate: Date; userId: string; where: Prisma.OrderWhereUniqueInput; }): Promise { @@ -988,8 +992,6 @@ export class ActivitiesService { data.comment = null; } - let isDraft = false; - if ( NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) || (data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' && @@ -1000,10 +1002,9 @@ export class ActivitiesService { } else { delete data.SymbolProfile.update; - isDraft = isAfter(data.date as Date, endOfToday()); - - if (!isDraft) { - // Gather symbol data of order in the background, if not draft + if (!isActivityInFuture({ date: data.date as Date })) { + // Gather symbol data of order in the background, if the date is not in + // the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { @@ -1023,14 +1024,9 @@ export class ActivitiesService { delete data.symbol; delete data.tags; - const storedActivity = await this.prismaService.order.findUnique({ - where, - select: { date: true } - }); - const tagsToSet = isDraftTagToBeAssigned({ + storedDate, date: data.date as Date, - storedDate: storedActivity?.date, type: data.type }) ? uniqBy([...tags, { id: TAG_ID_DRAFT }], 'id') @@ -1040,7 +1036,8 @@ export class ActivitiesService { where, data: { ...data, - isDraft, + // @deprecated Mirrors the "Draft" tag until the attribute is removed + isDraft: isDraftActivity({ tags: tagsToSet }), tags: { set: tagsToSet } diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 1bc164686..7d039169b 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { isDraftTagToBeAssigned } from '@ghostfolio/api/helper/activity.helper'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -13,6 +14,7 @@ import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, ghostfolioPrefix, NON_INVESTMENT_ACTIVITY_TYPES, + TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { @@ -22,6 +24,7 @@ import { } from '@ghostfolio/common/dtos'; import { getAssetProfileIdentifier, + isDraftActivity, isValidCustomAssetProfileSymbol, parseDate } from '@ghostfolio/common/helper'; @@ -41,7 +44,7 @@ import { Injectable } from '@nestjs/common'; import { Account, DataSource, Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { isISIN } from 'class-validator'; -import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; +import { isSameSecond, parseISO } from 'date-fns'; import { omit, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; @@ -775,6 +778,16 @@ export class ImportService { }); if (isDryRun) { + // Preview the "Draft" tag which createActivity() assigns in a real run + const draftTag = tags.find(({ id: tagId }) => { + return tagId === TAG_ID_DRAFT; + }); + + const previewTags = + draftTag && isDraftTagToBeAssigned({ date, type }) + ? uniqBy([...validatedTags, draftTag], 'id') + : validatedTags; + order = { comment, currency, @@ -788,7 +801,7 @@ export class ImportService { accountUserId: undefined, createdAt: new Date(), id: randomUUID(), - isDraft: isAfter(date, endOfToday()), + isDraft: isDraftActivity({ tags: previewTags }), SymbolProfile: { assetClass, assetSubClass, @@ -817,7 +830,7 @@ export class ImportService { userId: dataSource === 'MANUAL' ? user.id : undefined }, symbolProfileId: undefined, - tags: validatedTags, + tags: previewTags, updatedAt: new Date(), userId: user.id }; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b665b635a..390280fd1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -43,6 +43,7 @@ import { getAssetProfileIdentifier, getSum, isAccountExcluded, + isDraftActivity, parseDate } from '@ghostfolio/common/helper'; import { @@ -174,7 +175,7 @@ export class PortfolioService { this.accountService.accounts({ where, include: { - activities: { include: { SymbolProfile: true } }, + activities: { include: { SymbolProfile: true, tags: true } }, platform: true, tags: true }, @@ -200,12 +201,18 @@ export class PortfolioService { for (const { currency, date, - isDraft, quantity, SymbolProfile, + tags, type, unitPrice } of account.activities) { + activitiesCount += 1; + + if (isDraftActivity({ tags })) { + continue; + } + switch (type) { case ActivityType.DIVIDEND: dividendInBaseCurrency += @@ -226,10 +233,6 @@ export class PortfolioService { )) ?? 0; break; } - - if (!isDraft) { - activitiesCount += 1; - } } const valueInBaseCurrency = @@ -2101,8 +2104,8 @@ export class PortfolioService { }) { return getSum( activities - .filter(({ isDraft, type }) => { - return isDraft === false && type === activityType; + .filter((activity) => { + return !isDraftActivity(activity) && activity.type === activityType; }) .map(({ assetProfile, currency, quantity, unitPrice }) => { return new Big( diff --git a/apps/api/src/helper/activity.helper.ts b/apps/api/src/helper/activity.helper.ts index 07748a8bf..879ae7d53 100644 --- a/apps/api/src/helper/activity.helper.ts +++ b/apps/api/src/helper/activity.helper.ts @@ -1,8 +1,29 @@ -import { NON_INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config'; +import { + NON_INVESTMENT_ACTIVITY_TYPES, + TAG_ID_DRAFT +} from '@ghostfolio/common/config'; -import { Type as ActivityType } from '@prisma/client'; +import { Prisma, Type as ActivityType } from '@prisma/client'; import { endOfToday, isAfter } from 'date-fns'; +export const WHERE_ACTIVITY_NOT_DRAFT: Prisma.OrderWhereInput = { + tags: { + none: { + id: TAG_ID_DRAFT + } + } +}; + +export function isActivityInFuture({ + date, + endOfTodayDate = endOfToday() +}: { + date: Date; + endOfTodayDate?: Date; +}) { + return isAfter(date, endOfTodayDate); +} + export function isDraftTagToBeAssigned({ date, endOfTodayDate = endOfToday(), @@ -18,11 +39,13 @@ export function isDraftTagToBeAssigned({ return false; } - if (!isAfter(date, endOfTodayDate)) { + if (!isActivityInFuture({ date, endOfTodayDate })) { return false; } // Assign only when the date newly moves into the future, so that a tag the // user has removed is not restored by an unrelated change - return storedDate ? !isAfter(storedDate, endOfTodayDate) : true; + return storedDate + ? !isActivityInFuture({ endOfTodayDate, date: storedDate }) + : true; } diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 7f53da4fa..9c10b5cb7 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -177,7 +177,7 @@
{{ element.assetProfile?.name }} - @if (element.isDraft) { + @if (isDraftActivity(element)) { Draft }
@@ -513,7 +513,7 @@