diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9c86b9e..f83f4d8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migrated the clone, create and edit activity dialogs to dedicated routes +### Fixed + +- Fixed the missing validation of the tags when creating or updating an activity +- Fixed the missing validation of the tags when updating the tags of a holding +- Fixed an issue where the tags of an activity were lost if updating the activity failed +- Fixed an issue where the dividends, the interest and the liabilities of asset profiles without market data have been valued at zero in the portfolio calculation +- Fixed an issue where an error has been reported for asset profiles without market data which do not hold any units +- Fixed an issue with removing a linked account from a buy, sell or dividend activity + ## 3.27.0 - 2026-07-15 ### Changed diff --git a/README.md b/README.md index ce89412a3..473d5c367 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ The frontend is built with [Angular](https://angular.dev) and uses [Angular Mate We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64`, `linux/arm/v7` and `linux/arm64`. +Find answers to commonly asked questions about self-hosting Ghostfolio in our [Frequently Asked Questions (FAQ)](https://ghostfol.io/en/faq/self-hosting) section. +
[Buy me a coffee button](https://www.buymeacoffee.com/ghostfolio) diff --git a/apps/api/src/app/account-balance/account-balance.module.ts b/apps/api/src/app/account-balance/account-balance.module.ts index 02323acc9..f7b1efc51 100644 --- a/apps/api/src/app/account-balance/account-balance.module.ts +++ b/apps/api/src/app/account-balance/account-balance.module.ts @@ -1,6 +1,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -10,7 +11,7 @@ import { AccountBalanceService } from './account-balance.service'; @Module({ controllers: [AccountBalanceController], exports: [AccountBalanceService], - imports: [ExchangeRateDataModule, PrismaModule], + imports: [ExchangeRateDataModule, PrismaModule, TagModule], providers: [AccountBalanceService, AccountService] }) export class AccountBalanceModule {} diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 253c7fb1d..47b859ba3 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -7,6 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -25,6 +26,7 @@ import { AccountService } from './account.service'; PortfolioModule, PrismaModule, RedactValuesInResponseModule, + TagModule, UserModule ], providers: [AccountService] diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index d1f4009a9..f84f085a3 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -3,10 +3,11 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed. import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Filter } from '@ghostfolio/common/interfaces'; -import { HttpException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Account, @@ -19,7 +20,6 @@ import { } from '@prisma/client'; import { Big } from 'big.js'; import { format } from 'date-fns'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { groupBy } from 'lodash'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -30,7 +30,8 @@ export class AccountService { private readonly accountBalanceService: AccountBalanceService, private readonly eventEmitter: EventEmitter2, private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly prismaService: PrismaService + private readonly prismaService: PrismaService, + private readonly tagService: TagService ) {} public async account({ @@ -127,7 +128,7 @@ export class AccountService { aUserId: string, tagIds?: string[] ): Promise { - await this.validateTagIds(tagIds, aUserId); + await this.tagService.validateTagIds({ tagIds, userId: aUserId }); const account = await this.prismaService.account.create({ data: { @@ -259,15 +260,15 @@ export class AccountService { public async updateAccount( params: { - where: Prisma.AccountWhereUniqueInput; data: Prisma.AccountUpdateInput; + where: Prisma.AccountWhereUniqueInput; }, aUserId: string, tagIds?: string[] ): Promise { const { data, where } = params; - await this.validateTagIds(tagIds, aUserId); + await this.tagService.validateTagIds({ tagIds, userId: aUserId }); const account = await this.prismaService.account.update({ data: { @@ -340,26 +341,4 @@ export class AccountService { }); } } - - private async validateTagIds(tagIds: string[], userId: string) { - if (!tagIds?.length) { - return; - } - - const uniqueTagIds = Array.from(new Set(tagIds)); - - const tagsCount = await this.prismaService.tag.count({ - where: { - id: { in: uniqueTagIds }, - OR: [{ userId }, { userId: null }] - } - }); - - if (tagsCount !== uniqueTagIds.length) { - throw new HttpException( - getReasonPhrase(StatusCodes.BAD_REQUEST), - StatusCodes.BAD_REQUEST - ); - } - } } diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index d00dd7292..21dabc639 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -315,11 +315,13 @@ export class ActivitiesController { data: { ...data, date, - account: { - connect: { - id_userId: { id: accountId, userId: this.request.user.id } - } - }, + account: accountId + ? { + connect: { + id_userId: { id: accountId, userId: this.request.user.id } + } + } + : { disconnect: true }, SymbolProfile: { connect: { dataSource_symbol: { @@ -338,6 +340,7 @@ export class ActivitiesController { }), user: { connect: { id: this.request.user.id } } }, + userId: this.request.user.id, where: { id } diff --git a/apps/api/src/app/activities/activities.module.ts b/apps/api/src/app/activities/activities.module.ts index 661163ff1..34091ba5e 100644 --- a/apps/api/src/app/activities/activities.module.ts +++ b/apps/api/src/app/activities/activities.module.ts @@ -14,6 +14,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -36,6 +37,7 @@ import { ActivitiesService } from './activities.service'; RedactValuesInResponseModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule ], diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 427cddf4c..df616a5dd 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -12,6 +12,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, @@ -61,7 +62,8 @@ export class ActivitiesService { private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, - private readonly symbolProfileService: SymbolProfileService + private readonly symbolProfileService: SymbolProfileService, + private readonly tagService: TagService ) {} public areCashActivitiesExcludedByFilters(filters: Filter[] = []) { @@ -107,6 +109,13 @@ export class ActivitiesService { tags, userId }: { tags: Tag[]; userId: string } & AssetProfileIdentifier) { + await this.tagService.validateTagIds({ + userId, + tagIds: tags.map(({ id }) => { + return id; + }) + }); + const activities = await this.prismaService.order.findMany({ where: { userId, @@ -153,6 +162,15 @@ export class ActivitiesService { userId: string; } ): Promise { + const tags = data.tags ?? []; + + await this.tagService.validateTagIds({ + tagIds: tags.map(({ id }) => { + return id; + }), + userId: data.userId + }); + let account: Prisma.AccountCreateNestedOneWithoutActivitiesInput; if (data.accountId) { @@ -167,7 +185,6 @@ export class ActivitiesService { } const accountId = data.accountId; - const tags = data.tags ?? []; const updateAccountBalance = data.updateAccountBalance ?? false; const userId = data.userId; @@ -932,6 +949,7 @@ export class ActivitiesService { public async updateActivity({ data, + userId, where }: { data: Prisma.OrderUpdateInput & { @@ -942,14 +960,22 @@ export class ActivitiesService { tags?: { id: string }[]; type?: ActivityType; }; + userId: string; where: Prisma.OrderWhereUniqueInput; }): Promise { + const tags = data.tags ?? []; + + await this.tagService.validateTagIds({ + userId, + tagIds: tags.map(({ id }) => { + return id; + }) + }); + if (!data.comment) { data.comment = null; } - const tags = data.tags ?? []; - let isDraft = false; if ( @@ -957,10 +983,6 @@ export class ActivitiesService { (data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' && data.type === 'BUY') ) { - if (data.account?.connect?.id_userId?.id === null) { - data.account = { disconnect: true }; - } - delete data.SymbolProfile.connect; delete data.SymbolProfile.update.name; } else { @@ -989,19 +1011,13 @@ export class ActivitiesService { delete data.symbol; delete data.tags; - // Remove existing tags - await this.prismaService.order.update({ - where, - data: { tags: { set: [] } } - }); - const activity = await this.prismaService.order.update({ where, data: { ...data, isDraft, tags: { - connect: tags + set: tags } } }); diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index 5267f40c8..d5cf0e3e9 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -44,6 +45,7 @@ import { AiService } from './ai.service'; PropertyModule, RedisCacheModule, SymbolProfileModule, + TagModule, UserModule ], providers: [ diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts index 2bcd6177d..3c540c337 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts @@ -23,6 +23,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -46,6 +47,7 @@ import { BenchmarksService } from './benchmarks.service'; RedisCacheModule, SymbolModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule, UserModule diff --git a/apps/api/src/app/endpoints/public/public.module.ts b/apps/api/src/app/endpoints/public/public.module.ts index e8395228f..b992694c5 100644 --- a/apps/api/src/app/endpoints/public/public.module.ts +++ b/apps/api/src/app/endpoints/public/public.module.ts @@ -18,6 +18,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -38,6 +39,7 @@ import { PublicController } from './public.controller'; PrismaModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, UserModule ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 589168989..31e451051 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -91,6 +91,7 @@ describe('PortfolioCalculator', () => { accountBalanceService, null, exchangeRateDataService, + null, null ); @@ -122,6 +123,7 @@ describe('PortfolioCalculator', () => { exchangeRateDataService, null, null, + null, null ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index e5798818f..4e413c0c5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -113,7 +113,7 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), errors: [], - hasErrors: true, + hasErrors: false, positions: [], totalFeesWithCurrencyEffect: new Big('49'), totalInterestWithCurrencyEffect: new Big('0'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index f79e5bc79..747fca7a5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -236,6 +236,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }; } + // The dividends, the interest and the liabilities are derived from the + // activities only. Accumulate them upfront so that they survive the bail + // out for symbols without a market price below. + for (const order of orders) { + const exchangeRateAtOrderDate = exchangeRates[order.date]; + + if (order.type === 'DIVIDEND') { + const dividend = order.quantity.mul(order.unitPrice); + + totalDividend = totalDividend.plus(dividend); + totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( + dividend.mul(exchangeRateAtOrderDate ?? 1) + ); + } else if (order.type === 'INTEREST') { + const interest = order.quantity.mul(order.unitPrice); + + totalInterest = totalInterest.plus(interest); + totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( + interest.mul(exchangeRateAtOrderDate ?? 1) + ); + } else if (order.type === 'LIABILITY') { + const liabilities = order.quantity.mul(order.unitPrice); + + totalLiabilities = totalLiabilities.plus(liabilities); + totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( + liabilities.mul(exchangeRateAtOrderDate ?? 1) + ); + } + } + const dateOfFirstTransaction = new Date(orders[0].date); const endDateString = format(end, DATE_FORMAT); @@ -263,7 +293,20 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { !unitPriceAtEndDate || (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) ) { + // A missing market price can only affect the units which are held. The + // dividends, the interest and the liabilities do not hold any units and + // are therefore not in error. + const hasActivitiesWithUnits = orders.some(({ type }) => { + return ['BUY', 'SELL'].includes(type); + }); + return { + totalDividend, + totalDividendInBaseCurrency, + totalInterest, + totalInterestInBaseCurrency, + totalLiabilities, + totalLiabilitiesInBaseCurrency, currentValues: {}, currentValuesWithCurrencyEffect: {}, feesWithCurrencyEffect: new Big(0), @@ -271,7 +314,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformanceWithCurrencyEffect: new Big(0), - hasErrors: true, + hasErrors: hasActivitiesWithUnits, initialValue: new Big(0), initialValueWithCurrencyEffect: new Big(0), investmentValuesAccumulated: {}, @@ -288,14 +331,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { timeWeightedInvestmentValuesWithCurrencyEffect: {}, timeWeightedInvestmentWithCurrencyEffect: new Big(0), totalAccountBalanceInBaseCurrency: new Big(0), - totalDividend: new Big(0), - totalDividendInBaseCurrency: new Big(0), - totalInterest: new Big(0), - totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0), - totalLiabilities: new Big(0), - totalLiabilitiesInBaseCurrency: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0) }; } @@ -415,29 +452,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { const exchangeRateAtOrderDate = exchangeRates[order.date]; - if (order.type === 'DIVIDEND') { - const dividend = order.quantity.mul(order.unitPrice); - - totalDividend = totalDividend.plus(dividend); - totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( - dividend.mul(exchangeRateAtOrderDate ?? 1) - ); - } else if (order.type === 'INTEREST') { - const interest = order.quantity.mul(order.unitPrice); - - totalInterest = totalInterest.plus(interest); - totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( - interest.mul(exchangeRateAtOrderDate ?? 1) - ); - } else if (order.type === 'LIABILITY') { - const liabilities = order.quantity.mul(order.unitPrice); - - totalLiabilities = totalLiabilities.plus(liabilities); - totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( - liabilities.mul(exchangeRateAtOrderDate ?? 1) - ); - } - if (order.itemType === 'start') { // Take the unit price of the order as the market price if there are no // orders of this symbol before the start date diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index d818195ca..7f7a894df 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -50,6 +51,7 @@ import { RulesService } from './rules.service'; RedactValuesInResponseModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule, UserModule diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index adddb4013..c16590cce 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -54,6 +54,7 @@ describe('PortfolioService', () => { null, null, exchangeRateDataService, + null, null ); @@ -67,6 +68,7 @@ describe('PortfolioService', () => { exchangeRateDataService, null, null, + null, null ); diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index d967ce854..38fb90a69 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -1,7 +1,8 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; -import { Injectable } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import { Prisma, Tag } from '@prisma/client'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Injectable() export class TagService { @@ -124,4 +125,39 @@ export class TagService { where }); } + + public async validateTagIds({ + tagIds, + userId + }: { + tagIds: string[]; + userId: string; + }) { + if (!tagIds?.length) { + return; + } + + if (!userId) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + + const uniqueTagIds = Array.from(new Set(tagIds)); + + const tagsCount = await this.prismaService.tag.count({ + where: { + id: { in: uniqueTagIds }, + OR: [{ userId }, { userId: null }] + } + }); + + if (tagsCount !== uniqueTagIds.length) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + } } diff --git a/libs/common/src/lib/dtos/create-order.dto.ts b/libs/common/src/lib/dtos/create-order.dto.ts index 9236a12d8..36365b7be 100644 --- a/libs/common/src/lib/dtos/create-order.dto.ts +++ b/libs/common/src/lib/dtos/create-order.dto.ts @@ -4,6 +4,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code'; import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; import { Transform, TransformFnParams } from 'class-transformer'; import { + ArrayUnique, IsArray, IsBoolean, IsEnum, @@ -62,8 +63,10 @@ export class CreateOrderDto { @IsString() symbol: string; + @ArrayUnique() @IsArray() @IsOptional() + @IsString({ each: true }) tags?: string[]; @IsEnum(Type) diff --git a/libs/common/src/lib/dtos/update-order.dto.ts b/libs/common/src/lib/dtos/update-order.dto.ts index 7843f142c..991479bc8 100644 --- a/libs/common/src/lib/dtos/update-order.dto.ts +++ b/libs/common/src/lib/dtos/update-order.dto.ts @@ -4,6 +4,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code'; import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; import { Transform, TransformFnParams } from 'class-transformer'; import { + ArrayUnique, IsArray, IsEnum, IsISO8601, @@ -63,8 +64,10 @@ export class UpdateOrderDto { @IsString() symbol: string; + @ArrayUnique() @IsArray() @IsOptional() + @IsString({ each: true }) tags?: string[]; @IsString()