Feature/improve support for draft transactions (#265)

* Improve support for draft transactions

* Update changelog
pull/266/head
Thomas 3 years ago committed by GitHub
parent 2bd9309827
commit bb76ace95d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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
});

@ -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();
}
}

@ -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' }]

@ -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 } }
});
}
}
}

@ -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
}
}
});
}
}

@ -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
});
}

@ -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(

@ -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() {

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

@ -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;
}

@ -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>

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

@ -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])

Loading…
Cancel
Save