diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 80a46b39a..b665b635a 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1136,17 +1136,22 @@ export class PortfolioService { withSummary: true }); - const { balanceInBaseCurrency: cashBalanceInBaseCurrency } = - await this.accountService.getCashDetails({ - userId, - currency: this.getUserCurrency() - }); + // The cash balance of the summary is split into the emergency fund and + // the remainder, both denominated in the base currency of the user + const cashBalanceInBaseCurrency = new Big(summary.cash) + .plus(summary.emergencyFund.cash) + .toNumber(); const emergencyFundInBaseCurrency = userSettings.emergencyFund ?? 0; const emergencyFundHoldingsValueInBaseCurrency = this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings }); + const totalEmergencyFundInBaseCurrency = this.getTotalEmergencyFund({ + emergencyFundHoldingsValueInBaseCurrency, + userSettings + }).toNumber(); + const hasOpenHoldings = Object.keys(holdings).length > 0; const marketsAdvancedTotalInBaseCurrency = getSum( @@ -1192,14 +1197,11 @@ export class PortfolioService { this.exchangeRateDataService, this.i18nService, userSettings.language, - this.getTotalEmergencyFund({ - emergencyFundHoldingsValueInBaseCurrency, - userSettings - }).toNumber() + totalEmergencyFundInBaseCurrency ), - // The coverage is only meaningful once a target has been set, - // otherwise the set up rule already reports the missing setup - ...(emergencyFundInBaseCurrency > 0 + // The coverage is only meaningful once an emergency fund has been + // set up, either by an amount or by the tagged holdings + ...(totalEmergencyFundInBaseCurrency > 0 ? [ new EmergencyFundCoverage( this.exchangeRateDataService, diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts index 479781acc..7f9bbba09 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts @@ -4,67 +4,68 @@ import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { DEFAULT_CURRENCY, DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces'; -export class EmergencyFundCoverage extends Rule { - private cashBalanceInBaseCurrency: number; - private emergencyFundHoldingsValueInBaseCurrency: number; - private emergencyFundInBaseCurrency: number; +import { Big } from 'big.js'; +export class EmergencyFundCoverage extends Rule { public constructor( exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, - emergencyFundInBaseCurrency: number, - emergencyFundHoldingsValueInBaseCurrency: number, - cashBalanceInBaseCurrency: number + private emergencyFundInBaseCurrency: number, + private emergencyFundHoldingsValueInBaseCurrency: number, + private cashBalanceInBaseCurrency: number ) { super(exchangeRateDataService, { languageCode, key: EmergencyFundCoverage.name }); - - this.cashBalanceInBaseCurrency = cashBalanceInBaseCurrency; - this.emergencyFundHoldingsValueInBaseCurrency = - emergencyFundHoldingsValueInBaseCurrency; - this.emergencyFundInBaseCurrency = emergencyFundInBaseCurrency; } public evaluate(ruleSettings: Settings) { + if (!this.emergencyFundInBaseCurrency) { + return { + evaluation: this.i18nService.getTranslation({ + id: 'rule.emergencyFundCoverage.false.unset', + languageCode: this.getLanguageCode() + }), + value: false + }; + } + + const placeholders = { + baseCurrency: ruleSettings.baseCurrency, + emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString( + ruleSettings.locale + ) + }; + // Only the holdings tagged as emergency fund are an explicit commitment, // the cash balance covers the remainder if ( - this.emergencyFundHoldingsValueInBaseCurrency > - this.emergencyFundInBaseCurrency + new Big(this.emergencyFundHoldingsValueInBaseCurrency).gt( + this.emergencyFundInBaseCurrency + ) ) { return { evaluation: this.i18nService.getTranslation({ + placeholders, id: 'rule.emergencyFundCoverage.false.over', - languageCode: this.getLanguageCode(), - placeholders: { - baseCurrency: ruleSettings.baseCurrency, - emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString( - ruleSettings.locale - ) - } + languageCode: this.getLanguageCode() }), value: false }; } - const coverageInBaseCurrency = - this.emergencyFundHoldingsValueInBaseCurrency + - this.cashBalanceInBaseCurrency; + const coverageInBaseCurrency = new Big( + this.emergencyFundHoldingsValueInBaseCurrency + ).plus(this.cashBalanceInBaseCurrency); - if (coverageInBaseCurrency < this.emergencyFundInBaseCurrency) { + if (coverageInBaseCurrency.lt(this.emergencyFundInBaseCurrency)) { return { evaluation: this.i18nService.getTranslation({ + placeholders, id: 'rule.emergencyFundCoverage.false.under', - languageCode: this.getLanguageCode(), - placeholders: { - baseCurrency: ruleSettings.baseCurrency, - emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString( - ruleSettings.locale - ) - } + languageCode: this.getLanguageCode() }), value: false }; @@ -72,14 +73,9 @@ export class EmergencyFundCoverage extends Rule { return { evaluation: this.i18nService.getTranslation({ + placeholders, id: 'rule.emergencyFundCoverage.true', - languageCode: this.getLanguageCode(), - placeholders: { - baseCurrency: ruleSettings.baseCurrency, - emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString( - ruleSettings.locale - ) - } + languageCode: this.getLanguageCode() }), value: true }; diff --git a/apps/client/src/app/pages/i18n/i18n-page.html b/apps/client/src/app/pages/i18n/i18n-page.html index 4ac05acf4..a685074ee 100644 --- a/apps/client/src/app/pages/i18n/i18n-page.html +++ b/apps/client/src/app/pages/i18n/i18n-page.html @@ -151,6 +151,9 @@ Your emergency fund of ${emergencyFund} ${baseCurrency} is not covered +
  • + No emergency fund amount has been set to determine the coverage +
  • Your emergency fund of ${emergencyFund} ${baseCurrency} is covered