From 2c19d8c8e7bb4266342a986cf109ce023d793402 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Wed, 7 Jul 2021 21:23:36 +0200 Subject: [PATCH] Feature/add balance to account (#193) * Add balance attribute and calculate total balance * Update changelog --- CHANGELOG.md | 2 ++ apps/api/src/app/account/account.module.ts | 2 ++ apps/api/src/app/account/account.service.ts | 22 +++++++++++++- .../api/src/app/account/create-account.dto.ts | 10 +++++-- .../api/src/app/account/update-account.dto.ts | 10 +++++-- apps/api/src/app/order/create-order.dto.ts | 2 +- .../src/app/portfolio/portfolio.controller.ts | 28 +++++++++-------- .../api/src/app/portfolio/portfolio.module.ts | 10 ++++--- .../src/app/portfolio/portfolio.service.ts | 13 ++++++-- apps/api/src/models/portfolio.spec.ts | 2 ++ .../accounts-table.component.html | 30 +++++++++++++------ .../accounts-table.component.ts | 5 ++-- .../portfolio-overview.component.html | 14 +++++++++ .../pages/accounts/accounts-page.component.ts | 6 ++++ .../create-or-update-account-dialog.html | 27 +++++++++++++++-- .../portfolio-overview.interface.ts | 1 + .../migration.sql | 6 ++++ prisma/schema.prisma | 3 ++ prisma/seed.ts | 8 +++++ 19 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 prisma/migrations/20210703194509_added_balance_to_account/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index e95a21f0f..5d5a3f144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the total value in the create or edit transaction dialog +- Added a balance attribute to the account model +- Calculated the total balance (cash) ### Changed diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 3ac6c0ad0..bbd272978 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -4,6 +4,7 @@ import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alph import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { Module } from '@nestjs/common'; @@ -20,6 +21,7 @@ import { AccountService } from './account.service'; AlphaVantageService, ConfigurationService, DataProviderService, + ExchangeRateDataService, GhostfolioScraperApiService, ImpersonationService, PrismaService, diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index a18d388e0..26f85268e 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -1,12 +1,14 @@ +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { Injectable } from '@nestjs/common'; -import { Account, Order, Prisma } from '@prisma/client'; +import { Account, Currency, Order, Prisma } from '@prisma/client'; import { RedisCacheService } from '../redis-cache/redis-cache.service'; @Injectable() export class AccountService { public constructor( + private exchangeRateDataService: ExchangeRateDataService, private readonly redisCacheService: RedisCacheService, private prisma: PrismaService ) {} @@ -53,6 +55,24 @@ export class AccountService { }); } + public async calculateCashBalance(aUserId: string, aCurrency: Currency) { + let totalCashBalance = 0; + + const accounts = await this.accounts({ + where: { userId: aUserId } + }); + + accounts.forEach((account) => { + totalCashBalance += this.exchangeRateDataService.toCurrency( + account.balance, + account.currency, + aCurrency + ); + }); + + return totalCashBalance; + } + public async createAccount( data: Prisma.AccountCreateInput, aUserId: string diff --git a/apps/api/src/app/account/create-account.dto.ts b/apps/api/src/app/account/create-account.dto.ts index c5e5275ab..ef310293b 100644 --- a/apps/api/src/app/account/create-account.dto.ts +++ b/apps/api/src/app/account/create-account.dto.ts @@ -1,10 +1,16 @@ -import { AccountType } from '@prisma/client'; -import { IsString, ValidateIf } from 'class-validator'; +import { AccountType, Currency } from '@prisma/client'; +import { IsNumber, IsString, ValidateIf } from 'class-validator'; export class CreateAccountDto { @IsString() accountType: AccountType; + @IsNumber() + balance: number; + + @IsString() + currency: Currency; + @IsString() name: string; diff --git a/apps/api/src/app/account/update-account.dto.ts b/apps/api/src/app/account/update-account.dto.ts index ee5e21a9e..fc32d283f 100644 --- a/apps/api/src/app/account/update-account.dto.ts +++ b/apps/api/src/app/account/update-account.dto.ts @@ -1,10 +1,16 @@ -import { AccountType } from '@prisma/client'; -import { IsString, ValidateIf } from 'class-validator'; +import { AccountType, Currency } from '@prisma/client'; +import { IsNumber, IsString, ValidateIf } from 'class-validator'; export class UpdateAccountDto { @IsString() accountType: AccountType; + @IsNumber() + balance: number; + + @IsString() + currency: Currency; + @IsString() id: string; diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index 0f5daea3a..ef307f941 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -1,5 +1,5 @@ import { Currency, DataSource, Type } from '@prisma/client'; -import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; +import { IsISO8601, IsNumber, IsString } from 'class-validator'; export class CreateOrderDto { @IsString() diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 462a12404..454f0c438 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -142,10 +142,11 @@ export class PortfolioController { ): Promise<{ [symbol: string]: PortfolioPosition }> { let details: { [symbol: string]: PortfolioPosition } = {}; - const impersonationUserId = await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + const impersonationUserId = + await this.impersonationService.validateImpersonationId( + impersonationId, + this.request.user.id + ); const portfolio = await this.portfolioService.createPortfolio( impersonationUserId || this.request.user.id @@ -221,6 +222,7 @@ export class PortfolioController { ) ) { overview = nullifyValuesInObject(overview, [ + 'cash', 'committedFunds', 'fees', 'totalBuy', @@ -238,10 +240,11 @@ export class PortfolioController { @Query('range') range, @Res() res: Response ): Promise { - const impersonationUserId = await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + const impersonationUserId = + await this.impersonationService.validateImpersonationId( + impersonationId, + this.request.user.id + ); const portfolio = await this.portfolioService.createPortfolio( impersonationUserId || this.request.user.id @@ -306,10 +309,11 @@ export class PortfolioController { public async getReport( @Headers('impersonation-id') impersonationId ): Promise { - const impersonationUserId = await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + const impersonationUserId = + await this.impersonationService.validateImpersonationId( + impersonationId, + this.request.user.id + ); const portfolio = await this.portfolioService.createPortfolio( impersonationUserId || this.request.user.id diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 88fe47c64..84a7f1db5 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -1,3 +1,8 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; +import { OrderService } from '@ghostfolio/api/app/order/order.service'; +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; @@ -11,10 +16,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { RulesService } from '@ghostfolio/api/services/rules.service'; import { Module } from '@nestjs/common'; -import { CacheService } from '../cache/cache.service'; -import { OrderService } from '../order/order.service'; -import { RedisCacheModule } from '../redis-cache/redis-cache.module'; -import { UserService } from '../user/user.service'; import { PortfolioController } from './portfolio.controller'; import { PortfolioService } from './portfolio.service'; @@ -22,6 +23,7 @@ import { PortfolioService } from './portfolio.service'; imports: [RedisCacheModule], controllers: [PortfolioController], providers: [ + AccountService, AlphaVantageService, CacheService, ConfigurationService, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 2d4081337..540cc6f8d 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1,3 +1,7 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { OrderService } from '@ghostfolio/api/app/order/order.service'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; import { Portfolio } from '@ghostfolio/api/models/portfolio'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; @@ -30,9 +34,6 @@ import { import { isEmpty } from 'lodash'; import * as roundTo from 'round-to'; -import { OrderService } from '../order/order.service'; -import { RedisCacheService } from '../redis-cache/redis-cache.service'; -import { UserService } from '../user/user.service'; import { HistoricalDataItem, PortfolioPositionDetail @@ -41,6 +42,7 @@ import { @Injectable() export class PortfolioService { public constructor( + private readonly accountService: AccountService, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly impersonationService: ImpersonationService, @@ -192,10 +194,15 @@ export class PortfolioService { impersonationUserId || this.request.user.id ); + const cash = await this.accountService.calculateCashBalance( + impersonationUserId || this.request.user.id, + this.request.user.Settings.currency + ); const committedFunds = portfolio.getCommittedFunds(); const fees = portfolio.getFees(); return { + cash, committedFunds, fees, ordersCount: portfolio.getOrders().length, diff --git a/apps/api/src/models/portfolio.spec.ts b/apps/api/src/models/portfolio.spec.ts index 2f78df3fe..9a2f2cfe2 100644 --- a/apps/api/src/models/portfolio.spec.ts +++ b/apps/api/src/models/portfolio.spec.ts @@ -110,7 +110,9 @@ describe('Portfolio', () => { Account: [ { accountType: AccountType.SECURITIES, + balance: 0, createdAt: new Date(), + currency: Currency.USD, id: DEFAULT_ACCOUNT_ID, isDefault: true, name: 'Default Account', diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.html b/apps/client/src/app/components/accounts-table/accounts-table.component.html index bcf8bc475..34205e231 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.html +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.html @@ -26,6 +26,27 @@ + + + Transactions + + + {{ element.Order?.length }} + + + + + Balance + + + + + @@ -53,15 +74,6 @@ - - - Transactions - - - {{ element.Order?.length }} - - - diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.ts b/apps/client/src/app/components/accounts-table/accounts-table.component.ts index 3a7a592f6..8a0982304 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.ts +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.ts @@ -28,7 +28,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { @Output() accountDeleted = new EventEmitter(); @Output() accountToUpdate = new EventEmitter(); - public dataSource: MatTableDataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = + new MatTableDataSource(); public displayedColumns = []; public isLoading = true; public routeQueryParams: Subscription; @@ -40,7 +41,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { public ngOnInit() {} public ngOnChanges() { - this.displayedColumns = ['account', 'platform', 'transactions']; + this.displayedColumns = ['account', 'platform', 'transactions', 'balance']; if (this.showActions) { this.displayedColumns.push('actions'); diff --git a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html b/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html index ef5e2aab7..e3cb238d4 100644 --- a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html +++ b/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html @@ -1,4 +1,18 @@
+
+
Cash
+
+ +
+
+
+

+
Buy
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index a2de77c26..656b6776e 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -125,6 +125,8 @@ export class AccountsPageComponent implements OnInit { public openUpdateAccountDialog({ accountType, + balance, + currency, id, name, platformId @@ -133,6 +135,8 @@ export class AccountsPageComponent implements OnInit { data: { account: { accountType, + balance, + currency, id, name, platformId @@ -167,6 +171,8 @@ export class AccountsPageComponent implements OnInit { data: { account: { accountType: AccountType.SECURITIES, + balance: 0, + currency: this.user?.settings?.baseCurrency, name: null, platformId: null } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 25c3fc430..9e8d8f5af 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -8,14 +8,37 @@
-
+
Type - SECURITIES + Cash + Securities
+
+ + Currency + + {{ currency }} + + +
+
+ + Balance + + +
Platform diff --git a/libs/common/src/lib/interfaces/portfolio-overview.interface.ts b/libs/common/src/lib/interfaces/portfolio-overview.interface.ts index 88d934a41..c9c20c28c 100644 --- a/libs/common/src/lib/interfaces/portfolio-overview.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-overview.interface.ts @@ -1,4 +1,5 @@ export interface PortfolioOverview { + cash: number; committedFunds: number; fees: number; ordersCount: number; diff --git a/prisma/migrations/20210703194509_added_balance_to_account/migration.sql b/prisma/migrations/20210703194509_added_balance_to_account/migration.sql new file mode 100644 index 000000000..8c3952035 --- /dev/null +++ b/prisma/migrations/20210703194509_added_balance_to_account/migration.sql @@ -0,0 +1,6 @@ +-- AlterEnum +ALTER TYPE "AccountType" ADD VALUE 'CASH'; + +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0, +ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 85b89c75e..e00b95245 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,7 +26,9 @@ model Access { model Account { accountType AccountType @default(SECURITIES) + balance Float @default(0) createdAt DateTime @default(now()) + currency Currency @default(USD) id String @default(uuid()) isDefault Boolean @default(false) name String? @@ -158,6 +160,7 @@ model User { } enum AccountType { + CASH SECURITIES } diff --git a/prisma/seed.ts b/prisma/seed.ts index 4c47d7386..5c99c0f9b 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -87,6 +87,8 @@ async function main() { create: [ { accountType: AccountType.SECURITIES, + balance: 0, + currency: Currency.USD, id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb', isDefault: true, name: 'Default Account' @@ -109,18 +111,24 @@ async function main() { create: [ { accountType: AccountType.SECURITIES, + balance: 0, + currency: Currency.USD, id: 'd804de69-0429-42dc-b6ca-b308fd7dd926', name: 'Coinbase Account', platformId: platformCoinbase.id }, { accountType: AccountType.SECURITIES, + balance: 0, + currency: Currency.EUR, id: '65cfb79d-b6c7-4591-9d46-73426bc62094', name: 'DEGIRO Account', platformId: platformDegiro.id }, { accountType: AccountType.SECURITIES, + balance: 0, + currency: Currency.USD, id: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', isDefault: true, name: 'Interactive Brokers Account',