2022-02-01 19:12:00 +01:00
|
|
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
2021-04-21 20:27:39 +02:00
|
|
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
2022-01-23 11:39:30 +01:00
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
2021-04-21 20:27:39 +02:00
|
|
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
2022-02-10 09:39:10 +01:00
|
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/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[];
|
2022-03-05 11:07:27 +01:00
|
|
|
userId: string;
|
|
|
|
}
|
2022-02-01 19:12:00 +01:00
|
|
|
): Promise<Order> {
|
|
|
|
const defaultAccount = (
|
|
|
|
await this.accountService.getAccounts(data.userId)
|
|
|
|
).find((account) => {
|
|
|
|
return account.isDefault === true;
|
|
|
|
});
|
|
|
|
|
2022-07-19 20:24:01 +02:00
|
|
|
const tags = data.tags ?? [];
|
|
|
|
|
2022-02-10 09:39:10 +01:00
|
|
|
let Account = {
|
2022-02-01 19:12:00 +01:00
|
|
|
connect: {
|
|
|
|
id_userId: {
|
|
|
|
userId: data.userId,
|
|
|
|
id: data.accountId ?? defaultAccount?.id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
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;
|
2022-03-05 11:07:27 +01:00
|
|
|
const 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;
|
|
|
|
|
|
|
|
Account = undefined;
|
|
|
|
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
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
data.SymbolProfile.connectOrCreate.create.symbol =
|
|
|
|
data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase();
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2022-06-11 13:40:15 +02:00
|
|
|
await this.dataGatheringService.addJobToQueue(
|
|
|
|
GATHER_ASSET_PROFILE_PROCESS,
|
|
|
|
{
|
|
|
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
|
|
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
|
|
|
},
|
|
|
|
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
|
|
|
);
|
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;
|
2022-02-01 19:12:00 +01:00
|
|
|
delete data.userId;
|
|
|
|
|
|
|
|
const orderData: Prisma.OrderCreateInput = data;
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
return 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
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
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,
|
2021-08-07 20:52:55 +02:00
|
|
|
userId
|
|
|
|
}: {
|
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-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] };
|
|
|
|
})
|
|
|
|
},
|
|
|
|
{
|
|
|
|
SymbolProfileOverrides: {
|
2022-08-09 19:29:26 +02:00
|
|
|
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' }
|
|
|
|
})
|
|
|
|
).map((order) => {
|
2022-01-23 11:39:30 +01:00
|
|
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
|
|
|
|
2021-12-28 17:45:04 +01:00
|
|
|
return {
|
|
|
|
...order,
|
2022-01-23 11:39:30 +01:00
|
|
|
value,
|
|
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
order.fee,
|
2022-03-05 11:07:27 +01:00
|
|
|
order.SymbolProfile.currency,
|
2022-01-23 11:39:30 +01:00
|
|
|
userCurrency
|
|
|
|
),
|
|
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
value,
|
2022-03-05 11:07:27 +01:00
|
|
|
order.SymbolProfile.currency,
|
2022-01-23 11:39:30 +01:00
|
|
|
userCurrency
|
|
|
|
)
|
2021-12-28 17:45:04 +01:00
|
|
|
};
|
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
|
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|