ghostfolio/apps/api/src/app/order/order.service.ts

321 lines
8.5 KiB
TypeScript
Raw Normal View History

import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import {
DATA_GATHERING_QUEUE,
GATHER_ASSET_PROFILE_PROCESS
} from '@ghostfolio/common/config';
import { Filter } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { InjectQueue } from '@nestjs/bull';
2021-04-13 21:53:58 +02:00
import { Injectable } from '@nestjs/common';
import {
AssetClass,
AssetSubClass,
DataSource,
Order,
Prisma,
Type as TypeOfOrder
} from '@prisma/client';
import Big from 'big.js';
import { Queue } from 'bull';
import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
2021-04-13 21:53:58 +02:00
import { Activity } from './interfaces/activities.interface';
2021-04-13 21:53:58 +02:00
@Injectable()
export class OrderService {
public constructor(
private readonly accountService: AccountService,
2021-04-13 21:53:58 +02:00
private readonly cacheService: CacheService,
@InjectQueue(DATA_GATHERING_QUEUE)
private readonly dataGatheringQueue: Queue,
private readonly exchangeRateDataService: ExchangeRateDataService,
2021-04-13 21:53:58 +02:00
private readonly dataGatheringService: DataGatheringService,
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;
orderBy?: Prisma.OrderOrderByWithRelationInput;
}): 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
});
}
public async createOrder(
data: Prisma.OrderCreateInput & {
accountId?: string;
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
currency?: string;
dataSource?: DataSource;
symbol?: string;
userId: string;
}
): Promise<Order> {
const defaultAccount = (
await this.accountService.getAccounts(data.userId)
).find((account) => {
return account.isDefault === true;
});
let Account = {
connect: {
id_userId: {
userId: data.userId,
id: data.accountId ?? defaultAccount?.id
}
}
};
if (data.type === 'ITEM') {
const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass;
const currency = data.SymbolProfile.connectOrCreate.create.currency;
const dataSource: DataSource = 'MANUAL';
const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol;
Account = undefined;
data.id = id;
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;
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
await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, {
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
});
const 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.connectOrCreate.create.dataSource,
date: <Date>data.date,
symbol: data.SymbolProfile.connectOrCreate.create.symbol
}
]);
}
2021-04-13 21:53:58 +02:00
await this.cacheService.flush();
2021-04-13 21:53:58 +02:00
delete data.accountId;
delete data.assetClass;
delete data.assetSubClass;
delete data.currency;
delete data.dataSource;
delete data.symbol;
delete data.userId;
const orderData: Prisma.OrderCreateInput = data;
2021-08-07 22:38:07 +02:00
return this.prismaService.order.create({
data: {
...orderData,
Account,
isDraft
}
2021-04-13 21:53:58 +02:00
});
}
public async deleteOrder(
where: Prisma.OrderWhereUniqueInput
2021-04-13 21:53:58 +02:00
): Promise<Order> {
const order = await this.prismaService.order.delete({
2021-04-13 21:53:58 +02:00
where
});
if (order.type === 'ITEM') {
await this.symbolProfileService.deleteById(order.symbolProfileId);
}
return order;
2021-04-13 21:53:58 +02:00
}
public async getOrders({
filters,
includeDrafts = false,
types,
userCurrency,
userId
}: {
filters?: Filter[];
includeDrafts?: boolean;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
}): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId };
const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
if (filtersByAccount?.length > 0) {
where.accountId = {
in: filtersByAccount.map(({ id }) => {
return id;
})
};
}
if (includeDrafts === false) {
where.isDraft = false;
}
if (filtersByTag?.length > 0) {
where.tags = {
some: {
OR: filtersByTag.map(({ id }) => {
return { id };
})
}
};
}
if (types) {
where.OR = types.map((type) => {
return {
type: {
equals: type
}
};
});
}
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
SymbolProfile: true,
tags: true
},
orderBy: { date: 'asc' }
})
).map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return {
...order,
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
}
public async updateOrder({
data,
where
}: {
data: Prisma.OrderUpdateInput & {
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
currency?: string;
dataSource?: DataSource;
symbol?: string;
};
where: Prisma.OrderWhereUniqueInput;
}): Promise<Order> {
if (data.Account.connect.id_userId.id === null) {
delete data.Account;
}
let isDraft = false;
if (data.type === 'ITEM') {
delete data.SymbolProfile.connect;
} 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
this.dataGatheringService.gatherSymbols([
{
dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource,
date: <Date>data.date,
symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
}
]);
}
}
2021-04-13 21:53:58 +02:00
await this.cacheService.flush();
2021-04-13 21:53:58 +02:00
delete data.assetClass;
delete data.assetSubClass;
delete data.currency;
delete data.dataSource;
delete data.symbol;
2021-08-07 22:38:07 +02:00
return this.prismaService.order.update({
data: {
...data,
isDraft
},
2021-04-13 21:53:58 +02:00
where
});
}
}