From b12349a1484ce5ee4a6a1bf8581d91c8fce9b37a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:37:04 +0200 Subject: [PATCH] Feature/add support for interest on account level (#2354) * Add support for interest * Update changelog --- CHANGELOG.md | 4 + apps/api/src/app/export/export.service.ts | 5 +- apps/api/src/app/order/order.service.ts | 8 +- .../src/app/portfolio/portfolio.service.ts | 209 ++++++++---------- .../portfolio-summary.component.html | 12 + ...ate-or-update-activity-dialog.component.ts | 16 +- .../create-or-update-activity-dialog.html | 15 +- apps/client/src/locales/messages.de.xlf | 175 +++++++++------ apps/client/src/locales/messages.es.xlf | 170 ++++++++------ apps/client/src/locales/messages.fr.xlf | 170 ++++++++------ apps/client/src/locales/messages.it.xlf | 170 ++++++++------ apps/client/src/locales/messages.nl.xlf | 170 ++++++++------ apps/client/src/locales/messages.pt.xlf | 170 ++++++++------ apps/client/src/locales/messages.xlf | 168 ++++++++------ .../interfaces/portfolio-summary.interface.ts | 1 + .../activities-table.component.html | 8 +- .../activities-table.component.scss | 4 + .../activities-table.component.ts | 9 +- libs/ui/src/lib/i18n.ts | 1 + .../migration.sql | 2 + prisma/schema.prisma | 1 + 21 files changed, 859 insertions(+), 629 deletions(-) create mode 100644 prisma/migrations/20230918204124_added_interest_to_order_type/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e52fd354..13b448d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for interest on account level (experimental) + ### Changed - Improved the preselected currency based on the account's currency in the create or edit activity dialog diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index f717a3ead..2134a6520 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -78,7 +78,10 @@ export class ExportService { dataSource: SymbolProfile.dataSource, date: date.toISOString(), symbol: - type === 'FEE' || type === 'ITEM' || type === 'LIABILITY' + type === 'FEE' || + type === 'INTEREST' || + type === 'ITEM' || + type === 'LIABILITY' ? SymbolProfile.name : SymbolProfile.symbol }; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 13709fad1..3c20f9ba0 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -99,6 +99,7 @@ export class OrderService { if ( data.type === 'FEE' || + data.type === 'INTEREST' || data.type === 'ITEM' || data.type === 'LIABILITY' ) { @@ -155,7 +156,10 @@ export class OrderService { const orderData: Prisma.OrderCreateInput = data; const isDraft = - data.type === 'FEE' || data.type === 'ITEM' || data.type === 'LIABILITY' + data.type === 'FEE' || + data.type === 'INTEREST' || + data.type === 'ITEM' || + data.type === 'LIABILITY' ? false : isAfter(data.date as Date, endOfToday()); @@ -203,6 +207,7 @@ export class OrderService { if ( order.type === 'FEE' || + order.type === 'INTEREST' || order.type === 'ITEM' || order.type === 'LIABILITY' ) { @@ -378,6 +383,7 @@ export class OrderService { if ( data.type === 'FEE' || + data.type === 'INTEREST' || data.type === 'ITEM' || data.type === 'LIABILITY' ) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6deda49b7..228ab18f6 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -56,12 +56,11 @@ import { Platform, Prisma, Tag, - Type as TypeOfOrder + Type as ActivityType } from '@prisma/client'; import Big from 'big.js'; import { differenceInDays, - endOfToday, format, isAfter, isBefore, @@ -1342,36 +1341,6 @@ export class PortfolioService { return cashPositions; } - private getDividend({ - activities, - date = new Date(0), - userCurrency - }: { - activities: OrderWithAccount[]; - date?: Date; - userCurrency: string; - }) { - return activities - .filter((activity) => { - // Filter out all activities before given date (drafts) and type dividend - return ( - isBefore(date, new Date(activity.date)) && - activity.type === TypeOfOrder.DIVIDEND - ); - }) - .map(({ quantity, SymbolProfile, unitPrice }) => { - return this.exchangeRateDataService.toCurrency( - new Big(quantity).mul(unitPrice).toNumber(), - SymbolProfile.currency, - userCurrency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - private getDividendsByGroup({ dividends, groupBy @@ -1516,52 +1485,6 @@ export class PortfolioService { }; } - private getItems(activities: OrderWithAccount[], date = new Date(0)) { - return activities - .filter((activity) => { - // Filter out all activities before given date (drafts) and type item - return ( - isBefore(date, new Date(activity.date)) && - activity.type === TypeOfOrder.ITEM - ); - }) - .map(({ quantity, SymbolProfile, unitPrice }) => { - return this.exchangeRateDataService.toCurrency( - new Big(quantity).mul(unitPrice).toNumber(), - SymbolProfile.currency, - this.request.user.Settings.settings.baseCurrency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - - private getLiabilities({ - activities, - userCurrency - }: { - activities: OrderWithAccount[]; - userCurrency: string; - }) { - return activities - .filter(({ type }) => { - return type === TypeOfOrder.LIABILITY; - }) - .map(({ quantity, SymbolProfile, unitPrice }) => { - return this.exchangeRateDataService.toCurrency( - new Big(quantity).mul(unitPrice).toNumber(), - SymbolProfile.currency, - userCurrency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': @@ -1650,9 +1573,10 @@ export class PortfolioService { return account?.isExcluded ?? false; }); - const dividend = this.getDividend({ + const dividend = this.getSumOfActivityType({ activities, - userCurrency + userCurrency, + activityType: 'DIVIDEND' }).toNumber(); const emergencyFund = new Big( Math.max( @@ -1662,23 +1586,49 @@ export class PortfolioService { ); const fees = this.getFees({ activities, userCurrency }).toNumber(); const firstOrderDate = activities[0]?.date; - const items = this.getItems(activities).toNumber(); - const liabilities = this.getLiabilities({ + const interest = this.getSumOfActivityType({ + activities, + userCurrency, + activityType: 'INTEREST' + }).toNumber(); + const items = this.getSumOfActivityType({ activities, - userCurrency + userCurrency, + activityType: 'ITEM' + }).toNumber(); + const liabilities = this.getSumOfActivityType({ + activities, + userCurrency, + activityType: 'LIABILITY' }).toNumber(); - const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); - const totalSell = this.getTotalByType(activities, userCurrency, 'SELL'); + const totalBuy = this.getSumOfActivityType({ + activities, + userCurrency, + activityType: 'BUY' + }).toNumber(); + const totalSell = this.getSumOfActivityType({ + activities, + userCurrency, + activityType: 'SELL' + }).toNumber(); const cash = new Big(balanceInBaseCurrency) .minus(emergencyFund) .plus(emergencyFundPositionsValueInBaseCurrency) .toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); - const totalOfExcludedActivities = new Big( - this.getTotalByType(excludedActivities, userCurrency, 'BUY') - ).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL')); + const totalOfExcludedActivities = this.getSumOfActivityType({ + userCurrency, + activities: excludedActivities, + activityType: 'BUY' + }).minus( + this.getSumOfActivityType({ + userCurrency, + activities: excludedActivities, + activityType: 'SELL' + }) + ); const cashDetailsWithExcludedAccounts = await this.accountService.getCashDetails({ @@ -1725,6 +1675,7 @@ export class PortfolioService { excludedAccountsAndActivities, fees, firstOrderDate, + interest, items, liabilities, netWorth, @@ -1747,6 +1698,39 @@ export class PortfolioService { }; } + private getSumOfActivityType({ + activities, + activityType, + date = new Date(0), + userCurrency + }: { + activities: OrderWithAccount[]; + activityType: ActivityType; + date?: Date; + userCurrency: string; + }) { + return activities + .filter((activity) => { + // Filter out all activities before given date (drafts) and + // activity type + return ( + isBefore(date, new Date(activity.date)) && + activity.type === activityType + ); + }) + .map(({ quantity, SymbolProfile, unitPrice }) => { + return this.exchangeRateDataService.toCurrency( + new Big(quantity).mul(unitPrice).toNumber(), + SymbolProfile.currency, + userCurrency + ); + }) + .reduce( + (previous, current) => new Big(previous).plus(current), + new Big(0) + ); + } + private async getTransactionPoints({ filters, includeDrafts = false, @@ -1818,6 +1802,21 @@ export class PortfolioService { }; } + private getUserCurrency(aUser: UserWithSettings) { + return ( + aUser.Settings?.settings.baseCurrency ?? + this.request.user?.Settings?.settings.baseCurrency ?? + DEFAULT_CURRENCY + ); + } + + private async getUserId(aImpersonationId: string, aUserId: string) { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(aImpersonationId); + + return impersonationUserId || aUserId; + } + private async getValueOfAccountsAndPlatforms({ filters = [], orders, @@ -1961,38 +1960,4 @@ export class PortfolioService { return { accounts, platforms }; } - - private getTotalByType( - orders: OrderWithAccount[], - currency: string, - type: TypeOfOrder - ) { - return orders - .filter( - (order) => !isAfter(order.date, endOfToday()) && order.type === type - ) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - order.quantity * order.unitPrice, - order.SymbolProfile.currency, - currency - ); - }) - .reduce((previous, current) => previous + current, 0); - } - - private getUserCurrency(aUser: UserWithSettings) { - return ( - aUser.Settings?.settings.baseCurrency ?? - this.request.user?.Settings?.settings.baseCurrency ?? - DEFAULT_CURRENCY - ); - } - - private async getUserId(aImpersonationId: string, aUserId: string) { - const impersonationUserId = - await this.impersonationService.validateImpersonationId(aImpersonationId); - - return impersonationUserId || aUserId; - } } diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index a84fbebaf..aaf9bfb73 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -276,6 +276,18 @@