From bb76ace95d7d84e9eb35cc23daa526e6ca239a52 Mon Sep 17 00:00:00 2001
From: Thomas <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Aug 2021 20:52:55 +0200
Subject: [PATCH] Feature/improve support for draft transactions (#265)

* Improve support for draft transactions

* Update changelog
---
 CHANGELOG.md                                  | 10 ++
 apps/api/src/app/account/account.service.ts   |  2 -
 apps/api/src/app/cache/cache.controller.ts    |  2 +-
 apps/api/src/app/cache/cache.service.ts       |  2 +-
 apps/api/src/app/import/import.service.ts     | 25 +++--
 apps/api/src/app/order/order.controller.ts    | 91 +++++++++----------
 apps/api/src/app/order/order.service.ts       | 86 +++++++++++-------
 .../src/app/portfolio/portfolio.service.ts    | 53 +++++------
 apps/api/src/models/order.ts                  |  5 +-
 .../api/src/services/interfaces/interfaces.ts |  1 +
 .../investment-chart.component.ts             |  4 +-
 .../transactions-table.component.html         |  5 +-
 .../migration.sql                             |  2 +
 prisma/schema.prisma                          |  1 +
 14 files changed, 155 insertions(+), 134 deletions(-)
 create mode 100644 prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql

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<Account> {
-    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<void> {
     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<void> {
+  public async flush(): Promise<void> {
     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(<string>(<unknown>date)),
-          User: { connect: { id: userId } }
-        },
-        userId
-      );
+      await this.orderService.createOrder({
+        currency,
+        dataSource,
+        fee,
+        quantity,
+        symbol,
+        type,
+        unitPrice,
+        date: parseISO(<string>(<unknown>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<Order> {
-    this.redisCacheService.remove(`${aUserId}.portfolio`);
+  public async createOrder(data: Prisma.OrderCreateInput): Promise<Order> {
+    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<Order> {
-    this.redisCacheService.remove(`${aUserId}.portfolio`);
-
     return this.prisma.order.delete({
       where
     });
   }
 
-  public async updateOrder(
-    params: {
-      where: Prisma.OrderWhereUniqueInput;
-      data: Prisma.OrderUpdateInput;
-    },
-    aUserId: string
-  ): Promise<Order> {
+  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<Order> {
     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: <DataSource>data.dataSource,
-        date: <Date>data.date,
-        symbol: <string>data.symbol
-      }
-    ]);
+    if (!isDraft) {
+      // Gather symbol data of order in the background, if not draft
+      this.dataGatheringService.gatherSymbols([
+        {
+          dataSource: <DataSource>data.dataSource,
+          date: <Date>data.date,
+          symbol: <string>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<PortfolioPositionDetail> {
     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<T>(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 @@
     <td *matCellDef="let element" class="px-1" mat-cell>
       <div class="d-flex align-items-center">
         {{ element.symbol | gfSymbol }}
-        <span
-          *ngIf="isAfter(element.date, endOfToday)"
-          class="badge badge-secondary ml-1"
-          i18n
+        <span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
           >Draft</span
         >
       </div>
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])