You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

344 lines
8.2 KiB

import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
Account,
AccountBalance,
Order,
Platform,
Prisma,
SymbolProfile,
Tag
} from '@prisma/client';
import { Big } from 'big.js';
import { format } from 'date-fns';
import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface';
@Injectable()
export class AccountService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService,
private readonly tagService: TagService
) {}
public async account({
id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const [account] = await this.accounts({
where: id_userId
});
return account;
}
public async accountWithActivities(
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput,
accountInclude: Prisma.AccountInclude
): Promise<
Account & {
activities?: Order[];
}
> {
return this.prismaService.account.findUnique({
include: accountInclude,
where: accountWhereUniqueInput
});
}
public async accounts(params: {
include?: Prisma.AccountInclude;
skip?: number;
take?: number;
cursor?: Prisma.AccountWhereUniqueInput;
where?: Prisma.AccountWhereInput;
orderBy?: Prisma.AccountOrderByWithRelationInput;
}): Promise<
(Account & {
activities?: (Order & { SymbolProfile?: SymbolProfile })[];
balances?: AccountBalance[];
platform?: Platform;
tags?: Tag[];
})[]
> {
const { include = {}, skip, take, cursor, where, orderBy } = params;
const isBalancesIncluded = !!include.balances;
const isTagsIncluded = !!include.tags;
include.balances = {
orderBy: { date: 'desc' },
...(isBalancesIncluded ? {} : { take: 1 })
};
if (isTagsIncluded) {
include.tags = {
include: {
tag: true
}
};
}
const accounts = await this.prismaService.account.findMany({
cursor,
include,
orderBy,
skip,
take,
where
});
return accounts.map((account) => {
const result = {
...account,
balance: account.balances[0]?.value ?? 0,
tags: isTagsIncluded
? (account.tags as unknown as { tag: Tag }[]).map(({ tag }) => {
return tag;
})
: undefined
};
if (!isBalancesIncluded) {
delete result.balances;
}
if (!isTagsIncluded) {
delete result.tags;
}
return result;
});
}
public async createAccount(
data: Prisma.AccountCreateInput,
aUserId: string,
tagIds?: string[]
): Promise<Account> {
await this.tagService.validateTagIds({ tagIds, userId: aUserId });
const account = await this.prismaService.account.create({
data: {
...data,
tags: tagIds
? {
create: tagIds.map((tagId) => {
return {
tag: { connect: { id: tagId } }
};
})
}
: undefined
}
});
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: account.id,
balance: data.balance,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async deleteAccount(
where: Prisma.AccountWhereUniqueInput
): Promise<Account> {
const account = await this.prismaService.account.delete({
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async getAccounts(aUserId: string): Promise<Account[]> {
const accounts = await this.accounts({
include: {
activities: true,
platform: true,
tags: true
},
orderBy: { name: 'asc' },
where: { userId: aUserId }
});
return accounts.map((account) => {
let activitiesCount = 0;
for (const { isDraft } of account.activities) {
if (!isDraft) {
activitiesCount += 1;
}
}
const result = { ...account, activitiesCount };
delete result.activities;
return result;
});
}
public async getCashDetails({
currency,
filters = [],
userId,
withExcludedAccounts = false
}: {
currency: string;
filters?: Filter[];
userId: string;
withExcludedAccounts?: boolean;
}): Promise<CashDetails> {
let totalCashBalanceInBaseCurrency = new Big(0);
const where: Prisma.AccountWhereInput = {
userId
};
if (withExcludedAccounts === false) {
where.AND = [WHERE_ACCOUNT_NOT_EXCLUDED];
}
const { ACCOUNT: filtersByAccount = [] } = groupBy(filters, ({ type }) => {
return type;
});
if (filtersByAccount.length > 0) {
where.id = {
in: filtersByAccount.map(({ id }) => {
return id;
})
};
}
const accounts = await this.accounts({ where });
for (const account of accounts) {
totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus(
this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
currency
)
);
}
return {
accounts,
balanceInBaseCurrency: totalCashBalanceInBaseCurrency.toNumber()
};
}
public async updateAccount(
params: {
data: Prisma.AccountUpdateInput;
where: Prisma.AccountWhereUniqueInput;
},
aUserId: string,
tagIds?: string[]
): Promise<Account> {
const { data, where } = params;
await this.tagService.validateTagIds({ tagIds, userId: aUserId });
const account = await this.prismaService.account.update({
data: {
...data,
tags: tagIds
? {
create: tagIds.map((tagId) => {
return {
tag: { connect: { id: tagId } }
};
}),
deleteMany: {}
}
: undefined
},
where
});
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: account.id,
balance: data.balance as number,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async updateAccountBalance({
accountId,
amount,
currency,
date = new Date(),
userId
}: {
accountId: string;
amount: number;
currency: string;
date?: Date;
userId: string;
}) {
const { balance, currency: currencyOfAccount } = await this.account({
id_userId: {
userId,
id: accountId
}
});
const amountInCurrencyOfAccount =
await this.exchangeRateDataService.toCurrencyAtDate(
amount,
currency,
currencyOfAccount,
date
);
if (amountInCurrencyOfAccount) {
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId,
userId,
balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber(),
date: date.toISOString()
});
}
}
}