Browse Source

Task/remove redundant balance attribute from account (#7546)

* Remove redundant balance attribute from account

* Update changelog
pull/7522/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
ddd20d3099
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 48
      apps/api/src/app/account/account.controller.ts
  3. 64
      apps/api/src/app/account/account.service.ts
  4. 4
      apps/api/src/app/account/interfaces/cash-details.interface.ts
  5. 4
      apps/api/src/app/export/export.service.ts
  6. 12
      apps/api/src/app/import/import.service.ts
  7. 7
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  8. 4
      apps/api/src/app/portfolio/portfolio.service.ts
  9. 5
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  10. 9
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  11. 8
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts
  12. 2
      libs/common/src/lib/config.ts
  13. 7
      libs/common/src/lib/dtos/create-account.dto.ts
  14. 7
      libs/common/src/lib/dtos/update-account.dto.ts
  15. 5
      libs/common/src/lib/types/account-with-balance.type.ts
  16. 6
      libs/common/src/lib/types/account-with-value.type.ts
  17. 2
      libs/common/src/lib/types/index.ts
  18. 20
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  19. 14
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  20. 5
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  21. 2
      prisma/migrations/20260805120000_removed_balance_from_account/migration.sql
  22. 1
      prisma/schema.prisma
  23. 1
      test/import/not-ok/invalid-platform.json
  24. 1
      test/import/ok/500-activities.json
  25. 1
      test/import/ok/derived-currency.json
  26. 1
      test/import/ok/sample.json

1
CHANGELOG.md

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the usability of the create watchlist item dialog by setting the initial focus to the search field - Improved the usability of the create watchlist item dialog by setting the initial focus to the search field
- Migrated the abstract _Material_ form field from a component to a directive - Migrated the abstract _Material_ form field from a component to a directive
- Removed the redundant `balance` attribute of the account in favor of the account balances
## 3.43.0 - 2026-08-06 ## 3.43.0 - 2026-08-06

48
apps/api/src/app/account/account.controller.ts

@ -156,32 +156,34 @@ export class AccountController {
public async createAccount( public async createAccount(
@Body() data: CreateAccountDto @Body() data: CreateAccountDto
): Promise<AccountModel> { ): Promise<AccountModel> {
const { tags: tagIds, ...accountData } = data; const { balance, tags: tagIds, ...accountData } = data;
if (accountData.platformId) { if (accountData.platformId) {
const platformId = accountData.platformId; const platformId = accountData.platformId;
delete accountData.platformId; delete accountData.platformId;
return this.accountService.createAccount( return this.accountService.createAccount({
{ balance,
tagIds,
data: {
...accountData, ...accountData,
platform: { connect: { id: platformId } }, platform: { connect: { id: platformId } },
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
this.request.user.id, userId: this.request.user.id
tagIds });
);
} else { } else {
delete accountData.platformId; delete accountData.platformId;
return this.accountService.createAccount( return this.accountService.createAccount({
{ balance,
tagIds,
data: {
...accountData, ...accountData,
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
this.request.user.id, userId: this.request.user.id
tagIds });
);
} }
} }
@ -257,35 +259,35 @@ export class AccountController {
); );
} }
const { tags: tagIds, ...accountData } = data; const { balance, tags: tagIds, ...accountData } = data;
if (accountData.platformId) { if (accountData.platformId) {
const platformId = accountData.platformId; const platformId = accountData.platformId;
delete accountData.platformId; delete accountData.platformId;
return this.accountService.updateAccount( return this.accountService.updateAccount({
{ balance,
tagIds,
data: { data: {
...accountData, ...accountData,
platform: { connect: { id: platformId } }, platform: { connect: { id: platformId } },
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
userId: this.request.user.id,
where: { where: {
id_userId: { id_userId: {
id, id,
userId: this.request.user.id userId: this.request.user.id
} }
} }
}, });
this.request.user.id,
tagIds
);
} else { } else {
// platformId is null, remove it // platformId is null, remove it
delete accountData.platformId; delete accountData.platformId;
return this.accountService.updateAccount( return this.accountService.updateAccount({
{ balance,
tagIds,
data: { data: {
...accountData, ...accountData,
platform: originalAccount.platformId platform: originalAccount.platformId
@ -293,16 +295,14 @@ export class AccountController {
: undefined, : undefined,
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
userId: this.request.user.id,
where: { where: {
id_userId: { id_userId: {
id, id,
userId: this.request.user.id userId: this.request.user.id
} }
} }
}, });
this.request.user.id,
tagIds
);
} }
} }
} }

64
apps/api/src/app/account/account.service.ts

@ -10,6 +10,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { AccountWithBalance } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
@ -24,7 +25,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { endOfToday, format } from 'date-fns'; import { endOfToday, format } from 'date-fns';
import { groupBy } from 'lodash'; import { groupBy, isNil } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -40,7 +41,7 @@ export class AccountService {
public async account({ public async account({
id_userId id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> { }: Prisma.AccountWhereUniqueInput): Promise<AccountWithBalance | null> {
const account = await this.prismaService.account.findUnique({ const account = await this.prismaService.account.findUnique({
include: { include: {
balances: { balances: {
@ -87,7 +88,7 @@ export class AccountService {
where?: Prisma.AccountWhereInput; where?: Prisma.AccountWhereInput;
orderBy?: Prisma.AccountOrderByWithRelationInput; orderBy?: Prisma.AccountOrderByWithRelationInput;
}): Promise< }): Promise<
(Account & { (AccountWithBalance & {
activities?: (Order & { SymbolProfile?: SymbolProfile })[]; activities?: (Order & { SymbolProfile?: SymbolProfile })[];
balances?: AccountBalance[]; balances?: AccountBalance[];
platform?: Platform; platform?: Platform;
@ -160,12 +161,18 @@ export class AccountService {
}); });
} }
public async createAccount( public async createAccount({
data: Prisma.AccountCreateInput, balance,
aUserId: string, data,
tagIds?: string[] tagIds,
): Promise<Account> { userId
await this.tagService.validateTagIds({ tagIds, userId: aUserId }); }: {
balance?: number;
data: Prisma.AccountCreateInput;
tagIds?: string[];
userId: string;
}): Promise<Account> {
await this.tagService.validateTagIds({ tagIds, userId });
const account = await this.prismaService.account.create({ const account = await this.prismaService.account.create({
data: { data: {
@ -182,12 +189,14 @@ export class AccountService {
} }
}); });
if (!isNil(balance)) {
await this.accountBalanceService.createOrUpdateAccountBalance({ await this.accountBalanceService.createOrUpdateAccountBalance({
balance,
userId,
accountId: account.id, accountId: account.id,
balance: data.balance, date: format(new Date(), DATE_FORMAT)
date: format(new Date(), DATE_FORMAT),
userId: aUserId
}); });
}
this.eventEmitter.emit( this.eventEmitter.emit(
PortfolioChangedEvent.getName(), PortfolioChangedEvent.getName(),
@ -216,7 +225,7 @@ export class AccountService {
return account; return account;
} }
public async getAccounts(aUserId: string): Promise<Account[]> { public async getAccounts(aUserId: string): Promise<AccountWithBalance[]> {
const accounts = await this.accounts({ const accounts = await this.accounts({
include: { include: {
activities: true, activities: true,
@ -295,17 +304,20 @@ export class AccountService {
}; };
} }
public async updateAccount( public async updateAccount({
params: { balance,
data,
tagIds,
userId,
where
}: {
balance?: number;
data: Prisma.AccountUpdateInput; data: Prisma.AccountUpdateInput;
tagIds?: string[];
userId: string;
where: Prisma.AccountWhereUniqueInput; where: Prisma.AccountWhereUniqueInput;
}, }): Promise<Account> {
aUserId: string, await this.tagService.validateTagIds({ tagIds, userId });
tagIds?: string[]
): Promise<Account> {
const { data, where } = params;
await this.tagService.validateTagIds({ tagIds, userId: aUserId });
const account = await this.prismaService.account.update({ const account = await this.prismaService.account.update({
data: { data: {
@ -324,12 +336,14 @@ export class AccountService {
where where
}); });
if (!isNil(balance)) {
await this.accountBalanceService.createOrUpdateAccountBalance({ await this.accountBalanceService.createOrUpdateAccountBalance({
balance,
userId,
accountId: account.id, accountId: account.id,
balance: data.balance as number, date: format(new Date(), DATE_FORMAT)
date: format(new Date(), DATE_FORMAT),
userId: aUserId
}); });
}
this.eventEmitter.emit( this.eventEmitter.emit(
PortfolioChangedEvent.getName(), PortfolioChangedEvent.getName(),

4
apps/api/src/app/account/interfaces/cash-details.interface.ts

@ -1,6 +1,6 @@
import { Account } from '@prisma/client'; import { AccountWithBalance } from '@ghostfolio/common/types';
export interface CashDetails { export interface CashDetails {
accounts: Account[]; accounts: AccountWithBalance[];
balanceInBaseCurrency: number; balanceInBaseCurrency: number;
} }

4
apps/api/src/app/export/export.service.ts

@ -102,7 +102,6 @@ export class ExportService {
}) })
.map( .map(
({ ({
balance,
balances, balances,
comment, comment,
currency, currency,
@ -111,13 +110,12 @@ export class ExportService {
platform, platform,
platformId, platformId,
tags tags
}) => { }): ExportResponse['accounts'][number] => {
if (platformId) { if (platformId) {
platformsMap[platformId] = platform; platformsMap[platformId] = platform;
} }
return { return {
balance,
balances: balances.map(({ date, value }) => { balances: balances.map(({ date, value }) => {
return { date: date.toISOString(), value }; return { date: date.toISOString(), value };
}), }),

12
apps/api/src/app/import/import.service.ts

@ -355,6 +355,7 @@ export class ImportService {
// If there is no account or if the account belongs to a different user then create a new account // If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== user.id) { if (!accountWithSameId || accountWithSameId.userId !== user.id) {
const account = omit(accountWithBalances, [ const account = omit(accountWithBalances, [
'balance',
'balances', 'balances',
'isExcluded', 'isExcluded',
'tags' 'tags'
@ -408,11 +409,12 @@ export class ImportService {
}; };
} }
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount({
accountObject, tagIds,
user.id, balance: accountWithBalances.balance,
tagIds data: accountObject,
); userId: user.id
});
// Store the new to old account ID mappings for updating activities // Store the new to old account ID mappings for updating activities
if (accountWithSameId && oldAccountId) { if (accountWithSameId && oldAccountId) {

7
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -16,8 +16,9 @@ import {
AssetProfileIdentifier, AssetProfileIdentifier,
PortfolioSummary PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { AccountWithBalance } from '@ghostfolio/common/types';
import { Account, DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
@ -219,7 +220,7 @@ describe('PortfolioService', () => {
it('should return cash holdings when the calculator emits cash positions with the exchange-rate data source', async () => { it('should return cash holdings when the calculator emits cash positions with the exchange-rate data source', async () => {
const accountId = randomUUID(); const accountId = randomUUID();
const cashAccount: Account = { const cashAccount: AccountWithBalance = {
balance: 2000, balance: 2000,
comment: null, comment: null,
createdAt: parseDate('2024-01-01'), createdAt: parseDate('2024-01-01'),
@ -444,7 +445,7 @@ describe('PortfolioService', () => {
beforeEach(() => { beforeEach(() => {
jest jest
.spyOn(accountService, 'getAccounts') .spyOn(accountService, 'getAccounts')
.mockResolvedValue([account] as unknown as Account[]); .mockResolvedValue([account] as unknown as AccountWithBalance[]);
jest jest
.spyOn(exchangeRateDataService, 'toCurrency') .spyOn(exchangeRateDataService, 'toCurrency')

4
apps/api/src/app/portfolio/portfolio.service.ts

@ -64,6 +64,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models'; import { TimelinePosition } from '@ghostfolio/common/models';
import { import {
AccountWithBalance,
AccountWithValue, AccountWithValue,
DateRange, DateRange,
GroupBy, GroupBy,
@ -75,7 +76,6 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { import {
Account,
Type as ActivityType, Type as ActivityType,
AssetClass, AssetClass,
AssetSubClass, AssetSubClass,
@ -2142,7 +2142,7 @@ export class PortfolioService {
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
const platforms: PortfolioDetails['platforms'] = {}; const platforms: PortfolioDetails['platforms'] = {};
let currentAccounts: (Account & { let currentAccounts: (AccountWithBalance & {
Order?: Order[]; Order?: Order[];
platform?: Platform; platform?: Platform;
tags?: Tag[]; tags?: Tag[];

5
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -22,6 +22,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
@ -65,7 +66,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { NavigationStart, Router, RouterModule } from '@angular/router'; import { NavigationStart, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { Account, MarketData, Tag } from '@prisma/client'; import { MarketData, Tag } from '@prisma/client';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
@ -117,7 +118,7 @@ import {
templateUrl: 'holding-detail-dialog.html' templateUrl: 'holding-detail-dialog.html'
}) })
export class GfHoldingDetailDialogComponent implements OnInit { export class GfHoldingDetailDialogComponent implements OnInit {
protected accounts: Account[]; protected accounts: AccountWithValue[];
protected activitiesCount: number; protected activitiesCount: number;
protected assetClass: string; protected assetClass: string;
protected assetProfile: Pick< protected assetProfile: Pick<

9
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -12,6 +12,7 @@ import {
} from '@ghostfolio/common/dtos'; } from '@ghostfolio/common/dtos';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
@ -29,7 +30,7 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Account as AccountModel, Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -48,7 +49,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
templateUrl: './accounts-page.html' templateUrl: './accounts-page.html'
}) })
export class GfAccountsPageComponent implements OnInit { export class GfAccountsPageComponent implements OnInit {
protected accounts: AccountModel[]; protected accounts: AccountWithValue[];
protected activitiesCount = 0; protected activitiesCount = 0;
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToCreateAccount: boolean; protected hasPermissionToCreateAccount: boolean;
@ -155,7 +156,7 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
protected onUpdateAccount(aAccount: AccountModel) { protected onUpdateAccount(aAccount: AccountWithValue) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { accountId: aAccount.id, editDialog: true } queryParams: { accountId: aAccount.id, editDialog: true }
}); });
@ -194,7 +195,7 @@ export class GfAccountsPageComponent implements OnInit {
name, name,
platformId, platformId,
tags tags
}: AccountModel & { tags?: Tag[] }) { }: AccountWithValue & { tags?: Tag[] }) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfCreateOrUpdateAccountDialogComponent, GfCreateOrUpdateAccountDialogComponent,
CreateOrUpdateAccountDialogParams CreateOrUpdateAccountDialogParams

8
apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts

@ -1,9 +1,13 @@
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { AccountWithBalance } from '@ghostfolio/common/types';
import { Account, Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
export interface CreateOrUpdateAccountDialogParams { export interface CreateOrUpdateAccountDialogParams {
account: Omit<Account, 'createdAt' | 'id' | 'updatedAt' | 'userId'> & { account: Omit<
AccountWithBalance,
'createdAt' | 'id' | 'updatedAt' | 'userId'
> & {
id: string | null; id: string | null;
tags?: Tag[]; tags?: Tag[];
}; };

2
libs/common/src/lib/config.ts

@ -115,7 +115,6 @@ export const DEFAULT_REDACTED_PATHS = [
'accounts[*].interestInBaseCurrency', 'accounts[*].interestInBaseCurrency',
'accounts[*].value', 'accounts[*].value',
'accounts[*].valueInBaseCurrency', 'accounts[*].valueInBaseCurrency',
'activities[*].account.balance',
'activities[*].account.comment', 'activities[*].account.comment',
'activities[*].assetProfile.symbolMapping', 'activities[*].assetProfile.symbolMapping',
'activities[*].assetProfile.watchedByCount', 'activities[*].assetProfile.watchedByCount',
@ -128,7 +127,6 @@ export const DEFAULT_REDACTED_PATHS = [
'activities[*].valueInBaseCurrency', 'activities[*].valueInBaseCurrency',
'balance', 'balance',
'balanceInBaseCurrency', 'balanceInBaseCurrency',
'balances[*].account.balance',
'balances[*].account.comment', 'balances[*].account.comment',
'balances[*].value', 'balances[*].value',
'balances[*].valueInBaseCurrency', 'balances[*].valueInBaseCurrency',

7
libs/common/src/lib/dtos/create-account.dto.ts

@ -12,8 +12,13 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class CreateAccountDto { export class CreateAccountDto {
/**
* The initial balance, stored as the account balance of today.
* Optional because callers may instead supply the full history via `balances`.
*/
@IsNumber() @IsNumber()
balance: number; @IsOptional()
balance?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()

7
libs/common/src/lib/dtos/update-account.dto.ts

@ -12,8 +12,13 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class UpdateAccountDto { export class UpdateAccountDto {
/**
* The balance, stored as the account balance of today.
* Optional because the account balances are the source of truth.
*/
@IsNumber() @IsNumber()
balance: number; @IsOptional()
balance?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()

5
libs/common/src/lib/types/account-with-balance.type.ts

@ -0,0 +1,5 @@
import { Account as AccountModel } from '@prisma/client';
export type AccountWithBalance = AccountModel & {
balance: number;
};

6
libs/common/src/lib/types/account-with-value.type.ts

@ -1,6 +1,8 @@
import { Account as AccountModel, Platform, Tag } from '@prisma/client'; import { Platform, Tag } from '@prisma/client';
export type AccountWithValue = AccountModel & { import { AccountWithBalance } from './account-with-balance.type';
export type AccountWithValue = AccountWithBalance & {
activitiesCount: number; activitiesCount: number;
allocationInPercentage: number; allocationInPercentage: number;
balanceInBaseCurrency: number; balanceInBaseCurrency: number;

2
libs/common/src/lib/types/index.ts

@ -1,5 +1,6 @@
import type { AccessType } from './access-type.type'; import type { AccessType } from './access-type.type';
import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
import type { AccountWithBalance } from './account-with-balance.type';
import type { AccountWithPlatform } from './account-with-platform.type'; import type { AccountWithPlatform } from './account-with-platform.type';
import type { AccountWithValue } from './account-with-value.type'; import type { AccountWithValue } from './account-with-value.type';
import type { AiPromptMode } from './ai-prompt-mode.type'; import type { AiPromptMode } from './ai-prompt-mode.type';
@ -28,6 +29,7 @@ import type { ViewMode } from './view-mode.type';
export type { export type {
AccessType, AccessType,
AccessWithGranteeUser, AccessWithGranteeUser,
AccountWithBalance,
AccountWithPlatform, AccountWithPlatform,
AccountWithValue, AccountWithValue,
AiPromptMode, AiPromptMode,

20
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -1,3 +1,5 @@
import { AccountWithValue } from '@ghostfolio/common/types';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -14,16 +16,18 @@ import { NotificationService } from '../notifications';
import { GfValueComponent } from '../value'; import { GfValueComponent } from '../value';
import { GfAccountsTableComponent } from './accounts-table.component'; import { GfAccountsTableComponent } from './accounts-table.component';
const accounts = [ const accounts: AccountWithValue[] = [
{ {
activitiesCount: 0, activitiesCount: 0,
allocationInPercentage: null, allocationInPercentage: 0.002574748676949956,
balance: 278, balance: 278,
balanceInBaseCurrency: 278, balanceInBaseCurrency: 278,
comment: null, comment: null,
createdAt: new Date('2025-06-01T06:52:49.063Z'), createdAt: new Date('2025-06-01T06:52:49.063Z'),
currency: 'USD', currency: 'USD',
dividendInBaseCurrency: 0,
id: '460d7401-ca43-4ed4-b08e-349f1822e9db', id: '460d7401-ca43-4ed4-b08e-349f1822e9db',
interestInBaseCurrency: 0,
name: 'Coinbase Account', name: 'Coinbase Account',
platform: { platform: {
id: '8dc24b88-bb92-4152-af25-fe6a31643e26', id: '8dc24b88-bb92-4152-af25-fe6a31643e26',
@ -38,13 +42,15 @@ const accounts = [
}, },
{ {
activitiesCount: 0, activitiesCount: 0,
allocationInPercentage: null, allocationInPercentage: 0.11114023065971035,
balance: 12000, balance: 12000,
balanceInBaseCurrency: 12000, balanceInBaseCurrency: 12000,
comment: null, comment: null,
createdAt: new Date('2025-06-01T06:48:53.055Z'), createdAt: new Date('2025-06-01T06:48:53.055Z'),
currency: 'USD', currency: 'USD',
dividendInBaseCurrency: 0,
id: '6d773e31-0583-4c85-a247-e69870b4f1ee', id: '6d773e31-0583-4c85-a247-e69870b4f1ee',
interestInBaseCurrency: 0,
name: 'Private Banking Account', name: 'Private Banking Account',
platform: { platform: {
id: '43e8fcd1-5b79-4100-b678-d2229bd1660d', id: '43e8fcd1-5b79-4100-b678-d2229bd1660d',
@ -59,13 +65,15 @@ const accounts = [
}, },
{ {
activitiesCount: 12, activitiesCount: 12,
allocationInPercentage: null, allocationInPercentage: 0.8862850206633397,
balance: 150.2, balance: 150.2,
balanceInBaseCurrency: 150.2, balanceInBaseCurrency: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',
dividendInBaseCurrency: 0,
id: '776bd1e9-b2f6-4f7e-933d-18756c2f0625', id: '776bd1e9-b2f6-4f7e-933d-18756c2f0625',
interestInBaseCurrency: 0,
name: 'Trading Account', name: 'Trading Account',
platform: { platform: {
id: '9da3a8a7-4795-43e3-a6db-ccb914189737', id: '9da3a8a7-4795-43e3-a6db-ccb914189737',
@ -73,10 +81,10 @@ const accounts = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
}, },
platformId: '9da3a8a7-4795-43e3-a6db-ccb914189737', platformId: '9da3a8a7-4795-43e3-a6db-ccb914189737',
valueInBaseCurrency: 95693.70321466809,
updatedAt: new Date('2025-06-01T06:53:10.569Z'), updatedAt: new Date('2025-06-01T06:53:10.569Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
value: 95693.70321466809 value: 95693.70321466809,
valueInBaseCurrency: 95693.70321466809
} }
]; ];

14
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -4,6 +4,7 @@ import {
getLowercase, getLowercase,
isAccountExcluded isAccountExcluded
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { AccountWithValue } from '@ghostfolio/common/types';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -24,7 +25,6 @@ import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router'; import { Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { Account } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
arrowRedoOutline, arrowRedoOutline,
@ -55,7 +55,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './accounts-table.component.html' templateUrl: './accounts-table.component.html'
}) })
export class GfAccountsTableComponent { export class GfAccountsTableComponent {
public readonly accounts = input.required<Account[]>(); public readonly accounts = input.required<AccountWithValue[]>();
public readonly activitiesCount = input<number>(); public readonly activitiesCount = input<number>();
public readonly baseCurrency = input<string>(); public readonly baseCurrency = input<string>();
public readonly hasPermissionToOpenDetails = input(true); public readonly hasPermissionToOpenDetails = input(true);
@ -71,12 +71,12 @@ export class GfAccountsTableComponent {
public readonly totalValueInBaseCurrency = input<number>(); public readonly totalValueInBaseCurrency = input<number>();
public readonly accountDeleted = output<string>(); public readonly accountDeleted = output<string>();
public readonly accountToUpdate = output<Account>(); public readonly accountToUpdate = output<AccountWithValue>();
public readonly transferBalance = output<void>(); public readonly transferBalance = output<void>();
public readonly sort = viewChild.required(MatSort); public readonly sort = viewChild.required(MatSort);
protected readonly dataSource = new MatTableDataSource<Account>([]); protected readonly dataSource = new MatTableDataSource<AccountWithValue>([]);
protected readonly displayedColumns = computed(() => { protected readonly displayedColumns = computed(() => {
const columns = ['status', 'account', 'platform']; const columns = ['status', 'account', 'platform'];
@ -141,7 +141,9 @@ export class GfAccountsTableComponent {
}); });
} }
protected isExcluded(account: Account & { tags?: { id: string }[] }) { protected isExcluded(
account: AccountWithValue & { tags?: { id: string }[] }
) {
return isAccountExcluded(account); return isAccountExcluded(account);
} }
@ -173,7 +175,7 @@ export class GfAccountsTableComponent {
this.transferBalance.emit(); this.transferBalance.emit();
} }
protected onUpdateAccount(aAccount: Account) { protected onUpdateAccount(aAccount: AccountWithValue) {
this.accountToUpdate.emit(aAccount); this.accountToUpdate.emit(aAccount);
} }
} }

5
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -39,7 +39,6 @@ const activities: Activity[] = [
updatedAt: new Date('2025-05-31T18:43:01.840Z'), updatedAt: new Date('2025-05-31T18:43:01.840Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
account: { account: {
balance: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',
@ -105,7 +104,6 @@ const activities: Activity[] = [
updatedAt: new Date('2025-05-31T18:46:14.175Z'), updatedAt: new Date('2025-05-31T18:46:14.175Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
account: { account: {
balance: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',
@ -171,7 +169,6 @@ const activities: Activity[] = [
updatedAt: new Date('2025-05-31T18:49:54.064Z'), updatedAt: new Date('2025-05-31T18:49:54.064Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
account: { account: {
balance: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',
@ -237,7 +234,6 @@ const activities: Activity[] = [
updatedAt: new Date('2025-05-31T18:48:48.209Z'), updatedAt: new Date('2025-05-31T18:48:48.209Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
account: { account: {
balance: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',
@ -303,7 +299,6 @@ const activities: Activity[] = [
updatedAt: new Date('2025-05-31T18:46:44.616Z'), updatedAt: new Date('2025-05-31T18:46:44.616Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e', userId: '081aa387-487d-4438-83a4-3060eb2a016e',
account: { account: {
balance: 150.2,
comment: null, comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'), createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD', currency: 'USD',

2
prisma/migrations/20260805120000_removed_balance_from_account/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" DROP COLUMN "balance";

1
prisma/schema.prisma

@ -27,7 +27,6 @@ model Access {
model Account { model Account {
activities Order[] activities Order[]
balance Float @default(0)
balances AccountBalance[] balances AccountBalance[]
comment String? comment String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())

1
test/import/not-ok/invalid-platform.json

@ -5,7 +5,6 @@
}, },
"accounts": [ "accounts": [
{ {
"balance": 0,
"balances": [], "balances": [],
"currency": "USD", "currency": "USD",
"id": "e62be662-a2c8-4cff-8b79-dc0a46576659", "id": "e62be662-a2c8-4cff-8b79-dc0a46576659",

1
test/import/ok/500-activities.json

@ -5,7 +5,6 @@
}, },
"accounts": [ "accounts": [
{ {
"balance": 2000,
"currency": "USD", "currency": "USD",
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0", "id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"name": "My Online Trading Account", "name": "My Online Trading Account",

1
test/import/ok/derived-currency.json

@ -5,7 +5,6 @@
}, },
"accounts": [ "accounts": [
{ {
"balance": 2000,
"currency": "USD", "currency": "USD",
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0", "id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"name": "My Online Trading Account", "name": "My Online Trading Account",

1
test/import/ok/sample.json

@ -5,7 +5,6 @@
}, },
"accounts": [ "accounts": [
{ {
"balance": 2000,
"balances": [ "balances": [
{ {
"date": "2024-12-31T00:00:00.000Z", "date": "2024-12-31T00:00:00.000Z",

Loading…
Cancel
Save