diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec08080a..c4cf9b41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Refactored the API query parameters in various data provider services +- Extended the _Storybook_ stories of the portfolio proportion chart component by a story using percentage values +- Upgraded `@internationalized/number` from version `3.6.3` to `3.6.5` +- Upgraded `prettier` from version `3.7.2` to `3.7.3` + +### Fixed + +- Improved the country weightings in the _Financial Modeling Prep_ service + +## 2.220.0 - 2025-11-29 + +### Changed + +- Restricted the asset profile data gathering on Sundays to only process outdated asset profiles +- Removed the _Cypress_ testing setup +- Eliminated `uuid` in favor of using `randomUUID` from `node:crypto` +- Upgraded `color` from version `5.0.0` to `5.0.3` +- Upgraded `prettier` from version `3.6.2` to `3.7.2` + +### Fixed + +- Fixed an issue with the exchange rate calculation when converting between derived currencies and their root currencies + ## 2.219.0 - 2025-11-23 ### Added diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 8b5da4965..24467c732 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -93,7 +93,7 @@ export class AdminController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherMax(): Promise { const assetProfileIdentifiers = - await this.dataGatheringService.getAllActiveAssetProfileIdentifiers(); + await this.dataGatheringService.getActiveAssetProfileIdentifiers(); await this.dataGatheringService.addJobsToQueue( assetProfileIdentifiers.map(({ dataSource, symbol }) => { @@ -120,7 +120,7 @@ export class AdminController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async gatherProfileData(): Promise { const assetProfileIdentifiers = - await this.dataGatheringService.getAllActiveAssetProfileIdentifiers(); + await this.dataGatheringService.getActiveAssetProfileIdentifiers(); await this.dataGatheringService.addJobsToQueue( assetProfileIdentifiers.map(({ dataSource, symbol }) => { diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index b09ced4fb..b4ecd37ba 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -82,7 +82,6 @@ export class PublicController { ]); const { activities } = await this.orderService.getOrders({ - includeDrafts: false, sortColumn: 'date', sortDirection: 'desc', take: 10, diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index a5f3dda96..2deef1c44 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -35,7 +35,7 @@ import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; import { omit, uniqBy } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; import { ImportDataDto } from './import-data.dto'; @@ -277,7 +277,7 @@ export class ImportService { // Asset profile belongs to a different user if (existingAssetProfile) { - const symbol = uuidv4(); + const symbol = randomUUID(); assetProfileSymbolMapping[assetProfile.symbol] = symbol; assetProfile.symbol = symbol; } @@ -496,7 +496,7 @@ export class ImportService { accountId: validatedAccount?.id, accountUserId: undefined, createdAt: new Date(), - id: uuidv4(), + id: randomUUID(), isDraft: isAfter(date, endOfToday()), SymbolProfile: { assetClass, diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 7dc6c646d..001d43b7a 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -37,7 +37,7 @@ import { Big } from 'big.js'; import { isUUID } from 'class-validator'; import { endOfToday, isAfter } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; @Injectable() export class OrderService { @@ -143,7 +143,7 @@ export class OrderService { } else { // Create custom asset profile name = name ?? data.SymbolProfile.connectOrCreate.create.symbol; - symbol = uuidv4(); + symbol = randomUUID(); } data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; diff --git a/apps/api/src/services/cron/cron.service.ts b/apps/api/src/services/cron/cron.service.ts index 88fcabce2..ee91a811e 100644 --- a/apps/api/src/services/cron/cron.service.ts +++ b/apps/api/src/services/cron/cron.service.ts @@ -59,7 +59,9 @@ export class CronService { public async runEverySundayAtTwelvePm() { if (await this.isDataGatheringEnabled()) { const assetProfileIdentifiers = - await this.dataGatheringService.getAllActiveAssetProfileIdentifiers(); + await this.dataGatheringService.getActiveAssetProfileIdentifiers({ + maxAge: '60 days' + }); await this.dataGatheringService.addJobsToQueue( assetProfileIdentifiers.map(({ dataSource, symbol }) => { diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index 4123cc6cc..d0d96acac 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -110,12 +110,14 @@ export class CoinGeckoService implements DataProviderInterface { [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; }> { try { + const queryParams = new URLSearchParams({ + from: getUnixTime(from).toString(), + to: getUnixTime(to).toString(), + vs_currency: DEFAULT_CURRENCY.toLowerCase() + }); + const { error, prices, status } = await fetch( - `${ - this.apiUrl - }/coins/${symbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime( - from - )}&to=${getUnixTime(to)}`, + `${this.apiUrl}/coins/${symbol}/market_chart/range?${queryParams.toString()}`, { headers: this.headers, signal: AbortSignal.timeout(requestTimeout) @@ -172,10 +174,13 @@ export class CoinGeckoService implements DataProviderInterface { } try { + const queryParams = new URLSearchParams({ + ids: symbols.join(','), + vs_currencies: DEFAULT_CURRENCY.toLowerCase() + }); + const quotes = await fetch( - `${this.apiUrl}/simple/price?ids=${symbols.join( - ',' - )}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`, + `${this.apiUrl}/simple/price?${queryParams.toString()}`, { headers: this.headers, signal: AbortSignal.timeout(requestTimeout) @@ -219,10 +224,17 @@ export class CoinGeckoService implements DataProviderInterface { let items: LookupItem[] = []; try { - const { coins } = await fetch(`${this.apiUrl}/search?query=${query}`, { - headers: this.headers, - signal: AbortSignal.timeout(requestTimeout) - }).then((res) => res.json()); + const queryParams = new URLSearchParams({ + query + }); + + const { coins } = await fetch( + `${this.apiUrl}/search?${queryParams.toString()}`, + { + headers: this.headers, + signal: AbortSignal.timeout(requestTimeout) + } + ).then((res) => res.json()); items = coins.map(({ id: symbol, name }) => { return { diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index b93ca492a..cd20fca44 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -96,17 +96,19 @@ export class EodHistoricalDataService implements DataProviderInterface { } try { + const queryParams = new URLSearchParams({ + api_token: this.apiKey, + fmt: 'json', + from: format(from, DATE_FORMAT), + to: format(to, DATE_FORMAT) + }); + const response: { [date: string]: DataProviderHistoricalResponse; } = {}; const historicalResult = await fetch( - `${this.URL}/div/${symbol}?api_token=${ - this.apiKey - }&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format( - to, - DATE_FORMAT - )}`, + `${this.URL}/div/${symbol}?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -144,13 +146,16 @@ export class EodHistoricalDataService implements DataProviderInterface { symbol = this.convertToEodSymbol(symbol); try { + const queryParams = new URLSearchParams({ + api_token: this.apiKey, + fmt: 'json', + from: format(from, DATE_FORMAT), + period: granularity, + to: format(to, DATE_FORMAT) + }); + const response = await fetch( - `${this.URL}/eod/${symbol}?api_token=${ - this.apiKey - }&fmt=json&from=${format(from, DATE_FORMAT)}&to=${format( - to, - DATE_FORMAT - )}&period=${granularity}`, + `${this.URL}/eod/${symbol}?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -208,10 +213,14 @@ export class EodHistoricalDataService implements DataProviderInterface { }); try { + const queryParams = new URLSearchParams({ + api_token: this.apiKey, + fmt: 'json', + s: eodHistoricalDataSymbols.join(',') + }); + const realTimeResponse = await fetch( - `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${ - this.apiKey - }&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`, + `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -413,8 +422,12 @@ export class EodHistoricalDataService implements DataProviderInterface { })[] = []; try { + const queryParams = new URLSearchParams({ + api_token: this.apiKey + }); + const response = await fetch( - `${this.URL}/search/${query}?api_token=${this.apiKey}`, + `${this.URL}/search/${query}?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 6fe928d7a..27f462c90 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -44,6 +44,12 @@ import { @Injectable() export class FinancialModelingPrepService implements DataProviderInterface { + private static countriesMapping = { + 'Korea (the Republic of)': 'South Korea', + 'Russian Federation': 'Russia', + 'Taiwan (Province of China)': 'Taiwan' + }; + private apiKey: string; public constructor( @@ -79,8 +85,13 @@ export class FinancialModelingPrepService implements DataProviderInterface { symbol.length - DEFAULT_CURRENCY.length ); } else if (this.cryptocurrencyService.isCryptocurrency(symbol)) { + const queryParams = new URLSearchParams({ + symbol, + apikey: this.apiKey + }); + const [quote] = await fetch( - `${this.getUrl({ version: 'stable' })}/quote?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/quote?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -93,8 +104,13 @@ export class FinancialModelingPrepService implements DataProviderInterface { ); response.name = quote.name; } else { + const queryParams = new URLSearchParams({ + symbol, + apikey: this.apiKey + }); + const [assetProfile] = await fetch( - `${this.getUrl({ version: 'stable' })}/profile?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/profile?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -114,19 +130,31 @@ export class FinancialModelingPrepService implements DataProviderInterface { assetSubClass === AssetSubClass.ETF || assetSubClass === AssetSubClass.MUTUALFUND ) { + const queryParams = new URLSearchParams({ + symbol, + apikey: this.apiKey + }); + const etfCountryWeightings = await fetch( - `${this.getUrl({ version: 'stable' })}/etf/country-weightings?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/etf/country-weightings?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } ).then((res) => res.json()); - response.countries = etfCountryWeightings.map( - ({ country: countryName, weightPercentage }) => { + response.countries = etfCountryWeightings + .filter(({ country: countryName }) => { + return countryName.toLowerCase() !== 'other'; + }) + .map(({ country: countryName, weightPercentage }) => { let countryCode: string; for (const [code, country] of Object.entries(countries)) { - if (country.name === countryName) { + if ( + country.name === countryName || + country.name === + FinancialModelingPrepService.countriesMapping[countryName] + ) { countryCode = code; break; } @@ -136,11 +164,10 @@ export class FinancialModelingPrepService implements DataProviderInterface { code: countryCode, weight: parseFloat(weightPercentage.slice(0, -1)) / 100 }; - } - ); + }); const etfHoldings = await fetch( - `${this.getUrl({ version: 'stable' })}/etf/holdings?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/etf/holdings?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -159,7 +186,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { ); const [etfInformation] = await fetch( - `${this.getUrl({ version: 'stable' })}/etf/info?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/etf/info?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -170,7 +197,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } const etfSectorWeightings = await fetch( - `${this.getUrl({ version: 'stable' })}/etf/sector-weightings?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/etf/sector-weightings?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -242,12 +269,17 @@ export class FinancialModelingPrepService implements DataProviderInterface { } try { + const queryParams = new URLSearchParams({ + symbol, + apikey: this.apiKey + }); + const response: { [date: string]: DataProviderHistoricalResponse; } = {}; const dividends = await fetch( - `${this.getUrl({ version: 'stable' })}/dividends?symbol=${symbol}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/dividends?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -307,8 +339,15 @@ export class FinancialModelingPrepService implements DataProviderInterface { ? addYears(currentFrom, MAX_YEARS_PER_REQUEST) : to; + const queryParams = new URLSearchParams({ + symbol, + apikey: this.apiKey, + from: format(currentFrom, DATE_FORMAT), + to: format(currentTo, DATE_FORMAT) + }); + const historical = await fetch( - `${this.getUrl({ version: 'stable' })}/historical-price-eod/full?symbol=${symbol}&apikey=${this.apiKey}&from=${format(currentFrom, DATE_FORMAT)}&to=${format(currentTo, DATE_FORMAT)}`, + `${this.getUrl({ version: 'stable' })}/historical-price-eod/full?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -363,6 +402,11 @@ export class FinancialModelingPrepService implements DataProviderInterface { [symbol: string]: Pick; } = {}; + const queryParams = new URLSearchParams({ + symbols: symbols.join(','), + apikey: this.apiKey + }); + const [assetProfileResolutions, quotes] = await Promise.all([ this.prismaService.assetProfileResolution.findMany({ where: { @@ -371,7 +415,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } }), fetch( - `${this.getUrl({ version: 'stable' })}/batch-quote-short?symbols=${symbols.join(',')}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/batch-quote-short?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -463,12 +507,18 @@ export class FinancialModelingPrepService implements DataProviderInterface { const assetProfileBySymbolMap: { [symbol: string]: Partial; } = {}; + let items: LookupItem[] = []; try { if (isISIN(query?.toUpperCase())) { + const queryParams = new URLSearchParams({ + apikey: this.apiKey, + isin: query.toUpperCase() + }); + const result = await fetch( - `${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/search-isin?${queryParams.toString()}`, { signal: AbortSignal.timeout(requestTimeout) } @@ -494,8 +544,13 @@ export class FinancialModelingPrepService implements DataProviderInterface { }; }); } else { + const queryParams = new URLSearchParams({ + query, + apikey: this.apiKey + }); + const result = await fetch( - `${this.getUrl({ version: 'stable' })}/search-symbol?query=${query}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/search-symbol?${queryParams.toString()}`, { signal: AbortSignal.timeout( this.configurationService.get('REQUEST_TIMEOUT') diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index afbecc118..2b49e89c2 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -116,11 +116,14 @@ export class GhostfolioService implements DataProviderInterface { } = {}; try { + const queryParams = new URLSearchParams({ + granularity, + from: format(from, DATE_FORMAT), + to: format(to, DATE_FORMAT) + }); + const response = await fetch( - `${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format( - to, - DATE_FORMAT - )}`, + `${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?${queryParams.toString()}`, { headers: await this.getRequestHeaders(), signal: AbortSignal.timeout(requestTimeout) @@ -165,11 +168,14 @@ export class GhostfolioService implements DataProviderInterface { [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; }> { try { + const queryParams = new URLSearchParams({ + granularity, + from: format(from, DATE_FORMAT), + to: format(to, DATE_FORMAT) + }); + const response = await fetch( - `${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format( - to, - DATE_FORMAT - )}`, + `${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?${queryParams.toString()}`, { headers: await this.getRequestHeaders(), signal: AbortSignal.timeout(requestTimeout) @@ -235,8 +241,12 @@ export class GhostfolioService implements DataProviderInterface { } try { + const queryParams = new URLSearchParams({ + symbols: symbols.join(',') + }); + const response = await fetch( - `${this.URL}/v2/data-providers/ghostfolio/quotes?symbols=${symbols.join(',')}`, + `${this.URL}/v2/data-providers/ghostfolio/quotes?${queryParams.toString()}`, { headers: await this.getRequestHeaders(), signal: AbortSignal.timeout(requestTimeout) @@ -288,8 +298,12 @@ export class GhostfolioService implements DataProviderInterface { let searchResult: LookupResponse = { items: [] }; try { + const queryParams = new URLSearchParams({ + query + }); + const response = await fetch( - `${this.URL}/v2/data-providers/ghostfolio/lookup?query=${query}`, + `${this.URL}/v2/data-providers/ghostfolio/lookup?${queryParams.toString()}`, { headers: await this.getRequestHeaders(), signal: AbortSignal.timeout(requestTimeout) diff --git a/apps/api/src/services/demo/demo.service.ts b/apps/api/src/services/demo/demo.service.ts index 8f3658736..a24716d96 100644 --- a/apps/api/src/services/demo/demo.service.ts +++ b/apps/api/src/services/demo/demo.service.ts @@ -7,7 +7,7 @@ import { } from '@ghostfolio/common/config'; import { Injectable } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; @Injectable() export class DemoService { @@ -41,7 +41,7 @@ export class DemoService { accountId: demoAccountId, accountUserId: demoUserId, comment: null, - id: uuidv4(), + id: randomUUID(), userId: demoUserId }; }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 47c67c3de..8c1ba5b41 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -30,6 +30,7 @@ import ms from 'ms'; export class ExchangeRateDataService { private currencies: string[] = []; private currencyPairs: DataGatheringItem[] = []; + private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {}; public constructor( @@ -135,8 +136,14 @@ export class ExchangeRateDataService { public async initialize() { this.currencies = await this.prepareCurrencies(); this.currencyPairs = []; + this.derivedCurrencyFactors = {}; this.exchangeRates = {}; + for (const { currency, factor, rootCurrency } of DERIVED_CURRENCIES) { + this.derivedCurrencyFactors[`${currency}${rootCurrency}`] = 1 / factor; + this.derivedCurrencyFactors[`${rootCurrency}${currency}`] = factor; + } + for (const { currency1, currency2, @@ -266,10 +273,14 @@ export class ExchangeRateDataService { return this.toCurrency(aValue, aFromCurrency, aToCurrency); } + const derivedCurrencyFactor = + this.derivedCurrencyFactors[`${aFromCurrency}${aToCurrency}`]; let factor: number; if (aFromCurrency === aToCurrency) { factor = 1; + } else if (derivedCurrencyFactor) { + factor = derivedCurrencyFactor; } else { const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); @@ -357,111 +368,120 @@ export class ExchangeRateDataService { for (const date of dates) { factors[format(date, DATE_FORMAT)] = 1; } - } else { - const dataSource = - this.dataProviderService.getDataSourceForExchangeRates(); - const symbol = `${currencyFrom}${currencyTo}`; - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol - } - ], - dateQuery: { gte: startDate, lt: endDate } - }); + return factors; + } + + const derivedCurrencyFactor = + this.derivedCurrencyFactors[`${currencyFrom}${currencyTo}`]; + + if (derivedCurrencyFactor) { + for (const date of dates) { + factors[format(date, DATE_FORMAT)] = derivedCurrencyFactor; + } - if (marketData?.length > 0) { - for (const { date, marketPrice } of marketData) { - factors[format(date, DATE_FORMAT)] = marketPrice; + return factors; + } + + const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); + const symbol = `${currencyFrom}${currencyTo}`; + + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol } - } else { - // Calculate indirectly via base currency + ], + dateQuery: { gte: startDate, lt: endDate } + }); - const marketPriceBaseCurrencyFromCurrency: { - [dateString: string]: number; - } = {}; - const marketPriceBaseCurrencyToCurrency: { - [dateString: string]: number; - } = {}; + if (marketData?.length > 0) { + for (const { date, marketPrice } of marketData) { + factors[format(date, DATE_FORMAT)] = marketPrice; + } + } else { + // Calculate indirectly via base currency + + const marketPriceBaseCurrencyFromCurrency: { + [dateString: string]: number; + } = {}; + const marketPriceBaseCurrencyToCurrency: { + [dateString: string]: number; + } = {}; + + try { + if (currencyFrom === DEFAULT_CURRENCY) { + for (const date of dates) { + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = 1; + } + } else { + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol: `${DEFAULT_CURRENCY}${currencyFrom}` + } + ], + dateQuery: { gte: startDate, lt: endDate } + }); - try { - if (currencyFrom === DEFAULT_CURRENCY) { - for (const date of dates) { - marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = - 1; - } - } else { - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol: `${DEFAULT_CURRENCY}${currencyFrom}` - } - ], - dateQuery: { gte: startDate, lt: endDate } - }); - - for (const { date, marketPrice } of marketData) { - marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = - marketPrice; - } + for (const { date, marketPrice } of marketData) { + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = + marketPrice; } - } catch {} + } + } catch {} - try { - if (currencyTo === DEFAULT_CURRENCY) { - for (const date of dates) { - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; - } - } else { - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol: `${DEFAULT_CURRENCY}${currencyTo}` - } - ], - dateQuery: { - gte: startDate, - lt: endDate + try { + if (currencyTo === DEFAULT_CURRENCY) { + for (const date of dates) { + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; + } + } else { + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol: `${DEFAULT_CURRENCY}${currencyTo}` } - }); - - for (const { date, marketPrice } of marketData) { - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = - marketPrice; + ], + dateQuery: { + gte: startDate, + lt: endDate } + }); + + for (const { date, marketPrice } of marketData) { + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = + marketPrice; } - } catch {} + } + } catch {} - for (const date of dates) { - try { - const factor = - (1 / - marketPriceBaseCurrencyFromCurrency[ - format(date, DATE_FORMAT) - ]) * - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; - - if (isNaN(factor)) { - throw new Error('Exchange rate is not a number'); - } else { - factors[format(date, DATE_FORMAT)] = factor; - } - } catch { - let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( - date, - DATE_FORMAT - )}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`; - - if (DEFAULT_CURRENCY !== currencyTo) { - errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; - } + for (const date of dates) { + try { + const factor = + (1 / + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)]) * + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; - Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); + if (isNaN(factor)) { + throw new Error('Exchange rate is not a number'); + } else { + factors[format(date, DATE_FORMAT)] = factor; } + } catch { + let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( + date, + DATE_FORMAT + )}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`; + + if (DEFAULT_CURRENCY !== currencyTo) { + errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; + } + + Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); } } } diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index c433f692f..cec63c3eb 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -28,8 +28,9 @@ import { InjectQueue } from '@nestjs/bull'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { JobOptions, Queue } from 'bull'; -import { format, min, subDays, subYears } from 'date-fns'; +import { format, min, subDays, subMilliseconds, subYears } from 'date-fns'; import { isEmpty } from 'lodash'; +import ms, { StringValue } from 'ms'; @Injectable() export class DataGatheringService { @@ -160,8 +161,7 @@ export class DataGatheringService { ); if (!assetProfileIdentifiers) { - assetProfileIdentifiers = - await this.getAllActiveAssetProfileIdentifiers(); + assetProfileIdentifiers = await this.getActiveAssetProfileIdentifiers(); } if (assetProfileIdentifiers.length <= 0) { @@ -301,29 +301,36 @@ export class DataGatheringService { ); } - public async getAllActiveAssetProfileIdentifiers(): Promise< - AssetProfileIdentifier[] - > { - const symbolProfiles = await this.prismaService.symbolProfile.findMany({ - orderBy: [{ symbol: 'asc' }], + /** + * Returns active asset profile identifiers + * + * @param {StringValue} maxAge - Optional. Specifies the maximum allowed age + * of a profile’s last update timestamp. Only asset profiles considered stale + * are returned. + */ + public async getActiveAssetProfileIdentifiers({ + maxAge + }: { + maxAge?: StringValue; + } = {}): Promise { + return this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }, { dataSource: 'asc' }], + select: { + dataSource: true, + symbol: true + }, where: { - isActive: true + dataSource: { + notIn: ['MANUAL', 'RAPID_API'] + }, + isActive: true, + ...(maxAge && { + updatedAt: { + lt: subMilliseconds(new Date(), ms(maxAge)) + } + }) } }); - - return symbolProfiles - .filter(({ dataSource }) => { - return ( - dataSource !== DataSource.MANUAL && - dataSource !== DataSource.RAPID_API - ); - }) - .map(({ dataSource, symbol }) => { - return { - dataSource, - symbol - }; - }); } private async getAssetProfileIdentifiersWithCompleteMarketData(): Promise< diff --git a/apps/client-e2e/.eslintrc.json b/apps/client-e2e/.eslintrc.json deleted file mode 100644 index dbedf6bd4..000000000 --- a/apps/client-e2e/.eslintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "parserOptions": { - "project": ["apps/client-e2e/tsconfig.*?.json"] - }, - "rules": {} - }, - { - "files": ["src/plugins/index.js"], - "rules": { - "@typescript-eslint/no-var-requires": "off", - "no-undef": "off" - } - } - ] -} diff --git a/apps/client-e2e/cypress.json b/apps/client-e2e/cypress.json deleted file mode 100644 index a8219f0fe..000000000 --- a/apps/client-e2e/cypress.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "fileServerFolder": ".", - "fixturesFolder": "./src/fixtures", - "integrationFolder": "./src/integration", - "modifyObstructiveCode": false, - "pluginsFile": "./src/plugins/index", - "supportFile": "./src/support/index.ts", - "video": true, - "videosFolder": "../../dist/cypress/apps/client-e2e/videos", - "screenshotsFolder": "../../dist/cypress/apps/client-e2e/screenshots", - "chromeWebSecurity": false -} diff --git a/apps/client-e2e/project.json b/apps/client-e2e/project.json deleted file mode 100644 index 92e2f09ef..000000000 --- a/apps/client-e2e/project.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "client-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/client-e2e/src", - "projectType": "application", - "tags": [], - "implicitDependencies": ["client"], - "targets": { - "e2e": { - "executor": "@nx/cypress:cypress", - "options": { - "cypressConfig": "apps/client-e2e/cypress.json", - "devServerTarget": "client:serve" - }, - "configurations": { - "production": { - "devServerTarget": "client:serve:production" - } - } - } - } -} diff --git a/apps/client-e2e/src/fixtures/example.json b/apps/client-e2e/src/fixtures/example.json deleted file mode 100644 index 294cbed6c..000000000 --- a/apps/client-e2e/src/fixtures/example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io" -} diff --git a/apps/client-e2e/src/integration/app.spec.ts b/apps/client-e2e/src/integration/app.spec.ts deleted file mode 100644 index b194092d7..000000000 --- a/apps/client-e2e/src/integration/app.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getGreeting } from '../support/app.po'; - -describe('client', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - // Custom command example, see `../support/commands.ts` file - cy.login('my-email@something.com', 'myPassword'); - - // Function helper example, see `../support/app.po.ts` file - getGreeting().contains('Welcome to client!'); - }); -}); diff --git a/apps/client-e2e/src/plugins/index.js b/apps/client-e2e/src/plugins/index.js deleted file mode 100644 index 63aa33cbe..000000000 --- a/apps/client-e2e/src/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -const { preprocessTypescript } = require('@nx/cypress/plugins/preprocessor'); - -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - - // Preprocess Typescript file using Nx helper - on('file:preprocessor', preprocessTypescript(config)); -}; diff --git a/apps/client-e2e/src/support/app.po.ts b/apps/client-e2e/src/support/app.po.ts deleted file mode 100644 index 329342469..000000000 --- a/apps/client-e2e/src/support/app.po.ts +++ /dev/null @@ -1 +0,0 @@ -export const getGreeting = () => cy.get('h1'); diff --git a/apps/client-e2e/src/support/commands.ts b/apps/client-e2e/src/support/commands.ts deleted file mode 100644 index 36c834059..000000000 --- a/apps/client-e2e/src/support/commands.ts +++ /dev/null @@ -1,31 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** - -declare namespace Cypress { - interface Chainable { - login(email: string, password: string): void; - } -} -// -// -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); -}); -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/apps/client-e2e/src/support/index.ts b/apps/client-e2e/src/support/index.ts deleted file mode 100644 index fad130159..000000000 --- a/apps/client-e2e/src/support/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** -// Import commands.js using ES2015 syntax: -import './commands'; diff --git a/apps/client-e2e/tsconfig.e2e.json b/apps/client-e2e/tsconfig.e2e.json deleted file mode 100644 index 9dc3660a7..000000000 --- a/apps/client-e2e/tsconfig.e2e.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "sourceMap": false, - "outDir": "../../dist/out-tsc", - "allowJs": true, - "types": ["cypress", "node"] - }, - "include": ["src/**/*.ts", "src/**/*.js"] -} diff --git a/apps/client-e2e/tsconfig.json b/apps/client-e2e/tsconfig.json deleted file mode 100644 index 08841a7f5..000000000 --- a/apps/client-e2e/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.e2e.json" - } - ] -} diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index abeda6de8..cb41904d3 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -73,7 +73,7 @@ } diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index 14f1b211b..a82294001 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -205,14 +205,16 @@
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html index 8b1b510d7..86377c937 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.html +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -64,7 +64,7 @@
diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index e802e3272..eb63f8aa6 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -222,7 +222,9 @@ > - View Details + View Details... @if (hasPermissionToImpersonateAllUsers) { diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html index 551f9b943..60f6a2585 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -10,6 +10,12 @@
User ID
+
+ Role +
+ + +
Registration Date
-
- -
Authentication
-
- Role -
@if (data.hasPermissionForSubscription) { diff --git a/apps/ui-e2e/cypress.json b/apps/ui-e2e/cypress.json deleted file mode 100644 index 71520788e..000000000 --- a/apps/ui-e2e/cypress.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "fileServerFolder": ".", - "fixturesFolder": "./src/fixtures", - "integrationFolder": "./src/integration", - "modifyObstructiveCode": false, - "supportFile": "./src/support/index.ts", - "pluginsFile": "./src/plugins/index", - "video": true, - "videosFolder": "../../dist/cypress/apps/ui-e2e/videos", - "screenshotsFolder": "../../dist/cypress/apps/ui-e2e/screenshots", - "chromeWebSecurity": false, - "baseUrl": "http://localhost:4400" -} diff --git a/apps/ui-e2e/eslint.config.cjs b/apps/ui-e2e/eslint.config.cjs deleted file mode 100644 index 5e6707635..000000000 --- a/apps/ui-e2e/eslint.config.cjs +++ /dev/null @@ -1,33 +0,0 @@ -const { FlatCompat } = require('@eslint/eslintrc'); -const js = require('@eslint/js'); -const baseConfig = require('../../eslint.config.cjs'); - -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended -}); - -module.exports = [ - { - ignores: ['**/dist'] - }, - ...baseConfig, - ...compat.extends('plugin:cypress/recommended'), - { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, - languageOptions: { - parserOptions: { - project: ['apps/ui-e2e/tsconfig.json'] - } - } - }, - { - files: ['src/plugins/index.js'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - 'no-undef': 'off' - } - } -]; diff --git a/apps/ui-e2e/project.json b/apps/ui-e2e/project.json deleted file mode 100644 index a5b4cf53a..000000000 --- a/apps/ui-e2e/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "ui-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/ui-e2e/src", - "projectType": "application", - "tags": [], - "implicitDependencies": ["ui"], - "targets": { - "e2e": { - "executor": "@nx/cypress:cypress", - "options": { - "cypressConfig": "apps/ui-e2e/cypress.json", - "devServerTarget": "ui:storybook" - }, - "configurations": { - "ci": { - "devServerTarget": "ui:storybook:ci" - } - } - }, - "lint": { - "executor": "@nx/eslint:lint", - "options": { - "lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"] - } - } - } -} diff --git a/apps/ui-e2e/src/fixtures/example.json b/apps/ui-e2e/src/fixtures/example.json deleted file mode 100644 index 294cbed6c..000000000 --- a/apps/ui-e2e/src/fixtures/example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io" -} diff --git a/apps/ui-e2e/src/integration/value/value.component.spec.ts b/apps/ui-e2e/src/integration/value/value.component.spec.ts deleted file mode 100644 index 5b90784e7..000000000 --- a/apps/ui-e2e/src/integration/value/value.component.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -describe('ui', () => { - beforeEach(() => cy.visit('/iframe.html?id=valuecomponent--loading')); - it('should render the component', () => { - cy.get('gf-value').should('exist'); - }); -}); diff --git a/apps/ui-e2e/src/plugins/index.js b/apps/ui-e2e/src/plugins/index.js deleted file mode 100644 index 63aa33cbe..000000000 --- a/apps/ui-e2e/src/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -const { preprocessTypescript } = require('@nx/cypress/plugins/preprocessor'); - -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - - // Preprocess Typescript file using Nx helper - on('file:preprocessor', preprocessTypescript(config)); -}; diff --git a/apps/ui-e2e/src/support/commands.ts b/apps/ui-e2e/src/support/commands.ts deleted file mode 100644 index 310f1fa0e..000000000 --- a/apps/ui-e2e/src/support/commands.ts +++ /dev/null @@ -1,33 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** - -// eslint-disable-next-line @typescript-eslint/no-namespace -declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - login(email: string, password: string): void; - } -} -// -// -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); -}); -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/apps/ui-e2e/src/support/index.ts b/apps/ui-e2e/src/support/index.ts deleted file mode 100644 index fad130159..000000000 --- a/apps/ui-e2e/src/support/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** -// Import commands.js using ES2015 syntax: -import './commands'; diff --git a/apps/ui-e2e/tsconfig.json b/apps/ui-e2e/tsconfig.json deleted file mode 100644 index c4f818ecd..000000000 --- a/apps/ui-e2e/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "sourceMap": false, - "outDir": "../../dist/out-tsc", - "allowJs": true, - "types": ["cypress", "node"] - }, - "include": ["src/**/*.ts", "src/**/*.js"] -} diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index c5ebaa657..d127b4bf3 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -7,7 +7,7 @@ (click)="onTransferBalance()" > - Transfer Cash Balance... + Transfer Cash Balance... } @@ -304,13 +304,13 @@
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index e9bebaa16..b8e1882d4 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -6,7 +6,7 @@ (click)="onImport()" > - Import Activities... + Import Activities... @if (hasPermissionToExportActivities) { @if (hasPermissionToExportActivities) { @@ -379,7 +379,9 @@ > - Import Activities... + Import Activities... } @@ -391,7 +393,9 @@ > - Import Dividends... + Import Dividends... } @@ -443,20 +447,20 @@ }