Browse Source

Merge branch 'main' into task/move-support-for-tags-in-account-to-general-availability

pull/7415/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
1664b8bdb7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      CHANGELOG.md
  2. 4
      apps/api/src/app/access/access.controller.ts
  3. 2
      apps/api/src/app/account-balance/account-balance.service.ts
  4. 21
      apps/api/src/app/account/account.service.ts
  5. 2
      apps/api/src/app/activities/activities.service.ts
  6. 6
      apps/api/src/app/endpoints/public/public.controller.ts
  7. 4
      apps/api/src/app/import/import.controller.ts
  8. 90
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  9. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  10. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  11. 27
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  12. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  13. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  14. 10
      apps/api/src/app/portfolio/portfolio.controller.ts
  15. 11
      apps/api/src/app/portfolio/portfolio.service.ts
  16. 2
      apps/api/src/app/subscription/subscription.service.ts
  17. 17
      apps/api/src/services/benchmark/benchmark.service.ts
  18. 10
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  19. 8
      apps/api/src/services/data-provider/data-provider.service.ts
  20. 2
      apps/client/src/app/components/home-overview/home-overview.component.ts
  21. 2
      apps/client/src/app/components/home-overview/home-overview.html
  22. 54
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  23. 2
      libs/common/src/lib/interfaces/access.interface.ts
  24. 2
      libs/common/src/lib/interfaces/historical-data-item.interface.ts
  25. 4
      libs/common/src/lib/models/portfolio-snapshot.ts
  26. 2
      libs/common/src/lib/models/timeline-position.ts
  27. 2
      libs/common/src/lib/types/access-with-grantee-user.type.ts
  28. 3
      libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html
  29. 4
      libs/ui/src/lib/no-transactions-info/no-transactions-info.component.ts
  30. 102
      package-lock.json
  31. 8
      package.json

11
CHANGELOG.md

@ -9,7 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Included cash in the performance calculation of the portfolio
- Moved the support for tags in the account from experimental to general availability - Moved the support for tags in the account from experimental to general availability
- Upgraded `countup.js` from version `2.10.0` to `2.10.1`
- Upgraded `dotenv` from version `17.2.3` to `17.4.2`
- Upgraded `dotenv-expand` from version `12.0.3` to `13.0.0`
- Upgraded `fuse.js` from version `7.3.0` to `7.5.0`
### Fixed
- Fixed the _Add activity_ link of the onboarding on the overview tab of the home page to open the create activity dialog
- Fixed the link of the no transactions info component to open the create activity dialog
- Resolved an exception in the `POST api/v1/activities` endpoint when creating an activity with the update account balance option but without an account
## 3.33.0 - 2026-07-25 ## 3.33.0 - 2026-07-25

4
apps/api/src/app/access/access.controller.ts

@ -78,7 +78,7 @@ export class AccessController {
): Promise<AccessModel> { ): Promise<AccessModel> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
@ -134,7 +134,7 @@ export class AccessController {
): Promise<AccessModel> { ): Promise<AccessModel> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),

2
apps/api/src/app/account-balance/account-balance.service.ts

@ -178,7 +178,7 @@ export class AccountBalanceService {
accountId: balance.account.id, accountId: balance.account.id,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
balance.value, balance.value,
balance.account.currency, balance.account.currency ?? userCurrency,
userCurrency userCurrency
) )
}; };

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

@ -37,11 +37,26 @@ export class AccountService {
public async account({ public async account({
id_userId id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> { }: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const [account] = await this.accounts({ const account = await this.prismaService.account.findUnique({
where: id_userId include: {
balances: {
orderBy: { date: 'desc' },
take: 1
}
},
where: { id_userId }
}); });
return account; if (!account) {
return null;
}
const { balances, ...accountData } = account;
return {
...accountData,
balance: balances[0]?.value ?? 0
};
} }
public async accountWithActivities( public async accountWithActivities(

2
apps/api/src/app/activities/activities.service.ts

@ -275,7 +275,7 @@ export class ActivitiesService {
include: { SymbolProfile: true } include: { SymbolProfile: true }
}); });
if (updateAccountBalance === true) { if (accountId && updateAccountBalance === true) {
let amount = new Big(data.unitPrice).mul(data.quantity); let amount = new Big(data.unitPrice).mul(data.quantity);
if (['BUY', 'FEE'].includes(data.type)) { if (['BUY', 'FEE'].includes(data.type)) {

6
apps/api/src/app/endpoints/public/public.controller.ts

@ -65,7 +65,7 @@ export class PublicController {
}); });
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = user.subscription.type === SubscriptionType.Premium; hasDetails = user?.subscription?.type === SubscriptionType.Premium;
} }
const { filters } = (access.settings ?? {}) as AccessSettings; const { filters } = (access.settings ?? {}) as AccessSettings;
@ -98,7 +98,7 @@ export class PublicController {
sortDirection: 'desc', sortDirection: 'desc',
take: 10, take: 10,
types: [ActivityType.BUY, ActivityType.SELL], types: [ActivityType.BUY, ActivityType.SELL],
userCurrency: user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY, userCurrency: user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY,
userId: user.id, userId: user.id,
withExcludedAccountsAndActivities: false withExcludedAccountsAndActivities: false
}); });
@ -167,7 +167,7 @@ export class PublicController {
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
quantity * marketPrice, quantity * marketPrice,
assetProfile.currency, assetProfile.currency,
user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
) )
); );
}) })

4
apps/api/src/app/import/import.controller.ts

@ -65,7 +65,7 @@ export class ImportController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Premium this.request.user.subscription?.type === SubscriptionType.Premium
) { ) {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER; maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
} }
@ -109,7 +109,7 @@ export class ImportController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Premium this.request.user.subscription?.type === SubscriptionType.Premium
) { ) {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER; maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
} }

90
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -196,6 +196,7 @@ export abstract class PortfolioCalculator {
hasErrors: false, hasErrors: false,
historicalData: [], historicalData: [],
positions: [], positions: [],
totalCashInBaseCurrency: new Big(0),
totalFeesWithCurrencyEffect: new Big(0), totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0), totalInvestment: new Big(0),
@ -204,10 +205,12 @@ export abstract class PortfolioCalculator {
}; };
} }
const cashSymbols = new Set<string>();
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: DataGatheringItem[] = []; const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let totalCashInBaseCurrency = new Big(0);
let totalInterestWithCurrencyEffect = new Big(0); let totalInterestWithCurrencyEffect = new Big(0);
let totalLiabilitiesWithCurrencyEffect = new Big(0); let totalLiabilitiesWithCurrencyEffect = new Big(0);
@ -316,7 +319,7 @@ export abstract class PortfolioCalculator {
const accumulatedValuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: {
investmentValueWithCurrencyEffect: Big; investmentValueWithCurrencyEffect: Big;
totalAccountBalanceWithCurrencyEffect: Big; totalCashValueWithCurrencyEffect: Big;
totalCurrentValue: Big; totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big; totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big; totalInvestmentValue: Big;
@ -351,6 +354,8 @@ export abstract class PortfolioCalculator {
] ?? 1 ] ?? 1
); );
const valueInBaseCurrency = marketPriceInBaseCurrency.mul(item.quantity);
const { const {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
@ -391,25 +396,19 @@ export abstract class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
const includeInTotalAssetValue = valuesBySymbol[item.symbol] = {
item.assetSubClass !== AssetSubClass.CASH; currentValues,
currentValuesWithCurrencyEffect,
if (includeInTotalAssetValue) { investmentValuesAccumulated,
valuesBySymbol[item.symbol] = { investmentValuesAccumulatedWithCurrencyEffect,
currentValues, investmentValuesWithCurrencyEffect,
currentValuesWithCurrencyEffect, netPerformanceValues,
investmentValuesAccumulated, netPerformanceValuesWithCurrencyEffect,
investmentValuesAccumulatedWithCurrencyEffect, timeWeightedInvestmentValues,
investmentValuesWithCurrencyEffect, timeWeightedInvestmentValuesWithCurrencyEffect
netPerformanceValues, };
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
};
}
positions.push({ positions.push({
includeInTotalAssetValue,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
activitiesCount: item.activitiesCount, activitiesCount: item.activitiesCount,
@ -450,11 +449,16 @@ export abstract class PortfolioCalculator {
quantity: item.quantity, quantity: item.quantity,
symbol: item.symbol, symbol: item.symbol,
tags: item.tags, tags: item.tags,
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( valueInBaseCurrency
item.quantity
)
}); });
if (item.assetSubClass === AssetSubClass.CASH) {
cashSymbols.add(item.symbol);
totalCashInBaseCurrency =
totalCashInBaseCurrency.plus(valueInBaseCurrency);
}
totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus(
totalInterestInBaseCurrency totalInterestInBaseCurrency
); );
@ -474,28 +478,7 @@ export abstract class PortfolioCalculator {
} }
} }
const accountBalanceItemsMap = this.accountBalanceItems.reduce(
(map, { date, value }) => {
map[date] = new Big(value);
return map;
},
{} as { [date: string]: Big }
);
const accountBalanceMap: { [date: string]: Big } = {};
let lastKnownBalance = new Big(0);
for (const dateString of chartDates) { for (const dateString of chartDates) {
if (accountBalanceItemsMap[dateString] !== undefined) {
// If there's an exact balance for this date, update lastKnownBalance
lastKnownBalance = accountBalanceItemsMap[dateString];
}
// Add the most recent balance to the accountBalanceMap
accountBalanceMap[dateString] = lastKnownBalance;
for (const symbol of Object.keys(valuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol]; const symbolValues = valuesBySymbol[symbol];
@ -538,7 +521,14 @@ export abstract class PortfolioCalculator {
accumulatedValuesByDate[dateString] accumulatedValuesByDate[dateString]
?.investmentValueWithCurrencyEffect ?? new Big(0) ?.investmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect), ).add(investmentValueWithCurrencyEffect),
totalAccountBalanceWithCurrencyEffect: accountBalanceMap[dateString], totalCashValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalCashValueWithCurrencyEffect ?? new Big(0)
).add(
cashSymbols.has(symbol)
? currentValueWithCurrencyEffect
: new Big(0)
),
totalCurrentValue: ( totalCurrentValue: (
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue), ).add(currentValue),
@ -579,7 +569,7 @@ export abstract class PortfolioCalculator {
).map(([date, values]) => { ).map(([date, values]) => {
const { const {
investmentValueWithCurrencyEffect, investmentValueWithCurrencyEffect,
totalAccountBalanceWithCurrencyEffect, totalCashValueWithCurrencyEffect,
totalCurrentValue, totalCurrentValue,
totalCurrentValueWithCurrencyEffect, totalCurrentValueWithCurrencyEffect,
totalInvestmentValue, totalInvestmentValue,
@ -612,10 +602,8 @@ export abstract class PortfolioCalculator {
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect:
totalNetPerformanceValueWithCurrencyEffect.toNumber(), totalNetPerformanceValueWithCurrencyEffect.toNumber(),
netWorth: totalCurrentValueWithCurrencyEffect netWorth: totalCurrentValueWithCurrencyEffect.toNumber(),
.plus(totalAccountBalanceWithCurrencyEffect) totalCashInBaseCurrency: totalCashValueWithCurrencyEffect.toNumber(),
.toNumber(),
totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(),
totalInvestment: totalInvestmentValue.toNumber(), totalInvestment: totalInvestmentValue.toNumber(),
totalInvestmentValueWithCurrencyEffect: totalInvestmentValueWithCurrencyEffect:
totalInvestmentValueWithCurrencyEffect.toNumber(), totalInvestmentValueWithCurrencyEffect.toNumber(),
@ -639,6 +627,7 @@ export abstract class PortfolioCalculator {
...overall, ...overall,
errors, errors,
historicalData, historicalData,
totalCashInBaseCurrency,
totalInterestWithCurrencyEffect, totalInterestWithCurrencyEffect,
totalLiabilitiesWithCurrencyEffect, totalLiabilitiesWithCurrencyEffect,
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors,
@ -776,11 +765,6 @@ export abstract class PortfolioCalculator {
? 0 ? 0
: netPerformanceWithCurrencyEffectSinceStartDate / : netPerformanceWithCurrencyEffectSinceStartDate /
timeWeightedInvestmentValue timeWeightedInvestmentValue
// TODO: Add net worth
// netWorth: totalCurrentValueWithCurrencyEffect
// .plus(totalAccountBalanceWithCurrencyEffect)
// .toNumber()
// netWorth: 0
}); });
} }
} }

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -145,7 +145,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
@ -163,7 +163,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceWithCurrencyEffect: 5535.42, netPerformanceWithCurrencyEffect: 5535.42,
netWorth: 50098.3, // 1 * 50098.3 = 50098.3 netWorth: 50098.3, // 1 * 50098.3 = 50098.3
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 50098.3, // 1 * 50098.3 = 50098.3 value: 50098.3, // 1 * 50098.3 = 50098.3
@ -182,7 +182,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712,
netPerformanceWithCurrencyEffect: -1463.18, netPerformanceWithCurrencyEffect: -1463.18,
netWorth: 43099.7, netWorth: 43099.7,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 43099.7, value: 43099.7,

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -145,7 +145,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
@ -163,7 +163,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412 netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
netPerformanceWithCurrencyEffect: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42 netPerformanceWithCurrencyEffect: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
netWorth: 50098.3, // 1 * 50098.3 = 50098.3 netWorth: 50098.3, // 1 * 50098.3 = 50098.3
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 50098.3, // 1 * 50098.3 = 50098.3 value: 50098.3, // 1 * 50098.3 = 50098.3
@ -182,7 +182,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712,
netPerformanceWithCurrencyEffect: -1463.18, netPerformanceWithCurrencyEffect: -1463.18,
netWorth: 43099.7, netWorth: 43099.7,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 44558.42, totalInvestment: 44558.42,
totalInvestmentValueWithCurrencyEffect: 44558.42, totalInvestmentValueWithCurrencyEffect: 44558.42,
value: 43099.7, value: 43099.7,

27
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -248,7 +248,6 @@ describe('PortfolioCalculator', () => {
'0.08211603004634809014' '0.08211603004634809014'
), ),
grossPerformanceWithCurrencyEffect: new Big(70), grossPerformanceWithCurrencyEffect: new Big(70),
includeInTotalAssetValue: false,
investment: new Big(1820), investment: new Big(1820),
investmentWithCurrencyEffect: new Big(1750), investmentWithCurrencyEffect: new Big(1750),
marketPrice: 1, marketPrice: 1,
@ -283,11 +282,37 @@ describe('PortfolioCalculator', () => {
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big(1820),
hasErrors: false, hasErrors: false,
totalCashInBaseCurrency: new Big(1820),
totalFeesWithCurrencyEffect: new Big(0), totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0),
totalInvestment: new Big(1820),
totalLiabilitiesWithCurrencyEffect: new Big(0) totalLiabilitiesWithCurrencyEffect: new Big(0)
}); });
/**
* Value with currency effect: 2000 USD * 0.91 = 1820 CHF
* Net worth: 1820 CHF (the cash is included in the value and therefore
* not added on top of it again)
* Cash in base currency: 2000 USD * 0.91 = 1820 CHF (the whole portfolio
* consists of cash, hence it matches the value)
* Net performance with currency effect: 70 CHF / 852.45 CHF 8.21 %
*/
expect(portfolioSnapshot.historicalData.at(-1)).toEqual({
date: '2025-01-01',
investmentValueWithCurrencyEffect: 0,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0.08211603004634808,
netPerformanceWithCurrencyEffect: 70,
netWorth: 1820,
totalCashInBaseCurrency: 1820,
totalInvestment: 1820,
totalInvestmentValueWithCurrencyEffect: 1750,
value: 1820,
valueWithCurrencyEffect: 1820
});
}); });
}); });
}); });

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -142,7 +142,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
@ -161,7 +161,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceWithCurrencyEffect: 24, netPerformanceWithCurrencyEffect: 24,
netWorth: 175.6, // 2 * 87.8 = 175.6 netWorth: 175.6, // 2 * 87.8 = 175.6
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 151.6, totalInvestment: 151.6,
totalInvestmentValueWithCurrencyEffect: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6,
value: 175.6, // 2 * 87.8 = 175.6 value: 175.6, // 2 * 87.8 = 175.6
@ -180,7 +180,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744,
netPerformanceWithCurrencyEffect: 19.86, netPerformanceWithCurrencyEffect: 19.86,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalCashInBaseCurrency: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,

7
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -40,11 +40,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions.filter( for (const currentPosition of positions) {
({ includeInTotalAssetValue }) => {
return includeInTotalAssetValue;
}
)) {
if (currentPosition.feeInBaseCurrency) { if (currentPosition.feeInBaseCurrency) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency currentPosition.feeInBaseCurrency
@ -117,6 +113,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
createdAt: new Date(), createdAt: new Date(),
errors: [], errors: [],
historicalData: [], historicalData: [],
totalCashInBaseCurrency: new Big(0),
totalLiabilitiesWithCurrencyEffect: new Big(0) totalLiabilitiesWithCurrencyEffect: new Big(0)
}; };
} }

10
apps/api/src/app/portfolio/portfolio.controller.ts

@ -97,7 +97,7 @@ export class PortfolioController {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = hasDetails =
this.request.user.subscription.type === SubscriptionType.Premium; this.request.user.subscription?.type === SubscriptionType.Premium;
} }
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
@ -383,7 +383,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
dividends = dividends.map((item) => { dividends = dividends.map((item) => {
return nullifyValuesInObject(item, ['investment']); return nullifyValuesInObject(item, ['investment']);
@ -511,7 +511,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
investments = investments.map((item) => { investments = investments.map((item) => {
return nullifyValuesInObject(item, ['investment']); return nullifyValuesInObject(item, ['investment']);
@ -623,7 +623,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
performanceInformation.chart = performanceInformation.chart.map( performanceInformation.chart = performanceInformation.chart.map(
(item) => { (item) => {
@ -651,7 +651,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Basic this.request.user.subscription?.type === SubscriptionType.Basic
) { ) {
for (const category of report.xRay.categories) { for (const category of report.xRay.categories) {
category.rules = null; category.rules = null;

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

@ -541,12 +541,6 @@ export class PortfolioService {
let filteredValueInBaseCurrency = currentValueInBaseCurrency; let filteredValueInBaseCurrency = currentValueInBaseCurrency;
if (!this.activitiesService.areCashActivitiesExcludedByFilters(filters)) {
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
}
const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => { const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return { return {
dataSource, dataSource,
@ -1906,6 +1900,7 @@ export class PortfolioService {
const { const {
currentValueInBaseCurrency, currentValueInBaseCurrency,
totalCashInBaseCurrency,
totalInvestment, totalInvestment,
totalInvestmentWithCurrencyEffect totalInvestmentWithCurrencyEffect
} = await portfolioCalculator.getSnapshot(); } = await portfolioCalculator.getSnapshot();
@ -1982,8 +1977,7 @@ export class PortfolioService {
.plus(totalOfExcludedActivities) .plus(totalOfExcludedActivities)
.toNumber(); .toNumber();
const netWorth = new Big(balanceInBaseCurrency) const netWorth = new Big(currentValueInBaseCurrency)
.plus(currentValueInBaseCurrency)
.plus(excludedAccountsAndActivities) .plus(excludedAccountsAndActivities)
.minus(liabilities) .minus(liabilities)
.toNumber(); .toNumber();
@ -2035,6 +2029,7 @@ export class PortfolioService {
fireWealth: { fireWealth: {
today: { today: {
valueInBaseCurrency: new Big(currentValueInBaseCurrency) valueInBaseCurrency: new Big(currentValueInBaseCurrency)
.minus(totalCashInBaseCurrency ?? 0)
.minus(emergencyFundHoldingsValueInBaseCurrency) .minus(emergencyFundHoldingsValueInBaseCurrency)
.toNumber() .toNumber()
} }

2
apps/api/src/app/subscription/subscription.service.ts

@ -149,7 +149,7 @@ export class SubscriptionService {
} }
const subscriptionOffer: SubscriptionOffer = JSON.parse( const subscriptionOffer: SubscriptionOffer = JSON.parse(
session.metadata.subscriptionOffer ?? '{}' session.metadata?.subscriptionOffer ?? '{}'
); );
const durationExtension = subscriptionOffer?.durationExtension; const durationExtension = subscriptionOffer?.durationExtension;

17
apps/api/src/services/benchmark/benchmark.service.ts

@ -18,7 +18,6 @@ import {
BenchmarkProperty, BenchmarkProperty,
BenchmarkResponse BenchmarkResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
@ -146,7 +145,7 @@ export class BenchmarkService {
public async addBenchmark({ public async addBenchmark({
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier): Promise<Partial<SymbolProfile>> { }: AssetProfileIdentifier): Promise<Partial<SymbolProfile> | undefined> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({ const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: { where: {
dataSource, dataSource,
@ -183,7 +182,7 @@ export class BenchmarkService {
public async deleteBenchmark({ public async deleteBenchmark({
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier): Promise<Partial<SymbolProfile>> { }: AssetProfileIdentifier): Promise<Partial<SymbolProfile> | null> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({ const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: { where: {
dataSource, dataSource,
@ -240,12 +239,12 @@ export class BenchmarkService {
enableSharing enableSharing
}); });
const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] = const promisesAllTimeHighs: ReturnType<
[]; typeof this.marketDataService.getMax
const promisesBenchmarkTrends: Promise<{ >[] = [];
trend50d: BenchmarkTrend; const promisesBenchmarkTrends: ReturnType<
trend200d: BenchmarkTrend; typeof this.getBenchmarkTrends
}>[] = []; >[] = [];
const quotes = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {

10
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -200,13 +200,13 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
response.assetClass = assetClass; response.assetClass = assetClass;
response.assetSubClass = assetSubClass; response.assetSubClass = assetSubClass;
response.currency = assetProfile.price.currency; response.currency = assetProfile.price?.currency;
response.dataSource = this.getName(); response.dataSource = this.getName();
response.name = this.formatName({ response.name = this.formatName({
longName: assetProfile.price.longName, longName: assetProfile.price?.longName,
quoteType: assetProfile.price.quoteType, quoteType: assetProfile.price?.quoteType,
shortName: assetProfile.price.shortName, shortName: assetProfile.price?.shortName,
symbol: assetProfile.price.symbol symbol: assetProfile.price?.symbol
}); });
response.symbol = this.convertFromYahooFinanceSymbol( response.symbol = this.convertFromYahooFinanceSymbol(
assetProfile.price.symbol assetProfile.price.symbol

8
apps/api/src/services/data-provider/data-provider.service.ts

@ -99,7 +99,7 @@ export class DataProviderService implements OnModuleInit {
return dataSource; return dataSource;
}); });
const promises = []; const promises: Promise<void>[] = [];
for (const [dataSource, assetProfileIdentifiers] of Object.entries( for (const [dataSource, assetProfileIdentifiers] of Object.entries(
itemsGroupedByDataSource itemsGroupedByDataSource
@ -248,7 +248,7 @@ export class DataProviderService implements OnModuleInit {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === SubscriptionType.Basic user.subscription?.type === SubscriptionType.Basic
) { ) {
const dataProvider = this.getDataProvider(DataSource[dataSource]); const dataProvider = this.getDataProvider(DataSource[dataSource]);
@ -660,7 +660,7 @@ export class DataProviderService implements OnModuleInit {
} else if ( } else if (
dataProvider.getDataProviderInfo().isPremium && dataProvider.getDataProviderInfo().isPremium &&
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user?.subscription.type === SubscriptionType.Basic user?.subscription?.type === SubscriptionType.Basic
) { ) {
// Skip symbols of Premium data providers for users without subscription // Skip symbols of Premium data providers for users without subscription
return false; return false;
@ -876,7 +876,7 @@ export class DataProviderService implements OnModuleInit {
}) })
.map((lookupItem) => { .map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === SubscriptionType.Premium) { if (user.subscription?.type === SubscriptionType.Premium) {
lookupItem.dataProviderInfo.isPremium = false; lookupItem.dataProviderInfo.isPremium = false;
} }

2
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -58,6 +58,8 @@ export class GfHomeOverviewComponent implements OnInit {
protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink; protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink;
protected readonly routerLinkPortfolioActivities = protected readonly routerLinkPortfolioActivities =
internalRoutes.portfolio.subRoutes.activities.routerLink; internalRoutes.portfolio.subRoutes.activities.routerLink;
protected readonly routerLinkPortfolioActivitiesCreate =
internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink;
protected readonly deviceType = computed( protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType

2
apps/client/src/app/components/home-overview/home-overview.html

@ -52,7 +52,7 @@
<a <a
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="routerLinkPortfolioActivities" [routerLink]="routerLinkPortfolioActivitiesCreate"
> >
<ng-container i18n>Add activity</ng-container> <ng-container i18n>Add activity</ng-container>
</a> </a>

54
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -266,16 +266,9 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('currency')?.setValue(currency); this.activityForm.get('currency')?.setValue(currency);
this.activityForm.get('currencyOfUnitPrice')?.setValue(currency); this.activityForm.get('currencyOfUnitPrice')?.setValue(currency);
if (['FEE', 'INTEREST'].includes(type)) {
if (this.activityForm.get('accountId')?.value) {
this.activityForm.get('updateAccountBalance')?.enable();
} else {
this.activityForm.get('updateAccountBalance')?.disable();
this.activityForm.get('updateAccountBalance')?.setValue(false);
}
}
} }
this.syncUpdateAccountBalanceControl();
}); });
this.activityForm this.activityForm
@ -299,12 +292,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
}); });
this.activityForm.get('date')?.valueChanges.subscribe(() => { this.activityForm.get('date')?.valueChanges.subscribe(() => {
if (isToday(this.activityForm.get('date')?.value)) { this.syncUpdateAccountBalanceControl();
this.activityForm.get('updateAccountBalance')?.enable();
} else {
this.activityForm.get('updateAccountBalance')?.disable();
this.activityForm.get('updateAccountBalance')?.setValue(false);
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -384,8 +372,6 @@ export class GfCreateOrUpdateActivityDialogComponent {
.get('searchSymbol') .get('searchSymbol')
?.removeValidators(Validators.required); ?.removeValidators(Validators.required);
this.activityForm.get('searchSymbol')?.updateValueAndValidity(); this.activityForm.get('searchSymbol')?.updateValueAndValidity();
this.activityForm.get('updateAccountBalance')?.disable();
this.activityForm.get('updateAccountBalance')?.setValue(false);
} else if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { } else if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
const currency = const currency =
this.data.accounts.find(({ id }) => { this.data.accounts.find(({ id }) => {
@ -421,16 +407,6 @@ export class GfCreateOrUpdateActivityDialogComponent {
if (type === 'FEE') { if (type === 'FEE') {
this.activityForm.get('unitPrice')?.setValue(0); this.activityForm.get('unitPrice')?.setValue(0);
} }
if (
['FEE', 'INTEREST'].includes(type) &&
this.activityForm.get('accountId')?.value
) {
this.activityForm.get('updateAccountBalance')?.enable();
} else {
this.activityForm.get('updateAccountBalance')?.disable();
this.activityForm.get('updateAccountBalance')?.setValue(false);
}
} else { } else {
this.activityForm this.activityForm
.get('dataSource') .get('dataSource')
@ -442,9 +418,10 @@ export class GfCreateOrUpdateActivityDialogComponent {
.get('searchSymbol') .get('searchSymbol')
?.setValidators(Validators.required); ?.setValidators(Validators.required);
this.activityForm.get('searchSymbol')?.updateValueAndValidity(); this.activityForm.get('searchSymbol')?.updateValueAndValidity();
this.activityForm.get('updateAccountBalance')?.enable();
} }
this.syncUpdateAccountBalanceControl();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -559,6 +536,27 @@ export class GfCreateOrUpdateActivityDialogComponent {
} }
} }
private syncUpdateAccountBalanceControl() {
const accountBalanceControl = this.activityForm.get('updateAccountBalance');
const accountId = this.activityForm.get('accountId')?.value;
const dataSource = this.activityForm.get('dataSource')?.value;
const date = this.activityForm.get('date')?.value;
const type = this.activityForm.get('type')?.value;
const isEligible =
!!accountId &&
isToday(date) &&
!['LIABILITY', 'VALUABLE'].includes(type) &&
!(dataSource === 'MANUAL' && type === 'BUY');
if (isEligible) {
accountBalanceControl?.enable();
} else {
accountBalanceControl?.disable();
accountBalanceControl?.setValue(false);
}
}
private updateAssetProfile() { private updateAssetProfile() {
this.isLoading = true; this.isLoading = true;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

2
libs/common/src/lib/interfaces/access.interface.ts

@ -5,7 +5,7 @@ import { AccessPermission } from '@prisma/client';
import { AccessSettings } from './access-settings.interface'; import { AccessSettings } from './access-settings.interface';
export interface Access { export interface Access {
alias?: string; alias: string | null;
grantee?: string; grantee?: string;
id: string; id: string;
permissions: AccessPermission[]; permissions: AccessPermission[];

2
libs/common/src/lib/interfaces/historical-data-item.interface.ts

@ -11,7 +11,7 @@ export interface HistoricalDataItem {
netWorth?: number; netWorth?: number;
netWorthInPercentage?: number; netWorthInPercentage?: number;
quantity?: number; quantity?: number;
totalAccountBalance?: number; totalCashInBaseCurrency?: number;
totalInvestment?: number; totalInvestment?: number;
totalInvestmentValueWithCurrencyEffect?: number; totalInvestmentValueWithCurrencyEffect?: number;
value?: number; value?: number;

4
libs/common/src/lib/models/portfolio-snapshot.ts

@ -26,6 +26,10 @@ export class PortfolioSnapshot {
@Type(() => TimelinePosition) @Type(() => TimelinePosition)
positions: TimelinePosition[]; positions: TimelinePosition[];
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalCashInBaseCurrency: Big;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
totalFeesWithCurrencyEffect: Big; totalFeesWithCurrencyEffect: Big;

2
libs/common/src/lib/models/timeline-position.ts

@ -51,8 +51,6 @@ export class TimelinePosition {
@Type(() => Big) @Type(() => Big)
grossPerformanceWithCurrencyEffect: Big; grossPerformanceWithCurrencyEffect: Big;
includeInTotalAssetValue?: boolean;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
investment: Big; investment: Big;

2
libs/common/src/lib/types/access-with-grantee-user.type.ts

@ -1,3 +1,3 @@
import { Access, User } from '@prisma/client'; import { Access, User } from '@prisma/client';
export type AccessWithGranteeUser = Access & { granteeUser?: User }; export type AccessWithGranteeUser = Access & { granteeUser?: User | null };

3
libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html

@ -6,8 +6,7 @@
class="align-items-center justify-content-center" class="align-items-center justify-content-center"
color="primary" color="primary"
mat-button mat-button
[queryParams]="{ createDialog: true }" [routerLink]="routerLinkPortfolioActivitiesCreate"
[routerLink]="routerLinkPortfolioActivities"
> >
<span i18n>Time to add your first activity.</span> <span i18n>Time to add your first activity.</span>
</a> </a>

4
libs/ui/src/lib/no-transactions-info/no-transactions-info.component.ts

@ -23,6 +23,6 @@ import { GfLogoComponent } from '../logo';
export class GfNoTransactionsInfoComponent { export class GfNoTransactionsInfoComponent {
@HostBinding('class.has-border') @Input() hasBorder = true; @HostBinding('class.has-border') @Input() hasBorder = true;
public routerLinkPortfolioActivities = public routerLinkPortfolioActivitiesCreate =
internalRoutes.portfolio.subRoutes.activities.routerLink; internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink;
} }

102
package-lock.json

@ -64,13 +64,13 @@
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0", "countries-and-timezones": "3.9.0",
"countries-list": "3.4.0", "countries-list": "3.4.0",
"countup.js": "2.10.0", "countup.js": "2.10.1",
"date-fns": "4.4.0", "date-fns": "4.4.0",
"dotenv": "17.2.3", "dotenv": "17.4.2",
"dotenv-expand": "12.0.3", "dotenv-expand": "13.0.0",
"envalid": "8.2.0", "envalid": "8.2.0",
"fast-redact": "3.5.0", "fast-redact": "3.5.0",
"fuse.js": "7.3.0", "fuse.js": "7.5.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
"helmet": "8.2.0", "helmet": "8.2.0",
"http-status-codes": "2.3.0", "http-status-codes": "2.3.0",
@ -7090,6 +7090,33 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/@nestjs/config/node_modules/dotenv-expand": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "11.1.27", "version": "11.1.27",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.27.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.27.tgz",
@ -15770,19 +15797,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/c12/node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/c12/node_modules/jiti": { "node_modules/c12/node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -17068,9 +17082,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {
"version": "2.10.0", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.10.0.tgz", "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.10.1.tgz",
"integrity": "sha512-QQpZx7oYxsR+OeITlZe46fY/OQjV11oBqjY8wgIXzLU2jIz8GzOrbMhqKLysGY8bWI3T1ZNrYkwGzKb4JNgyzg==", "integrity": "sha512-UHW/BsPDgVZfN919D4iu2HPv+jJxZUuaR3EhT0ScFZaD446GiQtkMTgOnOyJXdA2AewdlyUk9UvERlTHTbVpcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/create-require": { "node_modules/create-require": {
@ -19169,9 +19183,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.2.3", "version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -19181,12 +19195,12 @@
} }
}, },
"node_modules/dotenv-expand": { "node_modules/dotenv-expand": {
"version": "12.0.3", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-13.0.0.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", "integrity": "sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"dotenv": "^16.4.5" "dotenv": "^17.4.2"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -19195,18 +19209,6 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -21313,9 +21315,9 @@
} }
}, },
"node_modules/fuse.js": { "node_modules/fuse.js": {
"version": "7.3.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.5.0.tgz",
"integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", "integrity": "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -27154,6 +27156,22 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/nx/node_modules/dotenv-expand": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/nx/node_modules/ejs": { "node_modules/nx/node_modules/ejs": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz",

8
package.json

@ -108,13 +108,13 @@
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0", "countries-and-timezones": "3.9.0",
"countries-list": "3.4.0", "countries-list": "3.4.0",
"countup.js": "2.10.0", "countup.js": "2.10.1",
"date-fns": "4.4.0", "date-fns": "4.4.0",
"dotenv": "17.2.3", "dotenv": "17.4.2",
"dotenv-expand": "12.0.3", "dotenv-expand": "13.0.0",
"envalid": "8.2.0", "envalid": "8.2.0",
"fast-redact": "3.5.0", "fast-redact": "3.5.0",
"fuse.js": "7.3.0", "fuse.js": "7.5.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
"helmet": "8.2.0", "helmet": "8.2.0",
"http-status-codes": "2.3.0", "http-status-codes": "2.3.0",

Loading…
Cancel
Save