|
|
@ -7,6 +7,12 @@ import { |
|
|
isAccountBalanceInFuture, |
|
|
isAccountBalanceInFuture, |
|
|
WHERE_ACCOUNT_NOT_EXCLUDED |
|
|
WHERE_ACCOUNT_NOT_EXCLUDED |
|
|
} from '@ghostfolio/api/helper/account.helper'; |
|
|
} from '@ghostfolio/api/helper/account.helper'; |
|
|
|
|
|
import { |
|
|
|
|
|
getTagsWithDraftTag, |
|
|
|
|
|
isActivityInFuture, |
|
|
|
|
|
isDraftTagToBeAssigned, |
|
|
|
|
|
WHERE_ACTIVITY_NOT_DRAFT |
|
|
|
|
|
} from '@ghostfolio/api/helper/activity.helper'; |
|
|
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; |
|
|
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; |
|
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
|
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
|
|
@ -21,11 +27,13 @@ import { |
|
|
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, |
|
|
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, |
|
|
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, |
|
|
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, |
|
|
NON_INVESTMENT_ACTIVITY_TYPES, |
|
|
NON_INVESTMENT_ACTIVITY_TYPES, |
|
|
|
|
|
TAG_ID_DRAFT, |
|
|
TAG_ID_EXCLUDE_FROM_ANALYSIS |
|
|
TAG_ID_EXCLUDE_FROM_ANALYSIS |
|
|
} from '@ghostfolio/common/config'; |
|
|
} from '@ghostfolio/common/config'; |
|
|
import { |
|
|
import { |
|
|
canDeleteAssetProfile, |
|
|
canDeleteAssetProfile, |
|
|
getAssetProfileIdentifier, |
|
|
getAssetProfileIdentifier, |
|
|
|
|
|
isDraftActivity, |
|
|
isValidCustomAssetProfileSymbol |
|
|
isValidCustomAssetProfileSymbol |
|
|
} from '@ghostfolio/common/helper'; |
|
|
} from '@ghostfolio/common/helper'; |
|
|
import { |
|
|
import { |
|
|
@ -49,7 +57,7 @@ import { |
|
|
Type as ActivityType |
|
|
Type as ActivityType |
|
|
} from '@prisma/client'; |
|
|
} from '@prisma/client'; |
|
|
import { Big } from 'big.js'; |
|
|
import { Big } from 'big.js'; |
|
|
import { endOfToday, isAfter } from 'date-fns'; |
|
|
import { endOfToday } from 'date-fns'; |
|
|
import { groupBy, uniqBy } from 'lodash'; |
|
|
import { groupBy, uniqBy } from 'lodash'; |
|
|
import { randomUUID } from 'node:crypto'; |
|
|
import { randomUUID } from 'node:crypto'; |
|
|
|
|
|
|
|
|
@ -112,7 +120,7 @@ export class ActivitiesService { |
|
|
tags, |
|
|
tags, |
|
|
userId |
|
|
userId |
|
|
}: { tags: Tag[]; userId: string } & AssetProfileIdentifier) { |
|
|
}: { tags: Tag[]; userId: string } & AssetProfileIdentifier) { |
|
|
await this.tagService.validateTagIds({ |
|
|
await this.tagService.validateTagIdsWithoutDraftTag({ |
|
|
userId, |
|
|
userId, |
|
|
tagIds: tags.map(({ id }) => { |
|
|
tagIds: tags.map(({ id }) => { |
|
|
return id; |
|
|
return id; |
|
|
@ -120,6 +128,7 @@ export class ActivitiesService { |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const activities = await this.prismaService.order.findMany({ |
|
|
const activities = await this.prismaService.order.findMany({ |
|
|
|
|
|
include: { tags: { select: { id: true } } }, |
|
|
where: { |
|
|
where: { |
|
|
userId, |
|
|
userId, |
|
|
SymbolProfile: { |
|
|
SymbolProfile: { |
|
|
@ -129,20 +138,31 @@ export class ActivitiesService { |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const tagsToAssign = tags.map(({ id }) => { |
|
|
|
|
|
return { id }; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
await Promise.all( |
|
|
await Promise.all( |
|
|
activities.map(({ id }) => |
|
|
activities.map((activity) => { |
|
|
this.prismaService.order.update({ |
|
|
// The set operation replaces all existing connections with the provided
|
|
|
|
|
|
// ones, hence the "Draft" tag of an individual activity is carried over
|
|
|
|
|
|
const isDraft = isDraftActivity(activity); |
|
|
|
|
|
|
|
|
|
|
|
const tagsToSet = isDraft |
|
|
|
|
|
? [...tagsToAssign, { id: TAG_ID_DRAFT }] |
|
|
|
|
|
: tagsToAssign; |
|
|
|
|
|
|
|
|
|
|
|
return this.prismaService.order.update({ |
|
|
data: { |
|
|
data: { |
|
|
|
|
|
// @deprecated Mirrors the "Draft" tag until the attribute is removed
|
|
|
|
|
|
isDraft, |
|
|
tags: { |
|
|
tags: { |
|
|
// The set operation replaces all existing connections with the provided ones
|
|
|
set: tagsToSet |
|
|
set: tags.map((tag) => { |
|
|
|
|
|
return { id: tag.id }; |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
where: { id } |
|
|
where: { id: activity.id } |
|
|
}) |
|
|
}); |
|
|
) |
|
|
}) |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
this.eventEmitter.emit( |
|
|
this.eventEmitter.emit( |
|
|
@ -261,17 +281,21 @@ export class ActivitiesService { |
|
|
|
|
|
|
|
|
const orderData: Prisma.OrderCreateInput = data; |
|
|
const orderData: Prisma.OrderCreateInput = data; |
|
|
|
|
|
|
|
|
const isDraft = NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) |
|
|
const tagsToConnect = getTagsWithDraftTag({ |
|
|
? false |
|
|
tags, |
|
|
: isAfter(data.date as Date, endOfToday()); |
|
|
date: data.date as Date, |
|
|
|
|
|
draftTag: { id: TAG_ID_DRAFT }, |
|
|
|
|
|
type: data.type |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const activity = await this.prismaService.order.create({ |
|
|
const activity = await this.prismaService.order.create({ |
|
|
data: { |
|
|
data: { |
|
|
...orderData, |
|
|
...orderData, |
|
|
account, |
|
|
account, |
|
|
isDraft, |
|
|
// @deprecated Mirrors the "Draft" tag until the attribute is removed
|
|
|
|
|
|
isDraft: isDraftActivity({ tags: tagsToConnect }), |
|
|
tags: { |
|
|
tags: { |
|
|
connect: tags |
|
|
connect: tagsToConnect |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
include: { SymbolProfile: true } |
|
|
include: { SymbolProfile: true } |
|
|
@ -637,8 +661,12 @@ export class ActivitiesService { |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (includeDrafts === false) { |
|
|
const isFilteredByDraftTag = filtersByTag.some(({ id }) => { |
|
|
where.isDraft = false; |
|
|
return id === TAG_ID_DRAFT; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (includeDrafts === false && !isFilteredByDraftTag) { |
|
|
|
|
|
andConditions.push(WHERE_ACTIVITY_NOT_DRAFT); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (filtersByAssetClass.length > 0) { |
|
|
if (filtersByAssetClass.length > 0) { |
|
|
@ -952,6 +980,7 @@ export class ActivitiesService { |
|
|
|
|
|
|
|
|
public async updateActivity({ |
|
|
public async updateActivity({ |
|
|
data, |
|
|
data, |
|
|
|
|
|
originalDate, |
|
|
userId, |
|
|
userId, |
|
|
where |
|
|
where |
|
|
}: { |
|
|
}: { |
|
|
@ -963,9 +992,11 @@ export class ActivitiesService { |
|
|
tags?: { id: string }[]; |
|
|
tags?: { id: string }[]; |
|
|
type?: ActivityType; |
|
|
type?: ActivityType; |
|
|
}; |
|
|
}; |
|
|
|
|
|
originalDate: Date; |
|
|
userId: string; |
|
|
userId: string; |
|
|
where: Prisma.OrderWhereUniqueInput; |
|
|
where: Prisma.OrderWhereUniqueInput; |
|
|
}): Promise<Order> { |
|
|
}): Promise<Order> { |
|
|
|
|
|
const areTagsProvided = data.tags !== undefined; |
|
|
const tags = data.tags ?? []; |
|
|
const tags = data.tags ?? []; |
|
|
|
|
|
|
|
|
await this.tagService.validateTagIds({ |
|
|
await this.tagService.validateTagIds({ |
|
|
@ -979,8 +1010,6 @@ export class ActivitiesService { |
|
|
data.comment = null; |
|
|
data.comment = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let isDraft = false; |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
if ( |
|
|
NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) || |
|
|
NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) || |
|
|
(data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' && |
|
|
(data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' && |
|
|
@ -991,10 +1020,9 @@ export class ActivitiesService { |
|
|
} else { |
|
|
} else { |
|
|
delete data.SymbolProfile.update; |
|
|
delete data.SymbolProfile.update; |
|
|
|
|
|
|
|
|
isDraft = isAfter(data.date as Date, endOfToday()); |
|
|
if (!isActivityInFuture({ date: data.date as Date })) { |
|
|
|
|
|
// Gather symbol data of order in the background, if the date is not in
|
|
|
if (!isDraft) { |
|
|
// the future
|
|
|
// Gather symbol data of order in the background, if not draft
|
|
|
|
|
|
this.dataGatheringService.gatherSymbols({ |
|
|
this.dataGatheringService.gatherSymbols({ |
|
|
dataGatheringItems: [ |
|
|
dataGatheringItems: [ |
|
|
{ |
|
|
{ |
|
|
@ -1014,14 +1042,40 @@ export class ActivitiesService { |
|
|
delete data.symbol; |
|
|
delete data.symbol; |
|
|
delete data.tags; |
|
|
delete data.tags; |
|
|
|
|
|
|
|
|
|
|
|
// Leave the tags untouched if the request does not provide them, so that a
|
|
|
|
|
|
// partial update cannot drop the "Draft" tag
|
|
|
|
|
|
let isDraft: boolean; |
|
|
|
|
|
let tagsToUpdate: Prisma.OrderUpdateInput['tags']; |
|
|
|
|
|
|
|
|
|
|
|
if (areTagsProvided) { |
|
|
|
|
|
const tagsToSet = getTagsWithDraftTag({ |
|
|
|
|
|
originalDate, |
|
|
|
|
|
tags, |
|
|
|
|
|
date: data.date as Date, |
|
|
|
|
|
draftTag: { id: TAG_ID_DRAFT }, |
|
|
|
|
|
type: data.type |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
isDraft = isDraftActivity({ tags: tagsToSet }); |
|
|
|
|
|
tagsToUpdate = { set: tagsToSet }; |
|
|
|
|
|
} else if ( |
|
|
|
|
|
isDraftTagToBeAssigned({ |
|
|
|
|
|
originalDate, |
|
|
|
|
|
date: data.date as Date, |
|
|
|
|
|
type: data.type |
|
|
|
|
|
}) |
|
|
|
|
|
) { |
|
|
|
|
|
isDraft = true; |
|
|
|
|
|
tagsToUpdate = { connect: { id: TAG_ID_DRAFT } }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const activity = await this.prismaService.order.update({ |
|
|
const activity = await this.prismaService.order.update({ |
|
|
where, |
|
|
where, |
|
|
data: { |
|
|
data: { |
|
|
...data, |
|
|
...data, |
|
|
|
|
|
// @deprecated Mirrors the "Draft" tag until the attribute is removed
|
|
|
isDraft, |
|
|
isDraft, |
|
|
tags: { |
|
|
tags: tagsToUpdate |
|
|
set: tags |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|