Feature/improve support for draft transactions (#265)
* Improve support for draft transactions * Update changelog
This commit is contained in:
parent
2bd9309827
commit
bb76ace95d
10
CHANGELOG.md
10
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
|
||||
|
@ -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
|
||||
});
|
||||
|
2
apps/api/src/app/cache/cache.controller.ts
vendored
2
apps/api/src/app/cache/cache.controller.ts
vendored
@ -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();
|
||||
}
|
||||
}
|
||||
|
2
apps/api/src/app/cache/cache.service.ts
vendored
2
apps/api/src/app/cache/cache.service.ts
vendored
@ -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…
x
Reference in New Issue
Block a user