diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ef49072..0f251472a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 ### Changed diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 750ef1fc9..ef98db016 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -69,8 +69,6 @@ export class AccountService { where: Prisma.AccountWhereUniqueInput, aUserId: string ): Promise { - this.redisCacheService.remove(`${aUserId}.portfolio`); - return this.prisma.account.delete({ where }); diff --git a/apps/api/src/app/cache/cache.controller.ts b/apps/api/src/app/cache/cache.controller.ts index 3e7fdbc76..23a358d20 100644 --- a/apps/api/src/app/cache/cache.controller.ts +++ b/apps/api/src/app/cache/cache.controller.ts @@ -21,6 +21,6 @@ export class CacheController { public async flushCache(): Promise { this.redisCacheService.reset(); - return this.cacheService.flush(this.request.user.id); + return this.cacheService.flush(); } } diff --git a/apps/api/src/app/cache/cache.service.ts b/apps/api/src/app/cache/cache.service.ts index 357c0fdff..af0885fb3 100644 --- a/apps/api/src/app/cache/cache.service.ts +++ b/apps/api/src/app/cache/cache.service.ts @@ -5,7 +5,7 @@ import { Injectable } from '@nestjs/common'; export class CacheService { public constructor(private prisma: PrismaService) {} - public async flush(aUserId: string): Promise { + public async flush(): Promise { await this.prisma.property.deleteMany({ where: { OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }] diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 6038a0783..91577b263 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -24,20 +24,17 @@ export class ImportService { type, unitPrice } of orders) { - await this.orderService.createOrder( - { - currency, - dataSource, - fee, - quantity, - symbol, - type, - unitPrice, - date: parseISO((date)), - User: { connect: { id: userId } } - }, - userId - ); + await this.orderService.createOrder({ + currency, + dataSource, + fee, + quantity, + symbol, + type, + unitPrice, + date: parseISO((date)), + User: { connect: { id: userId } } + }); } } } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 6e7a15026..d54bbcee2 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -52,15 +52,12 @@ export class OrderController { ); } - return this.orderService.deleteOrder( - { - id_userId: { - id, - userId: this.request.user.id - } - }, - this.request.user.id - ); + return this.orderService.deleteOrder({ + id_userId: { + id, + userId: this.request.user.id + } + }); } @Get() @@ -135,33 +132,30 @@ export class OrderController { const accountId = data.accountId; delete data.accountId; - return this.orderService.createOrder( - { - ...data, - Account: { - connect: { - id_userId: { id: accountId, userId: this.request.user.id } - } - }, - date, - SymbolProfile: { - connectOrCreate: { - where: { - dataSource_symbol: { - dataSource: data.dataSource, - symbol: data.symbol - } - }, - create: { + return this.orderService.createOrder({ + ...data, + Account: { + connect: { + id_userId: { id: accountId, userId: this.request.user.id } + } + }, + date, + SymbolProfile: { + connectOrCreate: { + where: { + dataSource_symbol: { dataSource: data.dataSource, 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') @@ -198,26 +192,23 @@ export class OrderController { const accountId = data.accountId; delete data.accountId; - return this.orderService.updateOrder( - { - data: { - ...data, - date, - Account: { - connect: { - 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 + return this.orderService.updateOrder({ + data: { + ...data, + date, + Account: { + connect: { + id_userId: { id: accountId, 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 + } + } + }); } } diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index b7c4d0b29..6012f1384 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -6,14 +6,12 @@ import { DataSource, Order, Prisma } from '@prisma/client'; import { endOfToday, isAfter } from 'date-fns'; import { CacheService } from '../cache/cache.service'; -import { RedisCacheService } from '../redis-cache/redis-cache.service'; @Injectable() export class OrderService { public constructor( private readonly cacheService: CacheService, private readonly dataGatheringService: DataGatheringService, - private readonly redisCacheService: RedisCacheService, private prisma: PrismaService ) {} @@ -45,13 +43,10 @@ export class OrderService { }); } - public async createOrder( - data: Prisma.OrderCreateInput, - aUserId: string - ): Promise { - this.redisCacheService.remove(`${aUserId}.portfolio`); + public async createOrder(data: Prisma.OrderCreateInput): Promise { + const isDraft = isAfter(data.date as Date, endOfToday()); - if (!isAfter(data.date as Date, endOfToday())) { + if (!isDraft) { // Gather symbol data of order in the background, if not draft this.dataGatheringService.gatherSymbols([ { @@ -64,48 +59,75 @@ export class OrderService { this.dataGatheringService.gatherProfileData([data.symbol]); - await this.cacheService.flush(aUserId); + await this.cacheService.flush(); return this.prisma.order.create({ - data + data: { + ...data, + isDraft + } }); } public async deleteOrder( - where: Prisma.OrderWhereUniqueInput, - aUserId: string + where: Prisma.OrderWhereUniqueInput ): Promise { - this.redisCacheService.remove(`${aUserId}.portfolio`); - return this.prisma.order.delete({ where }); } - public async updateOrder( - params: { - where: Prisma.OrderWhereUniqueInput; - data: Prisma.OrderUpdateInput; - }, - aUserId: string - ): Promise { + public getOrders({ + includeDrafts = false, + userId + }: { + includeDrafts?: boolean; + userId: string; + }) { + 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 { 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 - this.dataGatheringService.gatherSymbols([ - { - dataSource: data.dataSource, - date: data.date, - symbol: data.symbol - } - ]); + if (!isDraft) { + // Gather symbol data of order in the background, if not draft + this.dataGatheringService.gatherSymbols([ + { + dataSource: data.dataSource, + date: data.date, + symbol: data.symbol + } + ]); + } - await this.cacheService.flush(aUserId); + await this.cacheService.flush(); return this.prisma.order.update({ - data, + data: { + ...data, + isDraft + }, where }); } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 55bf6c277..333042bd1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -38,7 +38,12 @@ import { } from '@ghostfolio/common/types'; import { Inject, Injectable } from '@nestjs/common'; 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 { endOfToday, @@ -83,7 +88,10 @@ export class PortfolioService { this.request.user.Settings.currency ); - const { transactionPoints } = await this.getTransactionPoints(userId); + const { transactionPoints } = await this.getTransactionPoints({ + userId, + includeDrafts: true + }); portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { return []; @@ -108,7 +116,7 @@ export class PortfolioService { this.request.user.Settings.currency ); - const { transactionPoints } = await this.getTransactionPoints(userId); + const { transactionPoints } = await this.getTransactionPoints({ userId }); portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { return []; @@ -151,7 +159,7 @@ export class PortfolioService { userId, currency ); - const orders = await this.getOrders(userId); + const orders = await this.orderService.getOrders({ userId }); const fees = this.getFees(orders); const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY); @@ -178,9 +186,9 @@ export class PortfolioService { userCurrency ); - const { transactionPoints, orders } = await this.getTransactionPoints( + const { orders, transactionPoints } = await this.getTransactionPoints({ userId - ); + }); if (transactionPoints?.length <= 0) { return {}; @@ -258,7 +266,7 @@ export class PortfolioService { ): Promise { 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 ); @@ -453,7 +461,7 @@ export class PortfolioService { this.request.user.Settings.currency ); - const { transactionPoints } = await this.getTransactionPoints(userId); + const { transactionPoints } = await this.getTransactionPoints({ userId }); if (transactionPoints?.length <= 0) { return { @@ -514,7 +522,7 @@ export class PortfolioService { this.request.user.Settings.currency ); - const { transactionPoints } = await this.getTransactionPoints(userId); + const { transactionPoints } = await this.getTransactionPoints({ userId }); if (transactionPoints?.length <= 0) { return { @@ -576,9 +584,9 @@ export class PortfolioService { const userId = await this.getUserId(impersonationId); const baseCurrency = this.request.user.Settings.currency; - const { transactionPoints, orders } = await this.getTransactionPoints( + const { orders, transactionPoints } = await this.getTransactionPoints({ userId - ); + }); if (isEmpty(orders)) { return { @@ -674,11 +682,17 @@ export class PortfolioService { return portfolioStart; } - private async getTransactionPoints(userId: string): Promise<{ + private async getTransactionPoints({ + includeDrafts = false, + userId + }: { + includeDrafts?: boolean; + userId: string; + }): Promise<{ transactionPoints: TransactionPoint[]; orders: OrderWithAccount[]; }> { - const orders = await this.getOrders(userId); + const orders = await this.orderService.getOrders({ includeDrafts, userId }); if (orders.length <= 0) { return { transactionPoints: [], orders: [] }; @@ -750,19 +764,6 @@ export class PortfolioService { 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) { const impersonationUserId = await this.impersonationService.validateImpersonationId( diff --git a/apps/api/src/models/order.ts b/apps/api/src/models/order.ts index 621e07de8..c6ac4f2da 100644 --- a/apps/api/src/models/order.ts +++ b/apps/api/src/models/order.ts @@ -1,5 +1,4 @@ import { Account, Currency, SymbolProfile } from '@prisma/client'; -import { endOfToday, isAfter, parseISO } from 'date-fns'; import { v4 as uuidv4 } from 'uuid'; import { IOrder } from '../services/interfaces/interfaces'; @@ -11,6 +10,7 @@ export class Order { private fee: number; private date: string; private id: string; + private isDraft: boolean; private quantity: number; private symbol: string; private symbolProfile: SymbolProfile; @@ -24,6 +24,7 @@ export class Order { this.fee = data.fee; this.date = data.date; this.id = data.id || uuidv4(); + this.isDraft = data.isDraft; this.quantity = data.quantity; this.symbol = data.symbol; this.symbolProfile = data.symbolProfile; @@ -54,7 +55,7 @@ export class Order { } public getIsDraft() { - return isAfter(parseISO(this.date), endOfToday()); + return this.isDraft; } public getQuantity() { diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 845302d05..50fae5559 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -23,6 +23,7 @@ export interface IOrder { date: string; fee: number; id?: string; + isDraft: boolean; quantity: number; symbol: string; symbolProfile: SymbolProfile; diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index adbdac16b..796b182ab 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -154,8 +154,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { } } - private isInFuture(aContext: any, aValue: any) { - return isAfter(new Date(aContext?.p0?.parsed?.x), new Date()) + private isInFuture(aContext: any, aValue: T) { + return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) ? aValue : undefined; } diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.html b/apps/client/src/app/components/transactions-table/transactions-table.component.html index 6cf45b552..2fdee853d 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.html +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.html @@ -96,10 +96,7 @@
{{ element.symbol | gfSymbol }} - Draft
diff --git a/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql b/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql new file mode 100644 index 000000000..51970ec3c --- /dev/null +++ b/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3665c5fd..d6736e773 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -81,6 +81,7 @@ model Order { date DateTime fee Float id String @default(uuid()) + isDraft Boolean @default(false) quantity Float symbol String SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])