Feature/improve support for draft transactions (#265)

* Improve support for draft transactions

* Update changelog
This commit is contained in:
Thomas 2021-08-07 20:52:55 +02:00 committed by GitHub
parent 2bd9309827
commit bb76ace95d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 155 additions and 134 deletions

View File

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Improved the support for future transactions (drafts)
### Todo
- Apply data migration (`yarn database:push`)
## 1.34.0 - 07.08.2021 ## 1.34.0 - 07.08.2021
### Changed ### Changed

View File

@ -69,8 +69,6 @@ export class AccountService {
where: Prisma.AccountWhereUniqueInput, where: Prisma.AccountWhereUniqueInput,
aUserId: string aUserId: string
): Promise<Account> { ): Promise<Account> {
this.redisCacheService.remove(`${aUserId}.portfolio`);
return this.prisma.account.delete({ return this.prisma.account.delete({
where where
}); });

View File

@ -21,6 +21,6 @@ export class CacheController {
public async flushCache(): Promise<void> { public async flushCache(): Promise<void> {
this.redisCacheService.reset(); this.redisCacheService.reset();
return this.cacheService.flush(this.request.user.id); return this.cacheService.flush();
} }
} }

View File

@ -5,7 +5,7 @@ import { Injectable } from '@nestjs/common';
export class CacheService { export class CacheService {
public constructor(private prisma: PrismaService) {} public constructor(private prisma: PrismaService) {}
public async flush(aUserId: string): Promise<void> { public async flush(): Promise<void> {
await this.prisma.property.deleteMany({ await this.prisma.property.deleteMany({
where: { where: {
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }] OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]

View File

@ -24,20 +24,17 @@ export class ImportService {
type, type,
unitPrice unitPrice
} of orders) { } of orders) {
await this.orderService.createOrder( await this.orderService.createOrder({
{ currency,
currency, dataSource,
dataSource, fee,
fee, quantity,
quantity, symbol,
symbol, type,
type, unitPrice,
unitPrice, date: parseISO(<string>(<unknown>date)),
date: parseISO(<string>(<unknown>date)), User: { connect: { id: userId } }
User: { connect: { id: userId } } });
},
userId
);
} }
} }
} }

View File

@ -52,15 +52,12 @@ export class OrderController {
); );
} }
return this.orderService.deleteOrder( return this.orderService.deleteOrder({
{ id_userId: {
id_userId: { id,
id, userId: this.request.user.id
userId: this.request.user.id }
} });
},
this.request.user.id
);
} }
@Get() @Get()
@ -135,33 +132,30 @@ export class OrderController {
const accountId = data.accountId; const accountId = data.accountId;
delete data.accountId; delete data.accountId;
return this.orderService.createOrder( return this.orderService.createOrder({
{ ...data,
...data, Account: {
Account: { connect: {
connect: { id_userId: { id: accountId, userId: this.request.user.id }
id_userId: { id: accountId, userId: this.request.user.id } }
} },
}, date,
date, SymbolProfile: {
SymbolProfile: { connectOrCreate: {
connectOrCreate: { where: {
where: { dataSource_symbol: {
dataSource_symbol: {
dataSource: data.dataSource,
symbol: data.symbol
}
},
create: {
dataSource: data.dataSource, dataSource: data.dataSource,
symbol: data.symbol symbol: data.symbol
} }
},
create: {
dataSource: data.dataSource,
symbol: data.symbol
} }
}, }
User: { connect: { id: this.request.user.id } }
}, },
this.request.user.id User: { connect: { id: this.request.user.id } }
); });
} }
@Put(':id') @Put(':id')
@ -198,26 +192,23 @@ export class OrderController {
const accountId = data.accountId; const accountId = data.accountId;
delete data.accountId; delete data.accountId;
return this.orderService.updateOrder( return this.orderService.updateOrder({
{ data: {
data: { ...data,
...data, date,
date, Account: {
Account: { connect: {
connect: { id_userId: { id: accountId, userId: this.request.user.id }
id_userId: { id: accountId, userId: this.request.user.id }
}
},
User: { connect: { id: this.request.user.id } }
},
where: {
id_userId: {
id,
userId: this.request.user.id
} }
} },
User: { connect: { id: this.request.user.id } }
}, },
this.request.user.id where: {
); id_userId: {
id,
userId: this.request.user.id
}
}
});
} }
} }

View File

@ -6,14 +6,12 @@ import { DataSource, Order, Prisma } from '@prisma/client';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
@Injectable() @Injectable()
export class OrderService { export class OrderService {
public constructor( public constructor(
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly redisCacheService: RedisCacheService,
private prisma: PrismaService private prisma: PrismaService
) {} ) {}
@ -45,13 +43,10 @@ export class OrderService {
}); });
} }
public async createOrder( public async createOrder(data: Prisma.OrderCreateInput): Promise<Order> {
data: Prisma.OrderCreateInput, const isDraft = isAfter(data.date as Date, endOfToday());
aUserId: string
): Promise<Order> {
this.redisCacheService.remove(`${aUserId}.portfolio`);
if (!isAfter(data.date as Date, endOfToday())) { if (!isDraft) {
// Gather symbol data of order in the background, if not draft // Gather symbol data of order in the background, if not draft
this.dataGatheringService.gatherSymbols([ this.dataGatheringService.gatherSymbols([
{ {
@ -64,48 +59,75 @@ export class OrderService {
this.dataGatheringService.gatherProfileData([data.symbol]); this.dataGatheringService.gatherProfileData([data.symbol]);
await this.cacheService.flush(aUserId); await this.cacheService.flush();
return this.prisma.order.create({ return this.prisma.order.create({
data data: {
...data,
isDraft
}
}); });
} }
public async deleteOrder( public async deleteOrder(
where: Prisma.OrderWhereUniqueInput, where: Prisma.OrderWhereUniqueInput
aUserId: string
): Promise<Order> { ): Promise<Order> {
this.redisCacheService.remove(`${aUserId}.portfolio`);
return this.prisma.order.delete({ return this.prisma.order.delete({
where where
}); });
} }
public async updateOrder( public getOrders({
params: { includeDrafts = false,
where: Prisma.OrderWhereUniqueInput; userId
data: Prisma.OrderUpdateInput; }: {
}, includeDrafts?: boolean;
aUserId: string userId: string;
): Promise<Order> { }) {
const where: Prisma.OrderWhereInput = { userId };
if (includeDrafts === false) {
where.isDraft = false;
}
return this.orders({
where,
include: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Account: true,
// eslint-disable-next-line @typescript-eslint/naming-convention
SymbolProfile: true
},
orderBy: { date: 'asc' }
});
}
public async updateOrder(params: {
where: Prisma.OrderWhereUniqueInput;
data: Prisma.OrderUpdateInput;
}): Promise<Order> {
const { data, where } = params; const { data, where } = params;
this.redisCacheService.remove(`${aUserId}.portfolio`); const isDraft = isAfter(data.date as Date, endOfToday());
// Gather symbol data of order in the background if (!isDraft) {
this.dataGatheringService.gatherSymbols([ // Gather symbol data of order in the background, if not draft
{ this.dataGatheringService.gatherSymbols([
dataSource: <DataSource>data.dataSource, {
date: <Date>data.date, dataSource: <DataSource>data.dataSource,
symbol: <string>data.symbol date: <Date>data.date,
} symbol: <string>data.symbol
]); }
]);
}
await this.cacheService.flush(aUserId); await this.cacheService.flush();
return this.prisma.order.update({ return this.prisma.order.update({
data, data: {
...data,
isDraft
},
where where
}); });
} }

View File

@ -38,7 +38,12 @@ import {
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client'; import {
Currency,
DataSource,
Prisma,
Type as TypeOfOrder
} from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
endOfToday, endOfToday,
@ -83,7 +88,10 @@ export class PortfolioService {
this.request.user.Settings.currency this.request.user.Settings.currency
); );
const { transactionPoints } = await this.getTransactionPoints(userId); const { transactionPoints } = await this.getTransactionPoints({
userId,
includeDrafts: true
});
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return []; return [];
@ -108,7 +116,7 @@ export class PortfolioService {
this.request.user.Settings.currency this.request.user.Settings.currency
); );
const { transactionPoints } = await this.getTransactionPoints(userId); const { transactionPoints } = await this.getTransactionPoints({ userId });
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return []; return [];
@ -151,7 +159,7 @@ export class PortfolioService {
userId, userId,
currency currency
); );
const orders = await this.getOrders(userId); const orders = await this.orderService.getOrders({ userId });
const fees = this.getFees(orders); const fees = this.getFees(orders);
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY); const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
@ -178,9 +186,9 @@ export class PortfolioService {
userCurrency userCurrency
); );
const { transactionPoints, orders } = await this.getTransactionPoints( const { orders, transactionPoints } = await this.getTransactionPoints({
userId userId
); });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return {}; return {};
@ -258,7 +266,7 @@ export class PortfolioService {
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
const userId = await this.getUserId(aImpersonationId); const userId = await this.getUserId(aImpersonationId);
const orders = (await this.getOrders(userId)).filter( const orders = (await this.orderService.getOrders({ userId })).filter(
(order) => order.symbol === aSymbol (order) => order.symbol === aSymbol
); );
@ -453,7 +461,7 @@ export class PortfolioService {
this.request.user.Settings.currency this.request.user.Settings.currency
); );
const { transactionPoints } = await this.getTransactionPoints(userId); const { transactionPoints } = await this.getTransactionPoints({ userId });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { return {
@ -514,7 +522,7 @@ export class PortfolioService {
this.request.user.Settings.currency this.request.user.Settings.currency
); );
const { transactionPoints } = await this.getTransactionPoints(userId); const { transactionPoints } = await this.getTransactionPoints({ userId });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { return {
@ -576,9 +584,9 @@ export class PortfolioService {
const userId = await this.getUserId(impersonationId); const userId = await this.getUserId(impersonationId);
const baseCurrency = this.request.user.Settings.currency; const baseCurrency = this.request.user.Settings.currency;
const { transactionPoints, orders } = await this.getTransactionPoints( const { orders, transactionPoints } = await this.getTransactionPoints({
userId userId
); });
if (isEmpty(orders)) { if (isEmpty(orders)) {
return { return {
@ -674,11 +682,17 @@ export class PortfolioService {
return portfolioStart; return portfolioStart;
} }
private async getTransactionPoints(userId: string): Promise<{ private async getTransactionPoints({
includeDrafts = false,
userId
}: {
includeDrafts?: boolean;
userId: string;
}): Promise<{
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
}> { }> {
const orders = await this.getOrders(userId); const orders = await this.orderService.getOrders({ includeDrafts, userId });
if (orders.length <= 0) { if (orders.length <= 0) {
return { transactionPoints: [], orders: [] }; return { transactionPoints: [], orders: [] };
@ -750,19 +764,6 @@ export class PortfolioService {
return accounts; return accounts;
} }
private getOrders(aUserId: string) {
return this.orderService.orders({
include: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Account: true,
// eslint-disable-next-line @typescript-eslint/naming-convention
SymbolProfile: true
},
orderBy: { date: 'asc' },
where: { userId: aUserId }
});
}
private async getUserId(aImpersonationId: string) { private async getUserId(aImpersonationId: string) {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(

View File

@ -1,5 +1,4 @@
import { Account, Currency, SymbolProfile } from '@prisma/client'; import { Account, Currency, SymbolProfile } from '@prisma/client';
import { endOfToday, isAfter, parseISO } from 'date-fns';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { IOrder } from '../services/interfaces/interfaces'; import { IOrder } from '../services/interfaces/interfaces';
@ -11,6 +10,7 @@ export class Order {
private fee: number; private fee: number;
private date: string; private date: string;
private id: string; private id: string;
private isDraft: boolean;
private quantity: number; private quantity: number;
private symbol: string; private symbol: string;
private symbolProfile: SymbolProfile; private symbolProfile: SymbolProfile;
@ -24,6 +24,7 @@ export class Order {
this.fee = data.fee; this.fee = data.fee;
this.date = data.date; this.date = data.date;
this.id = data.id || uuidv4(); this.id = data.id || uuidv4();
this.isDraft = data.isDraft;
this.quantity = data.quantity; this.quantity = data.quantity;
this.symbol = data.symbol; this.symbol = data.symbol;
this.symbolProfile = data.symbolProfile; this.symbolProfile = data.symbolProfile;
@ -54,7 +55,7 @@ export class Order {
} }
public getIsDraft() { public getIsDraft() {
return isAfter(parseISO(this.date), endOfToday()); return this.isDraft;
} }
public getQuantity() { public getQuantity() {

View File

@ -23,6 +23,7 @@ export interface IOrder {
date: string; date: string;
fee: number; fee: number;
id?: string; id?: string;
isDraft: boolean;
quantity: number; quantity: number;
symbol: string; symbol: string;
symbolProfile: SymbolProfile; symbolProfile: SymbolProfile;

View File

@ -154,8 +154,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
private isInFuture(aContext: any, aValue: any) { private isInFuture<T>(aContext: any, aValue: T) {
return isAfter(new Date(aContext?.p0?.parsed?.x), new Date()) return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
? aValue ? aValue
: undefined; : undefined;
} }

View File

@ -96,10 +96,7 @@
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
{{ element.symbol | gfSymbol }} {{ element.symbol | gfSymbol }}
<span <span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
*ngIf="isAfter(element.date, endOfToday)"
class="badge badge-secondary ml-1"
i18n
>Draft</span >Draft</span
> >
</div> </div>

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

View File

@ -81,6 +81,7 @@ model Order {
date DateTime date DateTime
fee Float fee Float
id String @default(uuid()) id String @default(uuid())
isDraft Boolean @default(false)
quantity Float quantity Float
symbol String symbol String
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id]) SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])