2022-02-01 19:12:00 +01:00
|
|
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
2023-04-23 12:02:01 +02:00
|
|
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
|
|
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
2022-05-07 20:00:51 +02:00
|
|
|
import {
|
2022-06-11 13:40:15 +02:00
|
|
|
GATHER_ASSET_PROFILE_PROCESS,
|
|
|
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
2022-05-07 20:00:51 +02:00
|
|
|
} from '@ghostfolio/common/config';
|
2022-05-07 11:44:29 +02:00
|
|
|
import { Filter } from '@ghostfolio/common/interfaces';
|
2021-05-16 22:11:14 +02:00
|
|
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { Injectable } from '@nestjs/common';
|
2022-04-30 21:47:10 +02:00
|
|
|
import {
|
|
|
|
AssetClass,
|
|
|
|
AssetSubClass,
|
|
|
|
DataSource,
|
|
|
|
Order,
|
|
|
|
Prisma,
|
2022-07-19 20:24:01 +02:00
|
|
|
Tag,
|
2022-04-30 21:47:10 +02:00
|
|
|
Type as TypeOfOrder
|
|
|
|
} from '@prisma/client';
|
2021-12-28 17:45:04 +01:00
|
|
|
import Big from 'big.js';
|
2021-07-03 11:32:03 +02:00
|
|
|
import { endOfToday, isAfter } from 'date-fns';
|
2022-08-04 13:36:32 +02:00
|
|
|
import { groupBy } from 'lodash';
|
2022-02-10 09:39:10 +01:00
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-01-23 11:39:30 +01:00
|
|
|
import { Activity } from './interfaces/activities.interface';
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
@Injectable()
|
|
|
|
export class OrderService {
|
|
|
|
public constructor(
|
2022-02-01 19:12:00 +01:00
|
|
|
private readonly accountService: AccountService,
|
2021-04-13 21:53:58 +02:00
|
|
|
private readonly dataGatheringService: DataGatheringService,
|
2022-06-11 13:40:15 +02:00
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
2022-02-10 09:39:10 +01:00
|
|
|
private readonly prismaService: PrismaService,
|
|
|
|
private readonly symbolProfileService: SymbolProfileService
|
2021-04-13 21:53:58 +02:00
|
|
|
) {}
|
|
|
|
|
|
|
|
public async order(
|
|
|
|
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
|
|
|
): Promise<Order | null> {
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.order.findUnique({
|
2021-04-13 21:53:58 +02:00
|
|
|
where: orderWhereUniqueInput
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public async orders(params: {
|
|
|
|
include?: Prisma.OrderInclude;
|
|
|
|
skip?: number;
|
|
|
|
take?: number;
|
|
|
|
cursor?: Prisma.OrderWhereUniqueInput;
|
|
|
|
where?: Prisma.OrderWhereInput;
|
2021-12-05 16:52:24 +01:00
|
|
|
orderBy?: Prisma.OrderOrderByWithRelationInput;
|
2021-05-02 21:18:52 +02:00
|
|
|
}): Promise<OrderWithAccount[]> {
|
2021-04-13 21:53:58 +02:00
|
|
|
const { include, skip, take, cursor, where, orderBy } = params;
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.order.findMany({
|
2021-04-13 21:53:58 +02:00
|
|
|
cursor,
|
|
|
|
include,
|
|
|
|
orderBy,
|
|
|
|
skip,
|
|
|
|
take,
|
|
|
|
where
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-01 19:12:00 +01:00
|
|
|
public async createOrder(
|
2022-03-05 11:07:27 +01:00
|
|
|
data: Prisma.OrderCreateInput & {
|
|
|
|
accountId?: string;
|
2022-04-30 21:47:10 +02:00
|
|
|
assetClass?: AssetClass;
|
|
|
|
assetSubClass?: AssetSubClass;
|
2022-03-05 11:07:27 +01:00
|
|
|
currency?: string;
|
|
|
|
dataSource?: DataSource;
|
|
|
|
symbol?: string;
|
2022-07-19 20:24:01 +02:00
|
|
|
tags?: Tag[];
|
2023-05-06 09:01:09 +02:00
|
|
|
updateAccountBalance?: boolean;
|
2022-03-05 11:07:27 +01:00
|
|
|
userId: string;
|
|
|
|
}
|
2022-02-01 19:12:00 +01:00
|
|
|
): Promise<Order> {
|
2023-01-30 20:00:07 +01:00
|
|
|
let Account;
|
2022-07-19 20:24:01 +02:00
|
|
|
|
2023-01-30 20:00:07 +01:00
|
|
|
if (data.accountId) {
|
|
|
|
Account = {
|
|
|
|
connect: {
|
|
|
|
id_userId: {
|
|
|
|
userId: data.userId,
|
|
|
|
id: data.accountId
|
|
|
|
}
|
2022-02-01 19:12:00 +01:00
|
|
|
}
|
2023-01-30 20:00:07 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-05-06 09:01:09 +02:00
|
|
|
const accountId = data.accountId;
|
|
|
|
let currency = data.currency;
|
2023-01-30 20:00:07 +01:00
|
|
|
const tags = data.tags ?? [];
|
2023-05-06 09:01:09 +02:00
|
|
|
const updateAccountBalance = data.updateAccountBalance ?? false;
|
|
|
|
const userId = data.userId;
|
2022-02-01 19:12:00 +01:00
|
|
|
|
2022-02-10 09:39:10 +01:00
|
|
|
if (data.type === 'ITEM') {
|
2022-04-30 21:47:10 +02:00
|
|
|
const assetClass = data.assetClass;
|
|
|
|
const assetSubClass = data.assetSubClass;
|
2023-05-06 09:01:09 +02:00
|
|
|
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
2022-02-10 09:39:10 +01:00
|
|
|
const dataSource: DataSource = 'MANUAL';
|
|
|
|
const id = uuidv4();
|
|
|
|
const name = data.SymbolProfile.connectOrCreate.create.symbol;
|
|
|
|
|
|
|
|
data.id = id;
|
2022-04-30 21:47:10 +02:00
|
|
|
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
|
|
|
|
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;
|
2022-02-10 09:39:10 +01:00
|
|
|
data.SymbolProfile.connectOrCreate.create.currency = currency;
|
|
|
|
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
|
|
|
|
data.SymbolProfile.connectOrCreate.create.name = name;
|
|
|
|
data.SymbolProfile.connectOrCreate.create.symbol = id;
|
|
|
|
data.SymbolProfile.connectOrCreate.where.dataSource_symbol = {
|
|
|
|
dataSource,
|
|
|
|
symbol: id
|
|
|
|
};
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2023-04-15 11:54:47 +02:00
|
|
|
await this.dataGatheringService.addJobToQueue({
|
|
|
|
data: {
|
2022-06-11 13:40:15 +02:00
|
|
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
|
|
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
|
|
|
},
|
2023-04-15 11:54:47 +02:00
|
|
|
name: GATHER_ASSET_PROFILE_PROCESS,
|
|
|
|
opts: {
|
2023-04-14 19:57:23 +02:00
|
|
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
2023-04-16 09:49:27 +02:00
|
|
|
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}`
|
2023-04-14 19:57:23 +02:00
|
|
|
}
|
2023-04-15 11:54:47 +02:00
|
|
|
});
|
2022-03-01 21:32:19 +01:00
|
|
|
|
2022-02-10 09:39:10 +01:00
|
|
|
const isDraft = isAfter(data.date as Date, endOfToday());
|
2021-12-04 11:40:12 +01:00
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
if (!isDraft) {
|
2021-07-03 11:32:03 +02:00
|
|
|
// Gather symbol data of order in the background, if not draft
|
|
|
|
this.dataGatheringService.gatherSymbols([
|
|
|
|
{
|
2022-03-05 11:07:27 +01:00
|
|
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
2022-02-10 09:39:10 +01:00
|
|
|
date: <Date>data.date,
|
|
|
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
2021-07-03 11:32:03 +02:00
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-02-01 19:12:00 +01:00
|
|
|
delete data.accountId;
|
2022-04-30 21:47:10 +02:00
|
|
|
delete data.assetClass;
|
|
|
|
delete data.assetSubClass;
|
2022-07-27 21:07:27 +02:00
|
|
|
|
|
|
|
if (!data.comment) {
|
|
|
|
delete data.comment;
|
|
|
|
}
|
|
|
|
|
2022-03-05 11:07:27 +01:00
|
|
|
delete data.currency;
|
|
|
|
delete data.dataSource;
|
|
|
|
delete data.symbol;
|
2022-07-19 20:24:01 +02:00
|
|
|
delete data.tags;
|
2023-05-06 09:01:09 +02:00
|
|
|
delete data.updateAccountBalance;
|
2022-02-01 19:12:00 +01:00
|
|
|
delete data.userId;
|
|
|
|
|
|
|
|
const orderData: Prisma.OrderCreateInput = data;
|
|
|
|
|
2023-05-06 09:01:09 +02:00
|
|
|
const order = await this.prismaService.order.create({
|
2021-08-07 20:52:55 +02:00
|
|
|
data: {
|
2022-02-01 19:12:00 +01:00
|
|
|
...orderData,
|
|
|
|
Account,
|
2022-07-19 20:24:01 +02:00
|
|
|
isDraft,
|
|
|
|
tags: {
|
|
|
|
connect: tags.map(({ id }) => {
|
|
|
|
return { id };
|
|
|
|
})
|
|
|
|
}
|
2021-08-07 20:52:55 +02:00
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
});
|
2023-05-06 09:01:09 +02:00
|
|
|
|
|
|
|
if (updateAccountBalance === true) {
|
|
|
|
let amount = new Big(data.unitPrice)
|
|
|
|
.mul(data.quantity)
|
|
|
|
.plus(data.fee)
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
if (data.type === 'BUY') {
|
|
|
|
amount = new Big(amount).mul(-1).toNumber();
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.accountService.updateAccountBalance({
|
|
|
|
accountId,
|
|
|
|
amount,
|
|
|
|
currency,
|
|
|
|
userId,
|
|
|
|
date: data.date as Date
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return order;
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async deleteOrder(
|
2021-08-07 20:52:55 +02:00
|
|
|
where: Prisma.OrderWhereUniqueInput
|
2021-04-13 21:53:58 +02:00
|
|
|
): Promise<Order> {
|
2022-02-10 09:39:10 +01:00
|
|
|
const order = await this.prismaService.order.delete({
|
2021-04-13 21:53:58 +02:00
|
|
|
where
|
|
|
|
});
|
2022-02-10 09:39:10 +01:00
|
|
|
|
|
|
|
if (order.type === 'ITEM') {
|
|
|
|
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
|
|
|
}
|
|
|
|
|
|
|
|
return order;
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
2023-04-23 19:49:32 +02:00
|
|
|
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
|
|
|
|
const { count } = await this.prismaService.order.deleteMany({
|
|
|
|
where
|
|
|
|
});
|
|
|
|
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2021-12-28 17:45:04 +01:00
|
|
|
public async getOrders({
|
2022-05-07 11:44:29 +02:00
|
|
|
filters,
|
2021-08-07 20:52:55 +02:00
|
|
|
includeDrafts = false,
|
2021-12-28 21:12:12 +01:00
|
|
|
types,
|
2022-01-23 11:39:30 +01:00
|
|
|
userCurrency,
|
2022-09-25 18:02:46 +02:00
|
|
|
userId,
|
|
|
|
withExcludedAccounts = false
|
2021-08-07 20:52:55 +02:00
|
|
|
}: {
|
2022-05-07 11:44:29 +02:00
|
|
|
filters?: Filter[];
|
2021-08-07 20:52:55 +02:00
|
|
|
includeDrafts?: boolean;
|
2021-12-28 21:12:12 +01:00
|
|
|
types?: TypeOfOrder[];
|
2022-01-23 11:39:30 +01:00
|
|
|
userCurrency: string;
|
2021-08-07 20:52:55 +02:00
|
|
|
userId: string;
|
2022-09-25 18:02:46 +02:00
|
|
|
withExcludedAccounts?: boolean;
|
2022-01-23 11:39:30 +01:00
|
|
|
}): Promise<Activity[]> {
|
2021-08-07 20:52:55 +02:00
|
|
|
const where: Prisma.OrderWhereInput = { userId };
|
|
|
|
|
2022-05-16 21:49:22 +02:00
|
|
|
const {
|
|
|
|
ACCOUNT: filtersByAccount,
|
|
|
|
ASSET_CLASS: filtersByAssetClass,
|
|
|
|
TAG: filtersByTag
|
|
|
|
} = groupBy(filters, (filter) => {
|
|
|
|
return filter.type;
|
|
|
|
});
|
2022-05-07 11:44:29 +02:00
|
|
|
|
|
|
|
if (filtersByAccount?.length > 0) {
|
|
|
|
where.accountId = {
|
|
|
|
in: filtersByAccount.map(({ id }) => {
|
|
|
|
return id;
|
|
|
|
})
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
if (includeDrafts === false) {
|
|
|
|
where.isDraft = false;
|
|
|
|
}
|
|
|
|
|
2022-05-16 21:49:22 +02:00
|
|
|
if (filtersByAssetClass?.length > 0) {
|
|
|
|
where.SymbolProfile = {
|
|
|
|
OR: [
|
|
|
|
{
|
|
|
|
AND: [
|
|
|
|
{
|
|
|
|
OR: filtersByAssetClass.map(({ id }) => {
|
|
|
|
return { assetClass: AssetClass[id] };
|
|
|
|
})
|
|
|
|
},
|
|
|
|
{
|
2022-08-09 21:25:07 +02:00
|
|
|
OR: [
|
|
|
|
{ SymbolProfileOverrides: { is: null } },
|
|
|
|
{ SymbolProfileOverrides: { assetClass: null } }
|
|
|
|
]
|
2022-05-16 21:49:22 +02:00
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
SymbolProfileOverrides: {
|
|
|
|
OR: filtersByAssetClass.map(({ id }) => {
|
|
|
|
return { assetClass: AssetClass[id] };
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-05-07 11:44:29 +02:00
|
|
|
if (filtersByTag?.length > 0) {
|
2022-04-24 16:23:03 +02:00
|
|
|
where.tags = {
|
|
|
|
some: {
|
2022-05-07 11:44:29 +02:00
|
|
|
OR: filtersByTag.map(({ id }) => {
|
|
|
|
return { id };
|
2022-04-24 16:23:03 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-12-28 21:12:12 +01:00
|
|
|
if (types) {
|
|
|
|
where.OR = types.map((type) => {
|
|
|
|
return {
|
|
|
|
type: {
|
|
|
|
equals: type
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-12-28 17:45:04 +01:00
|
|
|
return (
|
|
|
|
await this.orders({
|
|
|
|
where,
|
|
|
|
include: {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
Account: {
|
|
|
|
include: {
|
|
|
|
Platform: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
2022-04-25 22:37:34 +02:00
|
|
|
SymbolProfile: true,
|
|
|
|
tags: true
|
2021-12-28 17:45:04 +01:00
|
|
|
},
|
|
|
|
orderBy: { date: 'asc' }
|
|
|
|
})
|
2022-09-25 18:02:46 +02:00
|
|
|
)
|
|
|
|
.filter((order) => {
|
|
|
|
return withExcludedAccounts || order.Account?.isExcluded === false;
|
|
|
|
})
|
|
|
|
.map((order) => {
|
|
|
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
|
|
|
|
|
|
|
return {
|
|
|
|
...order,
|
2022-01-23 11:39:30 +01:00
|
|
|
value,
|
2022-09-25 18:02:46 +02:00
|
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
order.fee,
|
|
|
|
order.SymbolProfile.currency,
|
|
|
|
userCurrency
|
|
|
|
),
|
|
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
value,
|
|
|
|
order.SymbolProfile.currency,
|
|
|
|
userCurrency
|
|
|
|
)
|
|
|
|
};
|
|
|
|
});
|
2021-08-07 20:52:55 +02:00
|
|
|
}
|
|
|
|
|
2022-03-05 11:07:27 +01:00
|
|
|
public async updateOrder({
|
|
|
|
data,
|
|
|
|
where
|
|
|
|
}: {
|
|
|
|
data: Prisma.OrderUpdateInput & {
|
2022-04-30 21:47:10 +02:00
|
|
|
assetClass?: AssetClass;
|
|
|
|
assetSubClass?: AssetSubClass;
|
2022-03-05 11:07:27 +01:00
|
|
|
currency?: string;
|
|
|
|
dataSource?: DataSource;
|
|
|
|
symbol?: string;
|
2022-07-19 20:24:01 +02:00
|
|
|
tags?: Tag[];
|
2022-03-05 11:07:27 +01:00
|
|
|
};
|
2021-08-07 20:52:55 +02:00
|
|
|
where: Prisma.OrderWhereUniqueInput;
|
|
|
|
}): Promise<Order> {
|
2022-02-10 09:39:10 +01:00
|
|
|
if (data.Account.connect.id_userId.id === null) {
|
|
|
|
delete data.Account;
|
|
|
|
}
|
|
|
|
|
2022-07-27 21:07:27 +02:00
|
|
|
if (!data.comment) {
|
|
|
|
data.comment = null;
|
|
|
|
}
|
|
|
|
|
2022-07-19 20:24:01 +02:00
|
|
|
const tags = data.tags ?? [];
|
|
|
|
|
2022-03-05 11:07:27 +01:00
|
|
|
let isDraft = false;
|
|
|
|
|
2022-02-10 09:39:10 +01:00
|
|
|
if (data.type === 'ITEM') {
|
2022-04-30 21:47:10 +02:00
|
|
|
delete data.SymbolProfile.connect;
|
2022-03-05 11:07:27 +01:00
|
|
|
} else {
|
2022-04-30 21:47:10 +02:00
|
|
|
delete data.SymbolProfile.update;
|
|
|
|
|
2022-03-05 11:07:27 +01:00
|
|
|
isDraft = isAfter(data.date as Date, endOfToday());
|
|
|
|
|
|
|
|
if (!isDraft) {
|
|
|
|
// Gather symbol data of order in the background, if not draft
|
|
|
|
this.dataGatheringService.gatherSymbols([
|
|
|
|
{
|
|
|
|
dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource,
|
|
|
|
date: <Date>data.date,
|
|
|
|
symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2021-08-07 20:52:55 +02:00
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-04-30 21:47:10 +02:00
|
|
|
delete data.assetClass;
|
|
|
|
delete data.assetSubClass;
|
2022-03-05 11:07:27 +01:00
|
|
|
delete data.currency;
|
|
|
|
delete data.dataSource;
|
|
|
|
delete data.symbol;
|
2022-07-19 20:24:01 +02:00
|
|
|
delete data.tags;
|
2022-03-05 11:07:27 +01:00
|
|
|
|
2022-12-25 12:23:52 +01:00
|
|
|
// Remove existing tags
|
|
|
|
await this.prismaService.order.update({
|
|
|
|
data: { tags: { set: [] } },
|
|
|
|
where
|
|
|
|
});
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
return this.prismaService.order.update({
|
2021-08-07 20:52:55 +02:00
|
|
|
data: {
|
|
|
|
...data,
|
2022-07-19 20:24:01 +02:00
|
|
|
isDraft,
|
|
|
|
tags: {
|
|
|
|
connect: tags.map(({ id }) => {
|
|
|
|
return { id };
|
|
|
|
})
|
|
|
|
}
|
2021-08-07 20:52:55 +02:00
|
|
|
},
|
2021-04-13 21:53:58 +02:00
|
|
|
where
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|