diff --git a/CHANGELOG.md b/CHANGELOG.md index 28173967b..c3b555960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the chart in the account detail dialog +## 2.75.0 - 2024-04-21 + +### Added + +- Added `accountId` and `date` as a unique constraint to the `AccountBalance` database schema + +### Fixed + +- Fixed an issue with `totalValueInBaseCurrency` in the value redaction interceptor for the impersonation mode + ## 2.74.0 - 2024-04-20 ### Added diff --git a/README.md b/README.md index 2c8cb664c..d63bb10d0 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ [![Shield: Contributions Welcome](https://img.shields.io/badge/Contributions-Welcome-orange.svg)](#contributing) [![Shield: License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) -New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2) - **Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation. diff --git a/apps/api/src/app/account-balance/account-balance.controller.ts b/apps/api/src/app/account-balance/account-balance.controller.ts index 12f21753b..4a8412003 100644 --- a/apps/api/src/app/account-balance/account-balance.controller.ts +++ b/apps/api/src/app/account-balance/account-balance.controller.ts @@ -1,6 +1,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { resetHours } from '@ghostfolio/common/helper'; import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -17,6 +18,7 @@ import { import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { AccountBalance } from '@prisma/client'; +import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AccountBalanceService } from './account-balance.service'; @@ -50,17 +52,11 @@ export class AccountBalanceController { ); } - return this.accountBalanceService.createAccountBalance({ - Account: { - connect: { - id_userId: { - id: account.id, - userId: account.userId - } - } - }, + return this.accountBalanceService.createOrUpdateAccountBalance({ + accountId: account.id, + balance: data.balance, date: data.date, - value: data.balance + userId: account.userId }); } diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts index 8a9d7b83e..5a5ec5be0 100644 --- a/apps/api/src/app/account-balance/account-balance.service.ts +++ b/apps/api/src/app/account-balance/account-balance.service.ts @@ -1,10 +1,14 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { resetHours } from '@ghostfolio/common/helper'; import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces'; import { UserWithSettings } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { AccountBalance, Prisma } from '@prisma/client'; +import { parseISO } from 'date-fns'; + +import { CreateAccountBalanceDto } from './create-account-balance.dto'; @Injectable() export class AccountBalanceService { @@ -24,11 +28,36 @@ export class AccountBalanceService { }); } - public async createAccountBalance( - data: Prisma.AccountBalanceCreateInput - ): Promise { - return this.prismaService.accountBalance.create({ - data + public async createOrUpdateAccountBalance({ + accountId, + balance, + date, + userId + }: CreateAccountBalanceDto & { + userId: string; + }): Promise { + return this.prismaService.accountBalance.upsert({ + create: { + Account: { + connect: { + id_userId: { + userId, + id: accountId + } + } + }, + date: resetHours(parseISO(date)), + value: balance + }, + update: { + value: balance + }, + where: { + accountId_date: { + accountId, + date: resetHours(parseISO(date)) + } + } }); } diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 697041645..fed1860cd 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -6,6 +6,7 @@ import { Filter } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { Account, Order, Platform, Prisma } from '@prisma/client'; import { Big } from 'big.js'; +import { parseISO } from 'date-fns'; import { groupBy } from 'lodash'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -242,17 +243,11 @@ export class AccountService { ); if (amountInCurrencyOfAccount) { - await this.accountBalanceService.createAccountBalance({ - date, - Account: { - connect: { - id_userId: { - userId, - id: accountId - } - } - }, - value: new Big(balance).plus(amountInCurrencyOfAccount).toNumber() + await this.accountBalanceService.createOrUpdateAccountBalance({ + accountId, + userId, + balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber(), + date: date.toISOString() }); } } diff --git a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts index 78ae918d2..17d4e3c17 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -57,6 +57,7 @@ export class RedactValuesInResponseInterceptor 'quantity', 'symbolMapping', 'totalBalanceInBaseCurrency', + 'totalValueInBaseCurrency', 'unitPrice', 'value', 'valueInBaseCurrency' diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index 579b35702..331defae4 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -2,12 +2,6 @@
-

Manage your wealth like a boss

diff --git a/package.json b/package.json index 49a992841..505db31f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.74.0", + "version": "2.75.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", diff --git a/prisma/migrations/20240421080039_added_account_id_and_date_to_account_balance_as_unique_constraint/migration.sql b/prisma/migrations/20240421080039_added_account_id_and_date_to_account_balance_as_unique_constraint/migration.sql new file mode 100644 index 000000000..96521bbfa --- /dev/null +++ b/prisma/migrations/20240421080039_added_account_id_and_date_to_account_balance_as_unique_constraint/migration.sql @@ -0,0 +1,29 @@ +-- Only keep the newest AccountBalance entry for each account / day +WITH entries_to_keep AS ( + SELECT + id, + "accountId", + date, + ROW_NUMBER() OVER (PARTITION BY "accountId", DATE(date) ORDER BY date DESC) AS row_num + FROM + "AccountBalance" +), +entries_to_delete AS ( + SELECT + id + FROM + entries_to_keep + WHERE + row_num > 1 +) +DELETE FROM + "AccountBalance" +WHERE + id IN (SELECT id FROM entries_to_delete); + +-- Reset time part of the date +UPDATE "AccountBalance" +SET date = DATE_TRUNC('day', date); + +-- CreateIndex +CREATE UNIQUE INDEX "AccountBalance_accountId_date_key" ON "AccountBalance"("accountId", "date"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f9a17d114..af7ad1845 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -59,6 +59,7 @@ model AccountBalance { value Float Account Account @relation(fields: [accountId, userId], onDelete: Cascade, references: [id, userId]) + @@unique([accountId, date]) @@index([accountId]) @@index([date]) }