Browse Source

Merge branch 'main' into feature/upgrade-prisma-to-version-5.16.1

pull/3526/head
Thomas Kaul 1 year ago
committed by GitHub
parent
commit
c07626942b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 5
      apps/api/src/app/account/create-account.dto.ts
  3. 5
      apps/api/src/app/account/update-account.dto.ts
  4. 5
      apps/api/src/app/admin/update-asset-profile.dto.ts
  5. 9
      apps/api/src/app/benchmark/benchmark.service.ts
  6. 6
      apps/api/src/app/order/create-order.dto.ts
  7. 1
      apps/api/src/app/order/order.controller.ts
  8. 27
      apps/api/src/app/order/order.service.ts
  9. 6
      apps/api/src/app/order/update-order.dto.ts
  10. 4
      apps/api/src/app/user/update-user-setting.dto.ts
  11. 2
      apps/api/src/app/user/user.module.ts
  12. 6
      apps/api/src/app/user/user.service.ts
  13. 44
      apps/api/src/validators/is-currency-code.ts
  14. 61
      libs/common/src/lib/personal-finance-tools.ts
  15. 30
      test/import/ok-derived-currency.json

7
CHANGELOG.md

@ -9,8 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Added support for derived currencies in the currency validation
- Added support for automatic deletion of unused asset profiles when deleting activities
- Improved the caching of the benchmarks in the markets overview (only cache if needed)
- Upgraded `prisma` from version `5.15.0` to `5.16.1` - Upgraded `prisma` from version `5.15.0` to `5.16.1`
### Fixed
- Fixed an issue with the all time high in the benchmarks of the markets overview
## 2.91.0 - 2024-06-26 ## 2.91.0 - 2024-06-26
### Added ### Added

5
apps/api/src/app/account/create-account.dto.ts

@ -1,7 +1,8 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsISO4217CurrencyCode,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
@ -20,7 +21,7 @@ export class CreateAccountDto {
) )
comment?: string; comment?: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
currency: string; currency: string;
@IsOptional() @IsOptional()

5
apps/api/src/app/account/update-account.dto.ts

@ -1,7 +1,8 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsISO4217CurrencyCode,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
@ -20,7 +21,7 @@ export class UpdateAccountDto {
) )
comment?: string; comment?: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
currency: string; currency: string;
@IsString() @IsString()

5
apps/api/src/app/admin/update-asset-profile.dto.ts

@ -1,8 +1,9 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client'; import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsObject, IsObject,
IsOptional, IsOptional,
IsString, IsString,
@ -26,7 +27,7 @@ export class UpdateAssetProfileDto {
@IsOptional() @IsOptional()
countries?: Prisma.InputJsonArray; countries?: Prisma.InputJsonArray;
@IsISO4217CurrencyCode() @IsCurrencyCode()
@IsOptional() @IsOptional()
currency?: string; currency?: string;

9
apps/api/src/app/benchmark/benchmark.service.ts

@ -135,7 +135,7 @@ export class BenchmarkService {
Promise.all(promisesAllTimeHighs), Promise.all(promisesAllTimeHighs),
Promise.all(promisesBenchmarkTrends) Promise.all(promisesBenchmarkTrends)
]); ]);
let storeInCache = true; let storeInCache = useCache;
benchmarks = allTimeHighs.map((allTimeHigh, index) => { benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { marketPrice } = const { marketPrice } =
@ -161,7 +161,10 @@ export class BenchmarkService {
performances: { performances: {
allTimeHigh: { allTimeHigh: {
date: allTimeHigh?.date, date: allTimeHigh?.date,
performancePercent: performancePercentFromAllTimeHigh performancePercent:
performancePercentFromAllTimeHigh >= 0
? 0
: performancePercentFromAllTimeHigh
} }
}, },
symbol: benchmarkAssetProfiles[index].symbol, symbol: benchmarkAssetProfiles[index].symbol,
@ -419,7 +422,7 @@ export class BenchmarkService {
private getMarketCondition( private getMarketCondition(
aPerformanceInPercent: number aPerformanceInPercent: number
): Benchmark['marketCondition'] { ): Benchmark['marketCondition'] {
if (aPerformanceInPercent === 0) { if (aPerformanceInPercent >= 0) {
return 'ALL_TIME_HIGH'; return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) { } else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET'; return 'BEAR_MARKET';

6
apps/api/src/app/order/create-order.dto.ts

@ -1,3 +1,4 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970'; import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import { import {
@ -12,7 +13,6 @@ import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
@ -42,10 +42,10 @@ export class CreateOrderDto {
) )
comment?: string; comment?: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
currency: string; currency: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
@IsOptional() @IsOptional()
customCurrency?: string; customCurrency?: string;

1
apps/api/src/app/order/order.controller.ts

@ -66,7 +66,6 @@ export class OrderController {
return this.orderService.deleteOrders({ return this.orderService.deleteOrders({
filters, filters,
userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id userId: this.request.user.id
}); });
} }

27
apps/api/src/app/order/order.service.ts

@ -184,7 +184,15 @@ export class OrderService {
where where
}); });
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type)) { const [symbolProfile] =
await this.symbolProfileService.getSymbolProfilesByIds([
order.symbolProfileId
]);
if (
['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type) ||
symbolProfile.activitiesCount === 0
) {
await this.symbolProfileService.deleteById(order.symbolProfileId); await this.symbolProfileService.deleteById(order.symbolProfileId);
} }
@ -200,18 +208,16 @@ export class OrderService {
public async deleteOrders({ public async deleteOrders({
filters, filters,
userCurrency,
userId userId
}: { }: {
filters?: Filter[]; filters?: Filter[];
userCurrency: string;
userId: string; userId: string;
}): Promise<number> { }): Promise<number> {
const { activities } = await this.getOrders({ const { activities } = await this.getOrders({
filters, filters,
userId, userId,
userCurrency,
includeDrafts: true, includeDrafts: true,
userCurrency: undefined,
withExcludedAccounts: true withExcludedAccounts: true
}); });
@ -225,6 +231,19 @@ export class OrderService {
} }
}); });
const symbolProfiles =
await this.symbolProfileService.getSymbolProfilesByIds(
activities.map(({ symbolProfileId }) => {
return symbolProfileId;
})
);
for (const { activitiesCount, id } of symbolProfiles) {
if (activitiesCount === 0) {
await this.symbolProfileService.deleteById(id);
}
}
this.eventEmitter.emit( this.eventEmitter.emit(
PortfolioChangedEvent.getName(), PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({ userId }) new PortfolioChangedEvent({ userId })

6
apps/api/src/app/order/update-order.dto.ts

@ -1,3 +1,4 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970'; import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import { import {
@ -11,7 +12,6 @@ import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
IsISO4217CurrencyCode,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
@ -41,10 +41,10 @@ export class UpdateOrderDto {
) )
comment?: string; comment?: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
currency: string; currency: string;
@IsISO4217CurrencyCode() @IsCurrencyCode()
@IsOptional() @IsOptional()
customCurrency?: string; customCurrency?: string;

4
apps/api/src/app/user/update-user-setting.dto.ts

@ -1,3 +1,4 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import type { import type {
ColorScheme, ColorScheme,
DateRange, DateRange,
@ -7,7 +8,6 @@ import type {
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsISO4217CurrencyCode,
IsISO8601, IsISO8601,
IsIn, IsIn,
IsNumber, IsNumber,
@ -21,7 +21,7 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
annualInterestRate?: number; annualInterestRate?: number;
@IsISO4217CurrencyCode() @IsCurrencyCode()
@IsOptional() @IsOptional()
baseCurrency?: string; baseCurrency?: string;

2
apps/api/src/app/user/user.module.ts

@ -1,3 +1,4 @@
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
@ -19,6 +20,7 @@ import { UserService } from './user.service';
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' } signOptions: { expiresIn: '30 days' }
}), }),
OrderModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,
SubscriptionModule, SubscriptionModule,

6
apps/api/src/app/user/user.service.ts

@ -1,3 +1,4 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
@ -40,6 +41,7 @@ export class UserService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly orderService: OrderService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService, private readonly subscriptionService: SubscriptionService,
@ -398,8 +400,8 @@ export class UserService {
} catch {} } catch {}
try { try {
await this.prismaService.order.deleteMany({ await this.orderService.deleteOrders({
where: { userId: where.id } userId: where.id
}); });
} catch {} } catch {}

44
apps/api/src/validators/is-currency-code.ts

@ -0,0 +1,44 @@
import { DERIVED_CURRENCIES } from '@ghostfolio/common/config';
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments
} from 'class-validator';
import { isISO4217CurrencyCode } from 'class-validator';
export function IsCurrencyCode(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
propertyName,
constraints: [],
options: validationOptions,
target: object.constructor,
validator: IsExtendedCurrencyConstraint
});
};
}
@ValidatorConstraint({ async: false })
export class IsExtendedCurrencyConstraint
implements ValidatorConstraintInterface
{
public defaultMessage(args: ValidationArguments) {
return '$value must be a valid ISO4217 currency code';
}
public validate(currency: any) {
// Return true if currency is a standard ISO 4217 code or a derived currency
return (
isISO4217CurrencyCode(currency) ||
[
...DERIVED_CURRENCIES.map((derivedCurrency) => {
return derivedCurrency.currency;
}),
'USX'
].includes(currency)
);
}
}

61
libs/common/src/lib/personal-finance-tools.ts

@ -18,6 +18,13 @@ export const personalFinanceTools: Product[] = [
origin: `United States`, origin: `United States`,
slogan: 'Investment Software Suite' slogan: 'Investment Software Suite'
}, },
{
founded: 2016,
key: 'alphatrackr',
languages: ['English'],
name: 'AlphaTrackr',
slogan: 'Investment Portfolio Tracking Tool'
},
{ {
founded: 2017, founded: 2017,
hasSelfHostingAbility: false, hasSelfHostingAbility: false,
@ -26,6 +33,17 @@ export const personalFinanceTools: Product[] = [
origin: `Switzerland`, origin: `Switzerland`,
slogan: 'Simplicity for Complex Wealth' slogan: 'Simplicity for Complex Wealth'
}, },
{
founded: 2018,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'anlage.app',
languages: ['English'],
name: 'Anlage.App',
origin: `Austria`,
pricingPerYear: '$120',
slogan: 'Analyze and track your portfolio.'
},
{ {
founded: 2022, founded: 2022,
hasFreePlan: true, hasFreePlan: true,
@ -190,6 +208,16 @@ export const personalFinanceTools: Product[] = [
origin: `Germany`, origin: `Germany`,
slogan: 'Volle Kontrolle über deine Investitionen' slogan: 'Volle Kontrolle über deine Investitionen'
}, },
{
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'holistic-capital',
languages: ['Deutsch'],
name: 'Holistic',
origin: `Germany`,
slogan: 'Die All-in-One Lösung für dein Vermögen.',
useAnonymously: true
},
{ {
hasFreePlan: true, hasFreePlan: true,
hasSelfHostingAbility: false, hasSelfHostingAbility: false,
@ -264,6 +292,17 @@ export const personalFinanceTools: Product[] = [
region: `United States`, region: `United States`,
slogan: 'Your financial future, in your control' slogan: 'Your financial future, in your control'
}, },
{
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'merlincrypto',
languages: ['English'],
name: 'Merlin',
origin: `United States`,
pricingPerYear: '$204',
region: 'Canada, United States',
slogan: 'The smartest way to track your crypto'
},
{ {
founded: 2019, founded: 2019,
hasFreePlan: false, hasFreePlan: false,
@ -331,6 +370,14 @@ export const personalFinanceTools: Product[] = [
pricingPerYear: '$360', pricingPerYear: '$360',
slogan: 'Tools for Better Investors' slogan: 'Tools for Better Investors'
}, },
{
hasFreePlan: true,
key: 'portfoloo',
name: 'Portfoloo',
note: 'Portfoloo has discontinued',
slogan:
'Free Stock Portfolio Tracker with unlimited portfolio and stocks for DIY investors'
},
{ {
founded: 2021, founded: 2021,
hasFreePlan: true, hasFreePlan: true,
@ -370,6 +417,13 @@ export const personalFinanceTools: Product[] = [
pricingPerYear: '$239', pricingPerYear: '$239',
slogan: 'Stock Market Analysis & Tools for Investors' slogan: 'Stock Market Analysis & Tools for Investors'
}, },
{
founded: 2022,
key: 'segmio',
name: 'Segmio',
origin: `Romania`,
slogan: 'Wealth Management and Net Worth Tracking'
},
{ {
founded: 2007, founded: 2007,
hasFreePlan: true, hasFreePlan: true,
@ -381,6 +435,13 @@ export const personalFinanceTools: Product[] = [
region: `Global`, region: `Global`,
slogan: 'Stock Portfolio Tracker' slogan: 'Stock Portfolio Tracker'
}, },
{
hasFreePlan: true,
key: 'sharesmaster',
name: 'SharesMaster',
note: 'SharesMaster has discontinued',
slogan: 'Free Stock Portfolio Tracker'
},
{ {
hasFreePlan: true, hasFreePlan: true,
hasSelfHostingAbility: false, hasSelfHostingAbility: false,

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

@ -0,0 +1,30 @@
{
"meta": {
"date": "2024-06-28T00:00:00.000Z",
"version": "dev"
},
"accounts": [
{
"balance": 2000,
"currency": "USD",
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"isExcluded": false,
"name": "My Online Trading Account",
"platformId": null
}
],
"activities": [
{
"accountId": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
"comment": null,
"fee": 0,
"quantity": 5,
"type": "BUY",
"unitPrice": 10875.00,
"currency": "ZAc",
"dataSource": "YAHOO",
"date": "2024-06-27T22:00:00.000Z",
"symbol": "JSE.JO"
}
]
}
Loading…
Cancel
Save