Feature/rename orders to activities in import and export (#786)

* Rename orders to activities

* Update changelog
pull/787/head
Thomas Kaul 2 years ago committed by GitHub
parent e986310302
commit 1214127ec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Renamed `orders` to `activities` in import and export functionality
- Improved the pricing page - Improved the pricing page
## 1.130.0 - 30.03.2022 ## 1.130.0 - 30.03.2022

@ -1,13 +1,6 @@
import { Export } from '@ghostfolio/common/interfaces'; import { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
Controller,
Get,
Headers,
Inject,
Query,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';

@ -14,7 +14,7 @@ export class ExportService {
activityIds?: string[]; activityIds?: string[];
userId: string; userId: string;
}): Promise<Export> { }): Promise<Export> {
let orders = await this.prismaService.order.findMany({ let activities = await this.prismaService.order.findMany({
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
select: { select: {
accountId: true, accountId: true,
@ -30,14 +30,14 @@ export class ExportService {
}); });
if (activityIds) { if (activityIds) {
orders = orders.filter((order) => { activities = activities.filter((activity) => {
return activityIds.includes(order.id); return activityIds.includes(activity.id);
}); });
} }
return { return {
meta: { date: new Date().toISOString(), version: environment.version }, meta: { date: new Date().toISOString(), version: environment.version },
orders: orders.map( activities: activities.map(
({ ({
accountId, accountId,
date, date,

@ -6,5 +6,5 @@ export class ImportDataDto {
@IsArray() @IsArray()
@Type(() => CreateOrderDto) @Type(() => CreateOrderDto)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
orders: CreateOrderDto[]; activities: CreateOrderDto[];
} }

@ -36,7 +36,7 @@ export class ImportController {
try { try {
return await this.importService.import({ return await this.importService.import({
orders: importData.orders, activities: importData.activities,
userId: this.request.user.id userId: this.request.user.id
}); });
} catch (error) { } catch (error) {

@ -16,23 +16,23 @@ export class ImportService {
) {} ) {}
public async import({ public async import({
orders, activities,
userId userId
}: { }: {
orders: Partial<CreateOrderDto>[]; activities: Partial<CreateOrderDto>[];
userId: string; userId: string;
}): Promise<void> { }): Promise<void> {
for (const order of orders) { for (const activity of activities) {
if (!order.dataSource) { if (!activity.dataSource) {
if (order.type === 'ITEM') { if (activity.type === 'ITEM') {
order.dataSource = 'MANUAL'; activity.dataSource = 'MANUAL';
} else { } else {
order.dataSource = this.dataProviderService.getPrimaryDataSource(); activity.dataSource = this.dataProviderService.getPrimaryDataSource();
} }
} }
} }
await this.validateOrders({ orders, userId }); await this.validateActivities({ activities, userId });
const accountIds = (await this.accountService.getAccounts(userId)).map( const accountIds = (await this.accountService.getAccounts(userId)).map(
(account) => { (account) => {
@ -50,7 +50,7 @@ export class ImportService {
symbol, symbol,
type, type,
unitPrice unitPrice
} of orders) { } of activities) {
await this.orderService.createOrder({ await this.orderService.createOrder({
fee, fee,
quantity, quantity,
@ -79,24 +79,24 @@ export class ImportService {
} }
} }
private async validateOrders({ private async validateActivities({
orders, activities,
userId userId
}: { }: {
orders: Partial<CreateOrderDto>[]; activities: Partial<CreateOrderDto>[];
userId: string; userId: string;
}) { }) {
if ( if (
orders?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT') activities?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT')
) { ) {
throw new Error( throw new Error(
`Too many transactions (${this.configurationService.get( `Too many activities (${this.configurationService.get(
'MAX_ORDERS_TO_IMPORT' 'MAX_ORDERS_TO_IMPORT'
)} at most)` )} at most)`
); );
} }
const existingOrders = await this.orderService.orders({ const existingActivities = await this.orderService.orders({
include: { SymbolProfile: true }, include: { SymbolProfile: true },
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
where: { userId } where: { userId }
@ -105,22 +105,22 @@ export class ImportService {
for (const [ for (const [
index, index,
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice } { currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
] of orders.entries()) { ] of activities.entries()) {
const duplicateOrder = existingOrders.find((order) => { const duplicateActivity = existingActivities.find((activity) => {
return ( return (
order.SymbolProfile.currency === currency && activity.SymbolProfile.currency === currency &&
order.SymbolProfile.dataSource === dataSource && activity.SymbolProfile.dataSource === dataSource &&
isSameDay(order.date, parseISO(<string>(<unknown>date))) && isSameDay(activity.date, parseISO(<string>(<unknown>date))) &&
order.fee === fee && activity.fee === fee &&
order.quantity === quantity && activity.quantity === quantity &&
order.SymbolProfile.symbol === symbol && activity.SymbolProfile.symbol === symbol &&
order.type === type && activity.type === type &&
order.unitPrice === unitPrice activity.unitPrice === unitPrice
); );
}); });
if (duplicateOrder) { if (duplicateActivity) {
throw new Error(`orders.${index} is a duplicate transaction`); throw new Error(`activities.${index} is a duplicate activity`);
} }
if (dataSource !== 'MANUAL') { if (dataSource !== 'MANUAL') {
@ -130,13 +130,13 @@ export class ImportService {
if (quotes[symbol] === undefined) { if (quotes[symbol] === undefined) {
throw new Error( throw new Error(
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
); );
} }
if (quotes[symbol].currency !== currency) { if (quotes[symbol].currency !== currency) {
throw new Error( throw new Error(
`orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"` `activities.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"`
); );
} }
} }

@ -27,12 +27,12 @@ export class ImportTransactionDialog implements OnDestroy {
public ngOnInit() { public ngOnInit() {
for (const message of this.data.messages) { for (const message of this.data.messages) {
if (message.includes('orders.')) { if (message.includes('activities.')) {
let [index] = message.split(' '); let [index] = message.split(' ');
index = index.replace('orders.', ''); index = index.replace('activities.', '');
[index] = index.split('.'); [index] = index.split('.');
this.details.push(this.data.orders[index]); this.details.push(this.data.activities[index]);
} else { } else {
this.details.push(''); this.details.push('');
} }

@ -1,5 +1,5 @@
export interface ImportTransactionDialogParams { export interface ImportTransactionDialogParams {
activities: any[];
deviceType: string; deviceType: string;
messages: string[]; messages: string[];
orders: any[];
} }

@ -185,19 +185,31 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
if (file.name.endsWith('.json')) { if (file.name.endsWith('.json')) {
const content = JSON.parse(fileContent); const content = JSON.parse(fileContent);
if (!isArray(content.orders)) { if (!isArray(content.activities)) {
throw new Error(); if (isArray(content.orders)) {
this.handleImportError({
activities: [],
error: {
error: {
message: [`orders needs to be renamed to activities`]
}
}
});
return;
} else {
throw new Error();
}
} }
try { try {
await this.importTransactionsService.importJson({ await this.importTransactionsService.importJson({
content: content.orders content: content.activities
}); });
this.handleImportSuccess(); this.handleImportSuccess();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.handleImportError({ error, orders: content.orders }); this.handleImportError({ error, activities: content.activities });
} }
return; return;
@ -212,10 +224,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.handleImportError({ this.handleImportError({
activities: error?.activities ?? [],
error: { error: {
error: { message: error?.error?.message ?? [error?.message] } error: { message: error?.error?.message ?? [error?.message] }
}, }
orders: error?.orders ?? []
}); });
} }
@ -226,8 +238,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.handleImportError({ this.handleImportError({
error: { error: { message: ['Unexpected format'] } }, activities: [],
orders: [] error: { error: { message: ['Unexpected format'] } }
}); });
} }
}; };
@ -281,12 +293,18 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private handleImportError({ error, orders }: { error: any; orders: any[] }) { private handleImportError({
activities,
error
}: {
activities: any[];
error: any;
}) {
this.snackBar.dismiss(); this.snackBar.dismiss();
this.dialog.open(ImportTransactionDialog, { this.dialog.open(ImportTransactionDialog, {
data: { data: {
orders, activities,
deviceType: this.deviceType, deviceType: this.deviceType,
messages: error?.error?.message messages: error?.error?.message
}, },

@ -37,9 +37,9 @@ export class ImportTransactionsService {
skipEmptyLines: true skipEmptyLines: true
}).data; }).data;
const orders: CreateOrderDto[] = []; const activities: CreateOrderDto[] = [];
for (const [index, item] of content.entries()) { for (const [index, item] of content.entries()) {
orders.push({ activities.push({
accountId: this.parseAccount({ item, userAccounts }), accountId: this.parseAccount({ item, userAccounts }),
currency: this.parseCurrency({ content, index, item }), currency: this.parseCurrency({ content, index, item }),
dataSource: this.parseDataSource({ item }), dataSource: this.parseDataSource({ item }),
@ -52,13 +52,13 @@ export class ImportTransactionsService {
}); });
} }
await this.importJson({ content: orders }); await this.importJson({ content: activities });
} }
public importJson({ content }: { content: CreateOrderDto[] }): Promise<void> { public importJson({ content }: { content: CreateOrderDto[] }): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.postImport({ this.postImport({
orders: content activities: content
}) })
.pipe( .pipe(
catchError((error) => { catchError((error) => {
@ -121,7 +121,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.currency is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.currency is not valid`
};
} }
private parseDataSource({ item }: { item: any }) { private parseDataSource({ item }: { item: any }) {
@ -164,7 +167,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.date is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.date is not valid`
};
} }
private parseFee({ private parseFee({
@ -184,7 +190,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.fee is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.fee is not valid`
};
} }
private parseQuantity({ private parseQuantity({
@ -204,7 +213,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.quantity is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.quantity is not valid`
};
} }
private parseSymbol({ private parseSymbol({
@ -224,7 +236,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.symbol is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.symbol is not valid`
};
} }
private parseType({ private parseType({
@ -255,7 +270,10 @@ export class ImportTransactionsService {
} }
} }
throw { message: `orders.${index}.type is not valid`, orders: content }; throw {
activities: content,
message: `activities.${index}.type is not valid`
};
} }
private parseUnitPrice({ private parseUnitPrice({
@ -276,12 +294,12 @@ export class ImportTransactionsService {
} }
throw { throw {
message: `orders.${index}.unitPrice is not valid`, activities: content,
orders: content message: `activities.${index}.unitPrice is not valid`
}; };
} }
private postImport(aImportData: { orders: CreateOrderDto[] }) { private postImport(aImportData: { activities: CreateOrderDto[] }) {
return this.http.post<void>('/api/v1/import', aImportData); return this.http.post<void>('/api/v1/import', aImportData);
} }
} }

@ -5,5 +5,5 @@ export interface Export {
date: string; date: string;
version: string; version: string;
}; };
orders: Partial<Order>[]; activities: Partial<Order>[];
} }

@ -3,7 +3,7 @@
"date": "2021-01-01T00:00:00.000Z", "date": "2021-01-01T00:00:00.000Z",
"version": "dev" "version": "dev"
}, },
"orders": [ "activities": [
{ {
"currency": "USD", "currency": "USD",
"dataSource": "YAHOO", "dataSource": "YAHOO",

@ -3,7 +3,7 @@
"date": "2021-01-01T00:00:00.000Z", "date": "2021-01-01T00:00:00.000Z",
"version": "dev" "version": "dev"
}, },
"orders": [ "activities": [
{ {
"currency": "USD", "currency": "USD",
"dataSource": "YAHOO", "dataSource": "YAHOO",

@ -0,0 +1,39 @@
{
"meta": {
"date": "2022-04-01T00:00:00.000Z",
"version": "dev"
},
"activities": [
{
"accountId": null,
"date": "2021-12-31T23:00:00.000Z",
"fee": 0,
"quantity": 1,
"type": "ITEM",
"unitPrice": 500000,
"currency": "USD",
"dataSource": "MANUAL",
"symbol": "Penthouse Apartment"
},
{
"date": "2021-11-16T23:00:00.000Z",
"fee": 0,
"quantity": 5,
"type": "DIVIDEND",
"unitPrice": 0.62,
"currency": "USD",
"dataSource": "YAHOO",
"symbol": "MSFT"
},
{
"date": "2021-09-15T22:00:00.000Z",
"fee": 19,
"quantity": 5,
"type": "BUY",
"unitPrice": 298.58,
"currency": "USD",
"dataSource": "YAHOO",
"symbol": "MSFT"
}
]
}
Loading…
Cancel
Save