Compare commits

..

13 Commits

Author SHA1 Message Date
761376d72d Release 1.106.0 (#647) 2022-01-23 17:41:54 +01:00
9c086edffe Feature/extend historical data view in admin control (#646)
* Extend market data view

* Update changelog
2022-01-23 17:02:12 +01:00
585f99e4df Feature/add summary row to activities table (#645)
* Add summary row to activities table

* Update changelog
2022-01-23 11:39:30 +01:00
9d907b5eb5 Bugfix/improve the redirection on logout (#642)
* Improve logout

* Update changelog
2022-01-22 09:38:01 +01:00
ba05f5ba30 Feature/upgrade prisma to version 3.8.1 (#640)
* Upgrade prisma to version 3.8.1

* Update changelog
2022-01-22 09:36:58 +01:00
3261e3ee59 Feature/upgrade stripe dependencies (#641)
* Upgrade stripe dependencies

* Update changelog
2022-01-21 20:30:41 +01:00
5607c6bb52 Update blog url (#639) 2022-01-21 20:07:56 +01:00
1c6050d3e3 Release 1.105.0 (#638) 2022-01-20 21:35:56 +01:00
38f2930ec6 Feature/improve data provider service (#637)
* Improve data provider service

* Update changelog
2022-01-20 21:34:23 +01:00
556be61fff Bugfix/fix unresolved account names in reports (#636)
* Fix unresolved account names

* Update changelog
2022-01-19 21:28:15 +01:00
651b4bcff7 Release 1.104.0 (#631) 2022-01-16 15:45:28 +01:00
0a8d159f78 Bugfix/fix missing symbol profile data connection in import (#630)
* Fix missing symbol profile data connection in import

* Update changelog
2022-01-16 15:31:56 +01:00
1a4109ebaa Bugfix/fix fallback to load currencies directly from data provider (#629)
* Fix fallback

* Update changelog
2022-01-16 13:46:00 +01:00
29 changed files with 426 additions and 141 deletions

View File

@ -5,6 +5,44 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.106.0 - 23.01.2022
### Added
- Added the footer row with total fees and total value to the activities table
### Changed
- Extended the historical data view in the admin control panel
- Upgraded _Stripe_ dependencies
- Upgraded `prisma` from version `3.7.0` to `3.8.1`
### Fixed
- Improved the redirection on logout
## 1.105.0 - 20.01.2022
### Added
- Added support for fetching multiple symbols in the `GOOGLE_SHEETS` data provider
### Changed
- Improved the data provider with grouping by data source and thereby reducing the number of requests
### Fixed
- Fixed the unresolved account names in the _X-ray_ section
- Fixed the date conversion in the `GOOGLE_SHEETS` data provider
## 1.104.0 - 16.01.2022
### Fixed
- Fixed the fallback to load currencies directly from the data provider
- Fixed the missing symbol profile data connection in the import functionality for activities
## 1.103.0 - 13.01.2022 ## 1.103.0 - 13.01.2022
### Changed ### Changed

View File

@ -12,7 +12,7 @@
<strong>Open Source Wealth Management Software made for Humans</strong> <strong>Open Source Wealth Management Software made for Humans</strong>
</p> </p>
<p> <p>
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a> <a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
</p> </p>
<p> <p>
<a href="#contributing"> <a href="#contributing">

View File

@ -9,7 +9,8 @@ import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails AdminMarketDataDetails,
AdminMarketDataItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client'; import { Property } from '@prisma/client';
@ -56,12 +57,67 @@ export class AdminService {
} }
public async getMarketData(): Promise<AdminMarketData> { public async getMarketData(): Promise<AdminMarketData> {
return { const marketData = await this.prismaService.marketData.groupBy({
marketData: await ( _count: true,
await this.dataGatheringService.getSymbolsMax() by: ['dataSource', 'symbol']
).map((symbol) => { });
return symbol;
const currencyPairsToGather: AdminMarketDataItem[] =
this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
const marketDataItemCount =
marketData.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
dataSource,
marketDataItemCount,
symbol
};
});
const symbolProfilesToGather: AdminMarketDataItem[] = (
await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }],
select: {
_count: {
select: { Order: true }
},
dataSource: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
scraperConfiguration: true,
symbol: true
}
}) })
).map((symbolProfile) => {
const marketDataItemCount =
marketData.find((marketDataItem) => {
return (
marketDataItem.dataSource === symbolProfile.dataSource &&
marketDataItem.symbol === symbolProfile.symbol
);
})?._count ?? 0;
return {
marketDataItemCount,
activityCount: symbolProfile._count.Order,
dataSource: symbolProfile.dataSource,
date: symbolProfile.Order?.[0]?.date,
symbol: symbolProfile.symbol
};
});
return {
marketData: [...currencyPairsToGather, ...symbolProfilesToGather]
}; };
} }

View File

@ -1,5 +1,5 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
@ -15,10 +15,11 @@ import { ImportService } from './import.service';
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,
OrderModule,
PrismaModule, PrismaModule,
RedisCacheModule RedisCacheModule
], ],
controllers: [ImportController], controllers: [ImportController],
providers: [CacheService, ImportService, OrderService] providers: [CacheService, ImportService]
}) })
export class ImportModule {} export class ImportModule {}

View File

@ -34,11 +34,6 @@ export class ImportService {
unitPrice unitPrice
} of orders) { } of orders) {
await this.orderService.createOrder({ await this.orderService.createOrder({
Account: {
connect: {
id_userId: { userId, id: accountId }
}
},
currency, currency,
dataSource, dataSource,
fee, fee,
@ -46,7 +41,26 @@ export class ImportService {
symbol, symbol,
type, type,
unitPrice, unitPrice,
Account: {
connect: {
id_userId: { userId, id: accountId }
}
},
date: parseISO(<string>(<unknown>date)), date: parseISO(<string>(<unknown>date)),
SymbolProfile: {
connectOrCreate: {
create: {
dataSource,
symbol
},
where: {
dataSource_symbol: {
dataSource,
symbol
}
}
}
},
User: { connect: { id: userId } } User: { connect: { id: userId } }
}); });
} }

View File

@ -0,0 +1,10 @@
import { OrderWithAccount } from '@ghostfolio/common/types';
export interface Activities {
activities: Activity[];
}
export interface Activity extends OrderWithAccount {
feeInBaseCurrency: number;
valueInBaseCurrency: number;
}

View File

@ -23,6 +23,7 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
import { Activities } from './interfaces/activities.interface';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto'; import { UpdateOrderDto } from './update-order.dto';
@ -59,14 +60,16 @@ export class OrderController {
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getAllOrders( public async getAllOrders(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<OrderModel[]> { ): Promise<Activities> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
const userCurrency = this.request.user.Settings.currency;
let orders = await this.orderService.getOrders({ let activities = await this.orderService.getOrders({
userCurrency,
includeDrafts: true, includeDrafts: true,
userId: impersonationUserId || this.request.user.id userId: impersonationUserId || this.request.user.id
}); });
@ -75,15 +78,17 @@ export class OrderController {
impersonationUserId || impersonationUserId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
orders = nullifyValuesInObjects(orders, [ activities = nullifyValuesInObjects(activities, [
'fee', 'fee',
'feeInBaseCurrency',
'quantity', 'quantity',
'unitPrice', 'unitPrice',
'value' 'value',
'valueInBaseCurrency'
]); ]);
} }
return orders; return { activities };
} }
@Get(':id') @Get(':id')
@ -116,23 +121,23 @@ export class OrderController {
return this.orderService.createOrder({ return this.orderService.createOrder({
...data, ...data,
date,
Account: { Account: {
connect: { connect: {
id_userId: { id: accountId, userId: this.request.user.id } id_userId: { id: accountId, userId: this.request.user.id }
} }
}, },
date,
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
create: {
dataSource: data.dataSource,
symbol: data.symbol
},
where: { where: {
dataSource_symbol: { dataSource_symbol: {
dataSource: data.dataSource, dataSource: data.dataSource,
symbol: data.symbol symbol: data.symbol
} }
},
create: {
dataSource: data.dataSource,
symbol: data.symbol
} }
} }
}, },

View File

@ -4,6 +4,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -16,6 +17,7 @@ import { OrderService } from './order.service';
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule, ImpersonationModule,
PrismaModule, PrismaModule,
RedisCacheModule, RedisCacheModule,

View File

@ -1,5 +1,6 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -7,10 +8,13 @@ import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { Activity } from './interfaces/activities.interface';
@Injectable() @Injectable()
export class OrderService { export class OrderService {
public constructor( public constructor(
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly prismaService: PrismaService private readonly prismaService: PrismaService
) {} ) {}
@ -86,12 +90,14 @@ export class OrderService {
public async getOrders({ public async getOrders({
includeDrafts = false, includeDrafts = false,
types, types,
userCurrency,
userId userId
}: { }: {
includeDrafts?: boolean; includeDrafts?: boolean;
types?: TypeOfOrder[]; types?: TypeOfOrder[];
userCurrency: string;
userId: string; userId: string;
}) { }): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
if (includeDrafts === false) { if (includeDrafts === false) {
@ -124,12 +130,21 @@ export class OrderService {
orderBy: { date: 'asc' } orderBy: { date: 'asc' }
}) })
).map((order) => { ).map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return { return {
...order, ...order,
value: new Big(order.quantity) value,
.mul(order.unitPrice) feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
.plus(order.fee) order.fee,
.toNumber() order.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.currency,
userCurrency
)
}; };
}); });
} }

View File

@ -388,11 +388,12 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aSymbol: string aSymbol: string
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
const userCurrency = this.request.user.Settings.currency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = (await this.orderService.getOrders({ userId })).filter( const orders = (
(order) => order.symbol === aSymbol await this.orderService.getOrders({ userCurrency, userId })
); ).filter((order) => order.symbol === aSymbol);
if (orders.length <= 0) { if (orders.length <= 0) {
return { return {
@ -846,24 +847,25 @@ export class PortfolioService {
} }
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> { public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const currency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const performanceInformation = await this.getPerformance(aImpersonationId); const performanceInformation = await this.getPerformance(aImpersonationId);
const { balance } = await this.accountService.getCashDetails( const { balance } = await this.accountService.getCashDetails(
userId, userId,
currency userCurrency
); );
const orders = await this.orderService.getOrders({ const orders = await this.orderService.getOrders({
userCurrency,
userId userId
}); });
const dividend = this.getDividend(orders).toNumber(); const dividend = this.getDividend(orders).toNumber();
const fees = this.getFees(orders).toNumber(); const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date; const firstOrderDate = orders[0]?.date;
const totalBuy = this.getTotalByType(orders, currency, 'BUY'); const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, currency, 'SELL'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const committedFunds = new Big(totalBuy).sub(totalSell); const committedFunds = new Big(totalBuy).sub(totalSell);
@ -895,8 +897,8 @@ export class PortfolioService {
}: { }: {
cashDetails: CashDetails; cashDetails: CashDetails;
investment: Big; investment: Big;
value: Big;
userCurrency: string; userCurrency: string;
value: Big;
}) { }) {
const cashPositions = {}; const cashPositions = {};
@ -1025,8 +1027,11 @@ export class PortfolioService {
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
}> { }> {
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const orders = await this.orderService.getOrders({ const orders = await this.orderService.getOrders({
includeDrafts, includeDrafts,
userCurrency,
userId, userId,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL']
}); });
@ -1035,7 +1040,6 @@ export class PortfolioService {
return { transactionPoints: [], orders: [] }; return { transactionPoints: [], orders: [] };
} }
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,

View File

@ -25,17 +25,17 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
}; };
} = {}; } = {};
for (const account of Object.keys(this.accounts)) { for (const [accountId, account] of Object.entries(this.accounts)) {
accounts[account] = { accounts[accountId] = {
name: account, name: account.name,
investment: this.accounts[account].current investment: account.current
}; };
} }
let maxItem; let maxItem;
let totalInvestment = 0; let totalInvestment = 0;
Object.values(accounts).forEach((account) => { for (const account of Object.values(accounts)) {
if (!maxItem) { if (!maxItem) {
maxItem = account; maxItem = account;
} }
@ -47,7 +47,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
if (account.investment > maxItem?.investment) { if (account.investment > maxItem?.investment) {
maxItem = account; maxItem = account;
} }
}); }
const maxInvestmentRatio = maxItem.investment / totalInvestment; const maxInvestmentRatio = maxItem.investment / totalInvestment;

View File

@ -19,35 +19,35 @@ export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
} }
public evaluate(ruleSettings?: Settings) { public evaluate(ruleSettings?: Settings) {
const platforms: { const accounts: {
[symbol: string]: Pick<PortfolioPosition, 'name'> & { [symbol: string]: Pick<PortfolioPosition, 'name'> & {
investment: number; investment: number;
}; };
} = {}; } = {};
for (const account of Object.keys(this.accounts)) { for (const [accountId, account] of Object.entries(this.accounts)) {
platforms[account] = { accounts[accountId] = {
name: account, name: account.name,
investment: this.accounts[account].original investment: account.original
}; };
} }
let maxItem; let maxItem;
let totalInvestment = 0; let totalInvestment = 0;
Object.values(platforms).forEach((platform) => { for (const account of Object.values(accounts)) {
if (!maxItem) { if (!maxItem) {
maxItem = platform; maxItem = account;
} }
// Calculate total investment // Calculate total investment
totalInvestment += platform.investment; totalInvestment += account.investment;
// Find maximum // Find maximum
if (platform.investment > maxItem?.investment) { if (account.investment > maxItem?.investment) {
maxItem = platform; maxItem = account;
} }
}); }
const maxInvestmentRatio = maxItem.investment / totalInvestment; const maxInvestmentRatio = maxItem.investment / totalInvestment;

View File

@ -12,7 +12,7 @@ import { Granularity } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { isEmpty } from 'lodash'; import { groupBy, isEmpty } from 'lodash';
@Injectable() @Injectable()
export class DataProviderService { export class DataProviderService {
@ -30,18 +30,27 @@ export class DataProviderService {
[symbol: string]: IDataProviderResponse; [symbol: string]: IDataProviderResponse;
} = {}; } = {};
for (const item of items) { const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
const dataProvider = this.getDataProvider(item.dataSource);
response[item.symbol] = (await dataProvider.get([item.symbol]))[
item.symbol
];
}
const promises = []; const promises = [];
for (const symbol of Object.keys(response)) {
const promise = Promise.resolve(response[symbol]); for (const [dataSource, dataGatheringItems] of Object.entries(
itemsGroupedByDataSource
)) {
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
const promise = Promise.resolve(
this.getDataProvider(DataSource[dataSource]).get(symbols)
);
promises.push( promises.push(
promise.then((currentResponse) => (response[symbol] = currentResponse)) promise.then((result) => {
for (const [symbol, dataProviderResponse] of Object.entries(result)) {
response[symbol] = dataProviderResponse;
}
})
); );
} }

View File

@ -8,7 +8,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -35,27 +35,36 @@ export class GoogleSheetsService implements DataProviderInterface {
} }
try { try {
const [symbol] = aSymbols; const response: { [symbol: string]: IDataProviderResponse } = {};
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[symbol] const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
aSymbols
); );
const sheet = await this.getSheet({ const sheet = await this.getSheet({
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'),
symbol symbol: 'Overview'
}); });
const marketPrice = parseFloat(
(await sheet.getCellByA1('B1').value) as string
);
return { const rows = await sheet.getRows();
[symbol]: {
marketPrice, for (const row of rows) {
currency: symbolProfile?.currency, const marketPrice = parseFloat(row['marketPrice']);
dataSource: this.getName(), const symbol = row['symbol'];
marketState: MarketState.delayed
if (aSymbols.includes(symbol)) {
response[symbol] = {
marketPrice,
currency: symbolProfiles.find((symbolProfile) => {
return symbolProfile.symbol === symbol;
})?.currency,
dataSource: this.getName(),
marketState: MarketState.delayed
};
} }
}; }
return response;
} catch (error) { } catch (error) {
Logger.error(error); Logger.error(error);
} }
@ -94,7 +103,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return index >= 1; return index >= 1;
}) })
.forEach((row) => { .forEach((row) => {
const date = new Date(row._rawData[0]); const date = parseDate(row._rawData[0]);
const close = parseFloat(row._rawData[1]); const close = parseFloat(row._rawData[1]);
historicalData[format(date, DATE_FORMAT)] = { marketPrice: close }; historicalData[format(date, DATE_FORMAT)] = { marketPrice: close };

View File

@ -58,9 +58,9 @@ export class ExchangeRateDataService {
getYesterday() getYesterday()
); );
if (isEmpty(result)) { if (Object.keys(result).length !== this.currencyPairs.length) {
// Load currencies directly from data provider as a fallback // Load currencies directly from data provider as a fallback
// if historical data is not yet available // if historical data is not fully available
const historicalData = await this.dataProviderService.get( const historicalData = await this.dataProviderService.get(
this.currencyPairs.map(({ dataSource, symbol }) => { this.currencyPairs.map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };

View File

@ -89,7 +89,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.tokenStorageService.signOut(); this.tokenStorageService.signOut();
this.userService.remove(); this.userService.remove();
this.router.navigate(['/']); document.location.href = '/';
} }
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -15,7 +15,7 @@
>(Default)</span >(Default)</span
> >
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell>Total</td> <td *matFooterCellDef class="px-1" mat-footer-cell i18n>Total</td>
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="currency">

View File

@ -6,7 +6,9 @@
<tr class="mat-header-row"> <tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th> <th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th> <th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
<th class="mat-header-cell px-1 py-2" i18n>First Transaction</th> <th class="mat-header-cell px-1 py-2" i18n>First Activity</th>
<th class="mat-header-cell px-1 py-2" i18n>Activity Count</th>
<th class="mat-header-cell px-1 py-2" i18n>Historical Data</th>
<th class="mat-header-cell px-1 py-2"></th> <th class="mat-header-cell px-1 py-2"></th>
</tr> </tr>
</thead> </thead>
@ -16,11 +18,13 @@
class="cursor-pointer mat-row" class="cursor-pointer mat-row"
(click)="setCurrentSymbol(item.symbol)" (click)="setCurrentSymbol(item.symbol)"
> >
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td> <td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
<td class="mat-cell px-1 py-2">{{ item.dataSource}}</td> <td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
{{ (item.date | date: defaultDateFormat) ?? '' }} {{ (item.date | date: defaultDateFormat) ?? '' }}
</td> </td>
<td class="mat-cell px-1 py-2">{{ item.activityCount }}</td>
<td class="mat-cell px-1 py-2">{{ item.marketDataItemCount }}</td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"

View File

@ -13,7 +13,7 @@
[showYAxis]="false" [showYAxis]="false"
></gf-line-chart> ></gf-line-chart>
<div <div
*ngIf="hasPermissionToCreateOrder&& historicalDataItems?.length === 0" *ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0"
class="align-items-center d-flex h-100 justify-content-center w-100" class="align-items-center d-flex h-100 justify-content-center w-100"
> >
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">

View File

@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -28,6 +29,7 @@ import { ImportTransactionDialog } from './import-transaction-dialog/import-tran
templateUrl: './transactions-page.html' templateUrl: './transactions-page.html'
}) })
export class TransactionsPageComponent implements OnDestroy, OnInit { export class TransactionsPageComponent implements OnDestroy, OnInit {
public activities: Activity[];
public defaultAccountId: string; public defaultAccountId: string;
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
@ -35,7 +37,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
public hasPermissionToDeleteOrder: boolean; public hasPermissionToDeleteOrder: boolean;
public hasPermissionToImportOrders: boolean; public hasPermissionToImportOrders: boolean;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public transactions: OrderModel[];
public user: User; public user: User;
private primaryDataSource: DataSource; private primaryDataSource: DataSource;
@ -65,8 +66,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
if (params['createDialog']) { if (params['createDialog']) {
this.openCreateTransactionDialog(); this.openCreateTransactionDialog();
} else if (params['editDialog']) { } else if (params['editDialog']) {
if (this.transactions) { if (this.activities) {
const transaction = this.transactions.find(({ id }) => { const transaction = this.activities.find(({ id }) => {
return id === params['transactionId']; return id === params['transactionId'];
}); });
@ -119,10 +120,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchOrders() .fetchOrders()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ activities }) => {
this.transactions = response; this.activities = activities;
if (this.hasPermissionToCreateOrder && this.transactions?.length <= 0) { if (this.hasPermissionToCreateOrder && this.activities?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });
} }

View File

@ -3,7 +3,7 @@
<div class="col"> <div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3> <h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
<gf-activities-table <gf-activities-table
[activities]="transactions" [activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder"

View File

@ -4,6 +4,7 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface'; import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
@ -169,14 +170,14 @@ export class DataService {
); );
} }
public fetchOrders(): Observable<OrderModel[]> { public fetchOrders(): Observable<Activities> {
return this.http.get<any[]>('/api/order').pipe( return this.http.get<any>('/api/order').pipe(
map((data) => { map(({ activities }) => {
for (const item of data) { for (const activity of activities) {
item.createdAt = parseISO(item.createdAt); activity.createdAt = parseISO(activity.createdAt);
item.date = parseISO(item.date); activity.date = parseISO(activity.date);
} }
return data; return { activities };
}) })
); );
} }

View File

@ -1,7 +1,12 @@
import { DataSource } from '@prisma/client';
export interface AdminMarketData { export interface AdminMarketData {
marketData: AdminMarketDataItem[]; marketData: AdminMarketDataItem[];
} }
export interface AdminMarketDataItem { export interface AdminMarketDataItem {
dataSource: DataSource;
date?: Date;
marketDataItemCount?: number;
symbol: string; symbol: string;
} }

View File

@ -2,7 +2,10 @@ import { Access } from './access.interface';
import { Accounts } from './accounts.interface'; import { Accounts } from './accounts.interface';
import { AdminData } from './admin-data.interface'; import { AdminData } from './admin-data.interface';
import { AdminMarketDataDetails } from './admin-market-data-details.interface'; import { AdminMarketDataDetails } from './admin-market-data-details.interface';
import { AdminMarketData } from './admin-market-data.interface'; import {
AdminMarketData,
AdminMarketDataItem
} from './admin-market-data.interface';
import { Coupon } from './coupon.interface'; import { Coupon } from './coupon.interface';
import { Export } from './export.interface'; import { Export } from './export.interface';
import { InfoItem } from './info-item.interface'; import { InfoItem } from './info-item.interface';
@ -29,6 +32,7 @@ export {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem,
Coupon, Coupon,
Export, Export,
InfoItem, InfoItem,

View File

@ -58,6 +58,11 @@
> >
{{ dataSource.data.length - i }} {{ dataSource.data.length - i }}
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="date"> <ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
@ -68,6 +73,7 @@
{{ element.date | date: defaultDateFormat }} {{ element.date | date: defaultDateFormat }}
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
@ -93,6 +99,7 @@
<span class="d-none d-lg-block mx-1">{{ element.type }}</span> <span class="d-none d-lg-block mx-1">{{ element.type }}</span>
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
@ -107,6 +114,7 @@
> >
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="currency">
@ -122,6 +130,9 @@
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
{{ element.currency }} {{ element.currency }}
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="quantity"> <ng-container matColumnDef="quantity">
@ -143,6 +154,11 @@
></gf-value> ></gf-value>
</div> </div>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="unitPrice"> <ng-container matColumnDef="unitPrice">
@ -164,6 +180,11 @@
></gf-value> ></gf-value>
</div> </div>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="fee"> <ng-container matColumnDef="fee">
@ -176,7 +197,7 @@
> >
Fee Fee
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isCurrency]="true" [isCurrency]="true"
@ -185,6 +206,15 @@
></gf-value> ></gf-value>
</div> </div>
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalFees"
></gf-value>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
@ -197,7 +227,7 @@
> >
Value Value
</th> </th>
<td *matCellDef="let element" class="px1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isCurrency]="true" [isCurrency]="true"
@ -206,6 +236,15 @@
></gf-value> ></gf-value>
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalValue"
></gf-value>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="account"> <ng-container matColumnDef="account">
@ -223,6 +262,11 @@
<span class="d-none d-lg-block">{{ element.Account?.name }}</span> <span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div> </div>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
@ -276,6 +320,7 @@
</button> </button>
</mat-menu> </mat-menu>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
@ -291,6 +336,11 @@
" "
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }" [ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
></tr> ></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
></tr>
</table> </table>
<ngx-skeleton-loader <ngx-skeleton-loader

View File

@ -15,6 +15,16 @@
} }
.mat-table { .mat-table {
td {
&.mat-footer-cell {
border-top: 1px solid
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
}
}
th { th {
::ng-deep { ::ng-deep {
.mat-sort-header-container { .mat-sort-header-container {
@ -55,6 +65,15 @@
} }
.mat-table { .mat-table {
td {
&.mat-footer-cell {
border-top-color: rgba(
var(--palette-foreground-divider-dark),
var(--palette-foreground-divider-dark-alpha)
);
}
}
.type-badge { .type-badge {
background-color: rgba( background-color: rgba(
var(--palette-foreground-text-dark), var(--palette-foreground-text-dark),

View File

@ -7,7 +7,6 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit,
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
@ -20,9 +19,12 @@ import { MatChipInputEvent } from '@angular/material/chips';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import Big from 'big.js';
import { endOfToday, format, isAfter } from 'date-fns'; import { endOfToday, format, isAfter } from 'date-fns';
import { isNumber } from 'lodash';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -36,7 +38,7 @@ const SEARCH_STRING_SEPARATOR = ',';
templateUrl: './activities-table.component.html' templateUrl: './activities-table.component.html'
}) })
export class ActivitiesTableComponent implements OnChanges, OnDestroy { export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@Input() activities: OrderWithAccount[]; @Input() activities: Activity[];
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean; @Input() hasPermissionToCreateActivity: boolean;
@ -57,8 +59,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>; @ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public dataSource: MatTableDataSource<OrderWithAccount> = public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
new MatTableDataSource();
public defaultDateFormat = DEFAULT_DATE_FORMAT; public defaultDateFormat = DEFAULT_DATE_FORMAT;
public displayedColumns = []; public displayedColumns = [];
public endOfToday = endOfToday(); public endOfToday = endOfToday();
@ -71,6 +72,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public searchControl = new FormControl(); public searchControl = new FormControl();
public searchKeywords: string[] = []; public searchKeywords: string[] = [];
public separatorKeysCodes: number[] = [ENTER, COMMA]; public separatorKeysCodes: number[] = [ENTER, COMMA];
public totalFees: number;
public totalValue: number;
private allFilters: string[]; private allFilters: string[];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -218,6 +221,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
); );
this.filters$.next(this.allFilters); this.filters$.next(this.allFilters);
this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue();
} }
private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
@ -263,4 +269,36 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
return item !== undefined; return item !== undefined;
}); });
} }
private getTotalFees() {
let totalFees = new Big(0);
for (const activity of this.dataSource.filteredData) {
if (isNumber(activity.feeInBaseCurrency)) {
totalFees = totalFees.plus(activity.feeInBaseCurrency);
} else {
return null;
}
}
return totalFees.toNumber();
}
private getTotalValue() {
let totalValue = new Big(0);
for (const activity of this.dataSource.filteredData) {
if (isNumber(activity.valueInBaseCurrency)) {
if (activity.type === 'BUY') {
totalValue = totalValue.plus(activity.valueInBaseCurrency);
} else if (activity.type === 'SELL') {
totalValue = totalValue.minus(activity.valueInBaseCurrency);
}
} else {
return null;
}
}
return totalValue.toNumber();
}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.103.0", "version": "1.106.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -69,11 +69,11 @@
"@nestjs/schedule": "1.0.2", "@nestjs/schedule": "1.0.2",
"@nestjs/serve-static": "2.2.2", "@nestjs/serve-static": "2.2.2",
"@nrwl/angular": "13.4.1", "@nrwl/angular": "13.4.1",
"@prisma/client": "3.7.0", "@prisma/client": "3.8.1",
"@simplewebauthn/browser": "4.1.0", "@simplewebauthn/browser": "4.1.0",
"@simplewebauthn/server": "4.1.0", "@simplewebauthn/server": "4.1.0",
"@simplewebauthn/typescript-types": "4.0.0", "@simplewebauthn/typescript-types": "4.0.0",
"@stripe/stripe-js": "1.15.0", "@stripe/stripe-js": "1.22.0",
"@types/papaparse": "5.2.6", "@types/papaparse": "5.2.6",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"angular-material-css-vars": "3.0.0", "angular-material-css-vars": "3.0.0",
@ -106,11 +106,11 @@
"passport": "0.4.1", "passport": "0.4.1",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "3.7.0", "prisma": "3.8.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"round-to": "5.0.0", "round-to": "5.0.0",
"rxjs": "7.4.0", "rxjs": "7.4.0",
"stripe": "8.156.0", "stripe": "8.199.0",
"svgmap": "2.6.0", "svgmap": "2.6.0",
"tslib": "2.0.0", "tslib": "2.0.0",
"uuid": "8.3.2", "uuid": "8.3.2",

View File

@ -3349,22 +3349,22 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
"@prisma/client@3.7.0": "@prisma/client@3.8.1":
version "3.7.0" version "3.8.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0"
integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw== integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ==
dependencies: dependencies:
"@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" "@prisma/engines-version" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": "@prisma/engines-version@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f":
version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4c8d9744b5e54650a8ba5fde0a711399d6adba24"
integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg== integrity sha512-G2JH6yWt6ixGKmsRmVgaQYahfwMopim0u/XLIZUo2o/mZ5jdu7+BL+2V5lZr7XiG1axhyrpvlyqE/c0OgYSl3g==
"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": "@prisma/engines@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f":
version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4479099b99f6a082ce5843ee7208943ccedd127f"
integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg== integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w==
"@samverschueren/stream-to-observable@^0.3.0": "@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1" version "0.3.1"
@ -4347,10 +4347,10 @@
resolve-from "^5.0.0" resolve-from "^5.0.0"
store2 "^2.12.0" store2 "^2.12.0"
"@stripe/stripe-js@1.15.0": "@stripe/stripe-js@1.22.0":
version "1.15.0" version "1.22.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.15.0.tgz#86178cfbe66151910b09b03595e60048ab4c698e" resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.22.0.tgz#9d3d2f0a1ce81f185ec477fd7cc67544b2b2a00c"
integrity sha512-KQsNPc+uVQkc8dewwz1A6uHOWeU2cWoZyNIbsx5mtmperr5TPxw4u8M20WOa22n6zmIOh/zLdzEe8DYK/0IjBw== integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg==
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
@ -15025,12 +15025,12 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
prisma@3.7.0: prisma@3.8.1:
version "3.7.0" version "3.8.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2" resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873"
integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg== integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA==
dependencies: dependencies:
"@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" "@prisma/engines" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
prismjs@^1.21.0, prismjs@~1.24.0: prismjs@^1.21.0, prismjs@~1.24.0:
version "1.24.1" version "1.24.1"
@ -16952,10 +16952,10 @@ strip-json-comments@^2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
stripe@8.156.0: stripe@8.199.0:
version "8.156.0" version "8.199.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.156.0.tgz#040de551df88d71ef670a8c8d4df114c3fa6eb4b" resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.199.0.tgz#dcd109f16ff0c33da638a0d154c966d0f20c73d1"
integrity sha512-q+bixlhaxnSI/Htk/iB1i5LhuZ557hL0pFgECBxQNhso1elxIsOsPOIXEuo3tSLJEb8CJSB7t/+Fyq6KP69tAQ== integrity sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA==
dependencies: dependencies:
"@types/node" ">=8.1.0" "@types/node" ">=8.1.0"
qs "^6.6.0" qs "^6.6.0"