Feature/add balance to account (#193)
* Add balance attribute and calculate total balance * Update changelog
This commit is contained in:
parent
db090229ce
commit
2c19d8c8e7
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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<PortfolioPerformance> {
|
||||
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<PortfolioReport> {
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -26,6 +26,27 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
|
||||
Transactions
|
||||
</th>
|
||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
||||
{{ element.Order?.length }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="balance">
|
||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>Balance</th>
|
||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[currency]="element.currency"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
></gf-value>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
@ -53,15 +74,6 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
|
||||
Transactions
|
||||
</th>
|
||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
||||
{{ element.Order?.length }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
|
@ -28,7 +28,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Output() accountDeleted = new EventEmitter<string>();
|
||||
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||
|
||||
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource();
|
||||
public dataSource: MatTableDataSource<AccountModel> =
|
||||
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');
|
||||
|
@ -1,4 +1,18 @@
|
||||
<div class="container p-0">
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : overview?.cash"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -8,14 +8,37 @@
|
||||
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="d-none">
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||
<mat-option value="SECURITIES" i18n> SECURITIES </mat-option>
|
||||
<mat-option value="CASH" i18n>Cash</mat-option>
|
||||
<mat-option value="SECURITIES" i18n>Securities</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
<mat-select name="currency" required [(value)]="data.account.currency">
|
||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||
>{{ currency }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Balance</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="balance"
|
||||
required
|
||||
type="number"
|
||||
[(ngModel)]="data.account.balance"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Platform</mat-label>
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface PortfolioOverview {
|
||||
cash: number;
|
||||
committedFunds: number;
|
||||
fees: number;
|
||||
ordersCount: number;
|
||||
|
@ -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';
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user