From 3476d9e41882dba42d22710fac7213c5518f65ec Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:34:44 +0200 Subject: [PATCH 01/10] Task/remove symbol profile from import preview (#8023) Remove symbol profile from import preview --- apps/api/src/app/import/import.service.ts | 61 +++-------------------- 1 file changed, 8 insertions(+), 53 deletions(-) diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 705066441f..8a2c80e2c3 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -857,28 +857,6 @@ export class ImportService { dataSource: activity.assetProfile.dataSource, symbol: activity.assetProfile.symbol }; - const { - assetClass, - assetSubClass, - countries, - createdAt, - cusip, - dataSource, - figi, - figiComposite, - figiShareClass, - holdings, - id, - isActive, - isin, - name, - scraperConfiguration, - sectors, - symbol, - symbolMapping, - url, - updatedAt - } = assetProfile; const validatedAccount = accounts.find(({ id }) => { return id === accountId; }); @@ -890,7 +868,7 @@ export class ImportService { let order: | OrderWithAccount - | (Omit & { + | (Omit & { account?: { id: string; name: string }; tags?: { id: string; name: string }[]; }); @@ -916,33 +894,6 @@ export class ImportService { accountUserId: undefined, createdAt: new Date(), id: randomUUID(), - SymbolProfile: { - assetClass, - assetSubClass, - countries, - createdAt, - cusip, - dataSource, - figi, - figiComposite, - figiShareClass, - holdings, - id, - isActive, - isin, - name, - scraperConfiguration, - sectors, - symbol, - symbolMapping, - updatedAt, - url, - comment: assetProfile.comment, - currency: assetProfile.currency, - dataGatheringFrequency: - assetProfile.dataGatheringFrequency ?? 'DAILY', - userId: dataSource === 'MANUAL' ? user.id : undefined - }, symbolProfileId: undefined, tags: previewTags, updatedAt: new Date(), @@ -953,7 +904,9 @@ export class ImportService { continue; } - order = await this.activitiesService.createActivity({ + const { dataSource, name, symbol } = assetProfile; + + const createdActivity = await this.activitiesService.createActivity({ comment, currency, date, @@ -987,10 +940,12 @@ export class ImportService { userId: user.id }); - if (order.SymbolProfile?.symbol) { + if (createdActivity.SymbolProfile?.symbol) { // Update symbol that may have been assigned in createOrder() - assetProfile.symbol = order.SymbolProfile.symbol; + assetProfile.symbol = createdActivity.SymbolProfile.symbol; } + + order = createdActivity; } const value = new Big(quantity).mul(unitPrice).toNumber(); From 5aec4a8a163e5e5422b29611613b0097f20bd08c Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:58:47 +0700 Subject: [PATCH 02/10] Task/widen input types in value component (#8032) Widen input types --- libs/ui/src/lib/value/value.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 4101b4066f..dfbe461f60 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -47,7 +47,7 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() size: 'large' | 'medium' | 'small' = 'small'; @Input() subLabel?: string = ''; @Input() unit?: string | null = ''; - @Input() value?: number | string | null = ''; + @Input() value?: Date | number | string | null = ''; @ViewChild('labelContent', { static: false }) labelContent!: ElementRef; @@ -171,7 +171,7 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy { } ); } else { - this.formattedValue = this.value; + this.formattedValue = String(this.value); } } } From 85338780eba8fcd879a2e41f5edf5a108554c3a7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:02:42 +0200 Subject: [PATCH 03/10] Task/improve Ghostfolio MCP description in Features page (#8033) Improve description --- apps/client/src/app/pages/features/features-page.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 7cc80901f8..9bec1073a5 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -224,8 +224,8 @@ >

- Let your AI client read your portfolio and import activities - into it via the Model Context Protocol (MCP). + Let your AI client analyze your portfolio and import + activities into it via the Model Context Protocol (MCP).

From d6e234266c696d1dbbaf9a83a69dc7a9d422dda6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:03:01 +0200 Subject: [PATCH 04/10] Task/upgrade simplewebauthn to version 14 (#8034) * Update simplewebauthn to version 14.0 * Update changelog --- CHANGELOG.md | 6 ++ package-lock.json | 258 +++++++++++++++++++++++++++++++--------------- package.json | 4 +- 3 files changed, 185 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d674289d4a..0dbd3d3c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 + +- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` + ## 3.78.0 - 2026-10-03 ### Added diff --git a/package-lock.json b/package-lock.json index d0096a72d3..8fe0f53a49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,8 +50,8 @@ "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.7", - "@simplewebauthn/browser": "13.3.0", - "@simplewebauthn/server": "13.3.1", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.3", "ai": "7.0.114", "alphavantage": "2.2.0", "big.js": "7.0.1", @@ -13034,129 +13034,200 @@ "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-asym-key": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.9.5.tgz", + "integrity": "sha512-U3Q2bUWYNVpfk+oOhPl/+OSHxXaBTQBwMf93jJ7oPi/sbmcY5CnARdjGMwLoXT/ggWSryo5t7A7o3/Gfz0U2WA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-pkcs8": "^2.9.5", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.5.tgz", + "integrity": "sha512-4Ivz0fsyFofe0bslNsYcGN9+iETdHX4v+BFwIPr+K/BAt61sHXECP63ff9FntU9zcfVQELJFdUG9so44OjUlBQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "@peculiar/asn1-x509-attr": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.5.tgz", + "integrity": "sha512-9dO6PcoJeI+1xJ4rG7eINMa6f9Ix/QdRs5UGZ0D7v/R3TzmsbVr5cqAgfYw4xgS9U54ouHYqEz0bIZBbM/UY1g==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.5.tgz", + "integrity": "sha512-OD+HLlcV7wCL64w0rheX/C0J31DbJce0nf8ulv3UNftQ617JEZE3sQ4cMN6PotXWlJ1M5GQx90G3bywtdKOstw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.5.tgz", + "integrity": "sha512-btcH7lbcD4410qnjHvv7F0kEuMNKbX8CFAZSs9CDPWHKzT1byLPHYeR40AnUw9z4CzsijMY0daBFYUvVx75sdA==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", + "@peculiar/asn1-cms": "^2.9.5", + "@peculiar/asn1-pkcs8": "^2.9.5", + "@peculiar/asn1-rsa": "^2.9.5", + "@peculiar/asn1-schema": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.5.tgz", + "integrity": "sha512-c7UghAGQdBfAHL9JdwcJkAc/lDJKNXfCdYXPXlf8BZdD/fVJjJnXKHxX4gyLhkFTvnGhME7caieSgc2ROtqYTQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.5.tgz", + "integrity": "sha512-4R/zMgLSHbQIZRBwP1KzX6iuNHmW3xFWwu9TS9A00N60fmQqp9DkkqnQFyd0mAYMQTrQefuzQ/yP4U8sOLGUgQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.5", + "@peculiar/asn1-pfx": "^2.9.5", + "@peculiar/asn1-pkcs8": "^2.9.5", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "@peculiar/asn1-x509-attr": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.5.tgz", + "integrity": "sha512-kHwBLVMkm9ljlM521ovS7y4z4mr8xuHTdNyyoPlDvckBwFKSgs9HqKBryLBeqHqL2YpIgl+Vspl7kzNGG2phzA==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.5.tgz", + "integrity": "sha512-Ez3wLKVjaxdsLcgeWN4OE31QkM7oBOgKuuBJxldRYAkfYw2C+8zJPcSd/SThnbhszsEOlAmoaS5kIIkN29fGKQ==", "license": "MIT", "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.5.tgz", + "integrity": "sha512-NP3ecvN6zTDlwCvogIWL7MJN9AZNuskkIbnlhrlkB97Mn2EqirBUQ8GKzsOmdAL3pRvjGu1Uk/a4DNfiZrWxhw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.5.tgz", + "integrity": "sha512-PlbL1a74RZiry7puPCZ5hdk/puvivxYJPbjadr2i7DBjHCJFEKCfTRPKL3DILeZFtzJjn95J5EBtSc4/TB9/6Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-post-quantum": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.9.5.tgz", + "integrity": "sha512-CTznDkboSgwRpXSFhta+Pe5cQ12JtojdMrmOZTWYkBSvbW/1R5pflPIHkpSlaILIfGcAaxIVpXq880xCfJYzow==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-asym-key": "^2.9.5", + "@peculiar/asn1-schema": "^2.9.5", + "@peculiar/asn1-x509": "^2.9.5", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, @@ -13164,6 +13235,7 @@ "version": "1.14.3", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.0", @@ -14195,15 +14267,15 @@ } }, "node_modules/@simplewebauthn/browser": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", - "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-14.0.0.tgz", + "integrity": "sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==", "license": "MIT" }, "node_modules/@simplewebauthn/server": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.1.tgz", - "integrity": "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.3.tgz", + "integrity": "sha512-D39/0fiDiYmGV+nraY9s63D1Ho2AX/waMLy22/9gUadTUuvbAnstfb4bpbHYbwZuV5ZLDYN1SmzDKuiHzKAA4Q==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", @@ -14213,7 +14285,31 @@ "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/x509": "^1.14.3" + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@simplewebauthn/server/node_modules/@peculiar/x509": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz", + "integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-csr": "^2.9.4", + "@peculiar/asn1-ecc": "^2.9.4", + "@peculiar/asn1-pkcs9": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" }, "engines": { "node": ">=20.0.0" @@ -17192,13 +17288,13 @@ } }, "node_modules/asn1js": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", - "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", + "pvutils": "^1.1.5", "tslib": "^2.8.1" }, "engines": { diff --git a/package.json b/package.json index b19db0a4bc..81668dcec2 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.7", - "@simplewebauthn/browser": "13.3.0", - "@simplewebauthn/server": "13.3.1", + "@simplewebauthn/browser": "14.0.0", + "@simplewebauthn/server": "14.0.3", "ai": "7.0.114", "alphavantage": "2.2.0", "big.js": "7.0.1", From 4cfabb0b3210192f906398e972bd607fcfd8e05e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 09:18:05 +0200 Subject: [PATCH 05/10] Task/extend activities service tests by get activities (#8025) Extend tests --- .../app/activities/activities.service.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/apps/api/src/app/activities/activities.service.spec.ts b/apps/api/src/app/activities/activities.service.spec.ts index 72d1950294..d1eb1b4873 100644 --- a/apps/api/src/app/activities/activities.service.spec.ts +++ b/apps/api/src/app/activities/activities.service.spec.ts @@ -4,6 +4,9 @@ import { assetProfileDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { INVESTMENT_ACTIVITY_TYPES, NON_INVESTMENT_ACTIVITY_TYPES @@ -41,6 +44,85 @@ describe('ActivitiesService', () => { ); }); + describe('getActivities', () => { + it('returns the activities with the asset profile but without the relation to the symbol profile', async () => { + const assetProfile = { + ...assetProfileDummyData, + currency: 'USD', + dataSource: DataSource.YAHOO, + name: 'Apple Inc.', + symbol: 'AAPL' + }; + + const findMany = jest.fn().mockResolvedValue([ + { + ...activityDummyData, + account: null, + currency: 'USD', + date: parseDate('2021-01-01'), + fee: 1, + id: 'activity-id', + quantity: 10, + SymbolProfile: { + currency: 'USD', + dataSource: DataSource.YAHOO, + symbol: 'AAPL' + }, + tags: [], + type: 'BUY', + unitPrice: 100 + } + ]); + + const service = new ActivitiesService( + null, + null, + null, + null, + null, + null, + null, + { + toCurrencyAtDate: jest.fn().mockResolvedValue(0) + } as unknown as ExchangeRateDataService, + null, + { + order: { + findMany, + count: jest.fn().mockResolvedValue(1) + } + } as unknown as PrismaService, + { + getSymbolProfiles: jest + .fn() + .mockResolvedValue([ + { ...assetProfile, dataSource: DataSource.MANUAL }, + assetProfile + ]) + } as unknown as SymbolProfileService, + null + ); + + const { activities } = await service.getActivities({ + userCurrency: 'USD', + userId: 'user-id' + }); + + expect(findMany).toHaveBeenCalledWith( + expect.objectContaining({ + include: expect.objectContaining({ + SymbolProfile: { + select: { currency: true, dataSource: true, symbol: true } + } + }) + }) + ); + expect(activities).toHaveLength(1); + expect(activities[0]).not.toHaveProperty('SymbolProfile'); + expect(activities[0].assetProfile).toBe(assetProfile); + }); + }); + describe('getActivitiesForPortfolioCalculator', () => { it('leaves an activity unchanged when no splits exist', async () => { const activity = createActivity({ symbol: 'AAPL' }); From 521f32054a5db358793702c1f1bb94f1c603d541 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 09:41:17 +0200 Subject: [PATCH 06/10] Task/upgrade nestjs to version 11.2.6 (#8040) * Update nestjs to version 11.2.6 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 134 ++++++++++------------------------------------ package.json | 10 ++-- 3 files changed, 33 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dbd3d3c09..4aa48dcce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` +- Upgraded `nestjs` from version `11.2.3` to `11.2.6` ## 3.78.0 - 2026-10-03 diff --git a/package-lock.json b/package-lock.json index 8fe0f53a49..b25373e7b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,14 +35,14 @@ "@nest-lab/throttler-storage-redis": "1.2.0", "@nestjs/bull": "11.0.5", "@nestjs/cache-manager": "3.1.3", - "@nestjs/common": "11.2.3", + "@nestjs/common": "11.2.6", "@nestjs/config": "4.0.4", - "@nestjs/core": "11.2.3", + "@nestjs/core": "11.2.6", "@nestjs/event-emitter": "3.1.0", "@nestjs/jwt": "11.0.2", - "@nestjs/microservices": "11.2.3", + "@nestjs/microservices": "11.2.6", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.2.3", + "@nestjs/platform-express": "11.2.6", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", @@ -124,7 +124,7 @@ "@angular/pwa": "22.1.4", "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", - "@nestjs/testing": "11.2.3", + "@nestjs/testing": "11.2.6", "@nx/angular": "23.1.1", "@nx/eslint-plugin": "23.1.1", "@nx/jest": "23.1.1", @@ -9542,9 +9542,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.3.tgz", - "integrity": "sha512-obdauJXHfthhepbV+LpGe88OeBlR/Kw9lwjLo0Utzc//agoLXYb9DUGhPQWtm81IpBWMv+19eiwcve9MsBZwXA==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.6.tgz", + "integrity": "sha512-uXs98qEOesTf6rGs0OcvFpRdF4M6JvzyBmoIXcTYvojdte4HiwDYv+whaALoboyfu/fuq2umF74FLQu3V7ZPHw==", "license": "MIT", "dependencies": { "file-type": "21.3.4", @@ -9627,9 +9627,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.3.tgz", - "integrity": "sha512-vkA9/Ja0Z3hvqXErSa+HaxrfF+cNXthNFi8VPNEKVli4rMd009yExAl0gLmko/Kf8peDXr72u1RN+j9Da2ukHg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.6.tgz", + "integrity": "sha512-KZkwy6KBePPeT3EUj+NACVq6VLT5S+Msbc761NAVVyQ0FyOj7o4I85GEg/cdEPu6KdLqhJFfuqRW2SN+J2ZubQ==", "license": "MIT", "dependencies": { "fast-safe-stringify": "2.1.1", @@ -9692,9 +9692,9 @@ } }, "node_modules/@nestjs/microservices": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.2.3.tgz", - "integrity": "sha512-HcMm6c8dwKbU0CSOFYxcHZ17YkQawnVzpfT58u24/fAzmsA1WJyfCej5+uONjuG7L/CG76OSxKZlZBNzcxkUiw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.2.6.tgz", + "integrity": "sha512-vtwCIFfVWMBwuSuTfBm/EOQyWTZaVP7GZblDrpLKPggCNkHWwDBLwAd73jyn+DPcDthZQOZLmYESI/3tlaZ8KA==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -9760,14 +9760,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.3.tgz", - "integrity": "sha512-YFQvRXT2de1qNL9LJPUBQ31+RsfI4cJ+sbpU9ENM/hDCgoHSEhm7oxUuGGKmhTZBNZEYm8mDYdfoTFmAH1LIJg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.6.tgz", + "integrity": "sha512-jPypI/+aueizkYG2Bw2kg8GaNsWE7ekFBSQ/Rwet7wEdBA/+q2FA364UbDabqjlR7C3VSNHmP/JXh+8+jXyDOw==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", - "multer": "2.2.0", + "multer": "2.4.0", "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, @@ -10043,9 +10043,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.2.3.tgz", - "integrity": "sha512-7ANDWlkm8Xw4CYIhCNZhtBzANsQUKqjteA2yx/6sjqGyWhekeBKz8wgCJykm0vo+ltrg6U34dZlm2NgiRcNHPQ==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.2.6.tgz", + "integrity": "sha512-Oua8EUw3WsnSIwAMIpvyuJxOdUxHeX04fX5MsSIpjaft5CcNnbWhSmM/u/GDDOEcAOrxOfG/KHa7LzzWdS/bxw==", "dev": true, "license": "MIT", "dependencies": { @@ -13721,67 +13721,6 @@ } } }, - "node_modules/@rekog/mcp-nest/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@rekog/mcp-nest/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@rekog/mcp-nest/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@rekog/mcp-nest/node_modules/multer": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", - "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">= 10.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@rekog/mcp-nest/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz", @@ -19039,21 +18978,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -27946,14 +27870,13 @@ } }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", + "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", - "concat-stream": "^2.0.0", "type-is": "^1.6.18" }, "engines": { @@ -31128,6 +31051,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -33577,6 +33501,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -35055,12 +34980,6 @@ "dev": true, "license": "MIT" }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -35372,6 +35291,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utila": { diff --git a/package.json b/package.json index 81668dcec2..d4d7169c0d 100644 --- a/package.json +++ b/package.json @@ -79,14 +79,14 @@ "@nest-lab/throttler-storage-redis": "1.2.0", "@nestjs/bull": "11.0.5", "@nestjs/cache-manager": "3.1.3", - "@nestjs/common": "11.2.3", + "@nestjs/common": "11.2.6", "@nestjs/config": "4.0.4", - "@nestjs/core": "11.2.3", + "@nestjs/core": "11.2.6", "@nestjs/event-emitter": "3.1.0", "@nestjs/jwt": "11.0.2", - "@nestjs/microservices": "11.2.3", + "@nestjs/microservices": "11.2.6", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.2.3", + "@nestjs/platform-express": "11.2.6", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", @@ -168,7 +168,7 @@ "@angular/pwa": "22.1.4", "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", - "@nestjs/testing": "11.2.3", + "@nestjs/testing": "11.2.6", "@nx/angular": "23.1.1", "@nx/eslint-plugin": "23.1.1", "@nx/jest": "23.1.1", From 903eee24052df20a16725318bea5030f5ec3d8cb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 10:02:37 +0200 Subject: [PATCH 07/10] Bugfix/asset profile of imported activities (#8024) * Fix asset profile of imported activities * Update changelog --- CHANGELOG.md | 4 + .../api/src/app/import/import.service.spec.ts | 365 ++++++++++++++++++ apps/api/src/app/import/import.service.ts | 37 +- 3 files changed, 396 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/app/import/import.service.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa48dcce6..b39f92677a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` - Upgraded `nestjs` from version `11.2.3` to `11.2.6` +### Fixed + +- Fixed the asset profile of the activities after a fee, an interest or a liability with the same symbol in the activities import + ## 3.78.0 - 2026-10-03 ### Added diff --git a/apps/api/src/app/import/import.service.spec.ts b/apps/api/src/app/import/import.service.spec.ts new file mode 100644 index 0000000000..afabdbcb1f --- /dev/null +++ b/apps/api/src/app/import/import.service.spec.ts @@ -0,0 +1,365 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; +import { NON_INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config'; +import { CreateOrderDto } from '@ghostfolio/common/dtos'; +import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; +import { + Activity, + AssetProfileIdentifier +} from '@ghostfolio/common/interfaces'; +import { UserWithSettings } from '@ghostfolio/common/types'; + +import { DataSource, SymbolProfile } from '@prisma/client'; +import { parseISO } from 'date-fns'; + +import { ImportService } from './import.service'; + +const CUSTOM_ASSET_PROFILE_SYMBOL = '1ad7d4a2-6b2d-4e0f-9b1f-2c0f8d3e5a7b'; + +describe('ImportService', () => { + let createActivity: jest.Mock; + let gatherSymbols: jest.Mock; + let importService: ImportService; + + beforeEach(() => { + const configuration = { + DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: [], + ENABLE_FEATURE_SUBSCRIPTION: false, + MAX_ACTIVITIES_TO_IMPORT: Number.MAX_SAFE_INTEGER + }; + + createActivity = jest.fn(); + gatherSymbols = jest.fn(); + + const validateActivities = jest.fn( + ({ activitiesDto }: { activitiesDto: CreateOrderDto[] }) => { + const assetProfiles: { + [assetProfileIdentifier: string]: Partial; + } = {}; + + for (const { currency, dataSource, symbol } of activitiesDto) { + assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] ??= { + currency, + dataSource, + symbol, + name: symbol + }; + } + + return assetProfiles; + } + ); + + importService = new ImportService( + { + getAccounts: jest.fn().mockResolvedValue([]) + } as unknown as AccountService, + { + createActivity, + getActivities: jest.fn().mockResolvedValue({ activities: [], count: 0 }) + } as unknown as ActivitiesService, + null, + { + get: (key: keyof typeof configuration) => { + return configuration[key]; + } + } as unknown as ConfigurationService, + { gatherSymbols } as unknown as DataGatheringService, + { + validateActivities, + getDataSourceForImport: jest.fn().mockReturnValue(DataSource.MANUAL) + } as unknown as DataProviderService, + { + toCurrencyAtDate: jest.fn().mockResolvedValue(0) + } as unknown as ExchangeRateDataService, + null, + null, + null, + null, + { + getTagsForUser: jest.fn().mockResolvedValue([]) + } as unknown as TagService + ); + }); + + describe('import', () => { + it('keeps the asset profile of an investment activity after a non-investment activity', async () => { + mockCreatedAssetProfiles([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.YAHOO, symbol: 'AAPL' } + ]); + + const activities = await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'FEE' + }), + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-02T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }) + ] + }); + + expect( + createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create + ).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' }); + + expect(getAssetProfileIdentifiers(activities)).toEqual([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.YAHOO, symbol: 'AAPL' } + ]); + + expect(gatherSymbols.mock.calls[0][0].dataGatheringItems).toEqual([ + { + dataSource: DataSource.MANUAL, + date: parseISO('2024-01-01T00:00:00.000Z'), + symbol: CUSTOM_ASSET_PROFILE_SYMBOL + }, + { + dataSource: DataSource.YAHOO, + date: parseISO('2024-01-02T00:00:00.000Z'), + symbol: 'AAPL' + } + ]); + }); + + it('shows the custom asset profile of a non-investment activity after an investment activity', async () => { + mockCreatedAssetProfiles([ + { dataSource: DataSource.YAHOO, symbol: 'AAPL' }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + const activities = await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }), + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-02T00:00:00.000Z', + symbol: 'AAPL', + type: 'FEE' + }) + ] + }); + + expect( + createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create + ).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' }); + + expect(getAssetProfileIdentifiers(activities)).toEqual([ + { dataSource: DataSource.YAHOO, symbol: 'AAPL' }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + }); + + it.each(NON_INVESTMENT_ACTIVITY_TYPES)( + 'refers the next %s activities of a data provider to the custom asset profile which createActivity() has created', + async (type) => { + mockCreatedAssetProfiles([ + { + dataSource: DataSource.MANUAL, + symbol: CUSTOM_ASSET_PROFILE_SYMBOL + }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + await importActivities({ + activitiesDto: [ + createActivityDto({ + type, + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL' + }), + createActivityDto({ + type, + dataSource: DataSource.YAHOO, + date: '2024-01-02T00:00:00.000Z', + symbol: 'AAPL' + }) + ] + }); + + expect( + createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create + ).toMatchObject({ symbol: CUSTOM_ASSET_PROFILE_SYMBOL }); + } + ); + + it('refers the next activities without a data source to the custom asset profile which createActivity() has created', async () => { + mockCreatedAssetProfiles([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + await importActivities({ + activitiesDto: [ + createActivityDto({ + date: '2024-01-01T00:00:00.000Z', + symbol: 'Broker fee', + type: 'FEE' + }), + createActivityDto({ + date: '2024-01-02T00:00:00.000Z', + symbol: 'Broker fee', + type: 'FEE' + }) + ] + }); + + expect( + createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create + ).toMatchObject({ + dataSource: DataSource.MANUAL, + symbol: CUSTOM_ASSET_PROFILE_SYMBOL + }); + }); + + it('refers the next investment activities without a data source to the custom asset profile which createActivity() has created', async () => { + mockCreatedAssetProfiles([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + await importActivities({ + activitiesDto: [ + createActivityDto({ + date: '2024-01-01T00:00:00.000Z', + symbol: 'Gold', + type: 'BUY' + }), + createActivityDto({ + date: '2024-01-02T00:00:00.000Z', + symbol: 'Gold', + type: 'BUY' + }), + createActivityDto({ + date: '2024-01-03T00:00:00.000Z', + symbol: 'Gold', + type: 'SELL' + }) + ] + }); + + expect( + createActivity.mock.calls.slice(1).map(([activity]) => { + return activity.SymbolProfile.connectOrCreate.create; + }) + ).toMatchObject([ + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }, + { dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL } + ]); + + expect(gatherSymbols.mock.calls[0][0].dataGatheringItems).toEqual([ + { + dataSource: DataSource.MANUAL, + date: parseISO('2024-01-01T00:00:00.000Z'), + symbol: CUSTOM_ASSET_PROFILE_SYMBOL + } + ]); + }); + + it('keeps the asset profiles of the activities in a dry run', async () => { + const activities = await importActivities({ + activitiesDto: [ + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-01T00:00:00.000Z', + symbol: 'AAPL', + type: 'FEE' + }), + createActivityDto({ + dataSource: DataSource.YAHOO, + date: '2024-01-02T00:00:00.000Z', + symbol: 'AAPL', + type: 'BUY' + }) + ], + isDryRun: true + }); + + expect(createActivity).not.toHaveBeenCalled(); + expect(gatherSymbols).not.toHaveBeenCalled(); + + expect(getAssetProfileIdentifiers(activities)).toEqual([ + { dataSource: DataSource.YAHOO, symbol: 'AAPL' }, + { dataSource: DataSource.YAHOO, symbol: 'AAPL' } + ]); + }); + }); + + function importActivities({ + activitiesDto, + isDryRun + }: { + activitiesDto: CreateOrderDto[]; + isDryRun?: boolean; + }) { + return importService.import({ + activitiesDto, + isDryRun, + accountsWithBalancesDto: [], + assetProfilesWithMarketDataDto: [], + platformsDto: [], + tagsDto: [], + user: { + id: 'user-id', + permissions: [], + settings: { settings: { baseCurrency: 'USD' } } + } as unknown as UserWithSettings + }); + } + + function mockCreatedAssetProfiles(assetProfiles: AssetProfileIdentifier[]) { + for (const assetProfile of assetProfiles) { + createActivity.mockImplementationOnce( + ({ + date, + type + }: Parameters[0]) => { + return { date, type, SymbolProfile: assetProfile }; + } + ); + } + } +}); + +function createActivityDto({ + dataSource, + date, + symbol, + type +}: Pick) { + return { + dataSource, + date, + symbol, + type, + currency: 'USD', + fee: 0, + quantity: 1, + unitPrice: 100 + } as CreateOrderDto; +} + +function getAssetProfileIdentifiers(activities: Activity[]) { + return activities.map(({ assetProfile: { dataSource, symbol } }) => { + return { dataSource, symbol }; + }); +} diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 8a2c80e2c3..80637ea4d6 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -836,6 +836,10 @@ export class ImportService { const activities: Activity[] = []; + const customAssetProfileSymbols: { + [assetProfileIdentifier: string]: string; + } = {}; + for (const activity of activitiesExtendedWithErrors) { const accountId = activity.accountId; const comment = activity.comment; @@ -848,12 +852,12 @@ export class ImportService { const type = activity.type; const unitPrice = activity.unitPrice; - const assetProfile = assetProfiles[ - getAssetProfileIdentifier({ - dataSource: activity.assetProfile.dataSource, - symbol: activity.assetProfile.symbol - }) - ] ?? { + const assetProfileIdentifier = getAssetProfileIdentifier({ + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol + }); + + let assetProfile = assetProfiles[assetProfileIdentifier] ?? { dataSource: activity.assetProfile.dataSource, symbol: activity.assetProfile.symbol }; @@ -904,7 +908,14 @@ export class ImportService { continue; } - const { dataSource, name, symbol } = assetProfile; + const customAssetProfileSymbol = + NON_INVESTMENT_ACTIVITY_TYPES.includes(type) || + assetProfile.dataSource === DataSource.MANUAL + ? customAssetProfileSymbols[assetProfileIdentifier] + : undefined; + + const { dataSource, name } = assetProfile; + const symbol = customAssetProfileSymbol ?? assetProfile.symbol; const createdActivity = await this.activitiesService.createActivity({ comment, @@ -940,9 +951,15 @@ export class ImportService { userId: user.id }); - if (createdActivity.SymbolProfile?.symbol) { - // Update symbol that may have been assigned in createOrder() - assetProfile.symbol = createdActivity.SymbolProfile.symbol; + if (createdActivity.SymbolProfile.dataSource === DataSource.MANUAL) { + customAssetProfileSymbols[assetProfileIdentifier] = + createdActivity.SymbolProfile.symbol; + + assetProfile = { + ...assetProfile, + dataSource: DataSource.MANUAL, + symbol: createdActivity.SymbolProfile.symbol + }; } order = createdActivity; From 047a2f705b3255417b8487abb301e17548ca34eb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 10:45:19 +0200 Subject: [PATCH 08/10] Feature/liveness probe (#8037) * Setup liveness probe endpoint * Update documentation * Update changelog --- CHANGELOG.md | 4 ++++ README.md | 24 +++++++++++++++++++ apps/api/src/app/health/health.controller.ts | 16 +++++++++---- libs/common/src/lib/interfaces/index.ts | 2 ++ .../liveness-health-response.interface.ts | 3 +++ 5 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b39f92677a..c3280f51ca 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 + +- Extended the _Public API_ with the liveness probe endpoint (`GET api/v1/health/liveness`) (experimental) + ### Changed - Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` diff --git a/README.md b/README.md index b9352e3bd3..59c7c5d2c6 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony ### Health Check (experimental) +The endpoint checks the connection to the database and to _Redis_. It is suitable as the readiness probe of an orchestrator (e.g. `readinessProbe` in _Kubernetes_) and as the `healthcheck` in _Docker Compose_. The check of _Redis_ can take up to 5 seconds. Thus, set the timeout of the probe to at least 5 seconds (e.g. `timeoutSeconds: 5` in _Kubernetes_). + #### Request `GET http://localhost:3333/api/v1/health` @@ -211,6 +213,28 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony } ``` +### Liveness Probe (experimental) + +The endpoint answers without a check of the database or _Redis_. It is suitable as the liveness probe of an orchestrator (e.g. `livenessProbe` in _Kubernetes_), so that an outage of a dependency after the start does not restart the container. The database migrations run before the server starts. Thus, also use the endpoint as the startup probe (e.g. `startupProbe` in _Kubernetes_), so that a long migration does not restart the container. + +#### Request + +`GET http://localhost:3333/api/v1/health/liveness` + +**Info:** No Bearer Token is required for liveness probe + +#### Response + +##### Success + +`200 OK` + +``` +{ + "status": "OK" +} +``` + ### Import Portfolio Data #### Prerequisites diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts index 4f88a03f0c..32aae38e3c 100644 --- a/apps/api/src/app/health/health.controller.ts +++ b/apps/api/src/app/health/health.controller.ts @@ -3,7 +3,8 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce import { AiServiceHealthResponse, DataEnhancerHealthResponse, - DataProviderHealthResponse + DataProviderHealthResponse, + LivenessHealthResponse } from '@ghostfolio/common/interfaces'; import { @@ -33,9 +34,11 @@ export class HealthController { @Get() public async getHealth(@Res() response: Response) { - const databaseServiceHealthy = await this.healthService.isDatabaseHealthy(); - const redisCacheServiceHealthy = - await this.healthService.isRedisCacheHealthy(); + const [databaseServiceHealthy, redisCacheServiceHealthy] = + await Promise.all([ + this.healthService.isDatabaseHealthy(), + this.healthService.isRedisCacheHealthy() + ]); if (databaseServiceHealthy && redisCacheServiceHealthy) { return response @@ -116,4 +119,9 @@ export class HealthController { .json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) }); } } + + @Get('liveness') + public getLiveness(): LivenessHealthResponse { + return { status: getReasonPhrase(StatusCodes.OK) }; + } } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index c581b8e56e..113e485b74 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -68,6 +68,7 @@ import type { ExportResponse } from './responses/export-response.interface'; import type { HistoricalResponse } from './responses/historical-response.interface'; import type { ImportResponse } from './responses/import-response.interface'; import type { InfoResponse } from './responses/info-response.interface'; +import type { LivenessHealthResponse } from './responses/liveness-health-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface'; import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface'; @@ -156,6 +157,7 @@ export { InfoResponse, InvestmentItem, LineChartItem, + LivenessHealthResponse, LookupItem, LookupResponse, MarketData, diff --git a/libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts b/libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts new file mode 100644 index 0000000000..28db1fb5b3 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts @@ -0,0 +1,3 @@ +export interface LivenessHealthResponse { + status: string; +} From 75576266bb6fe193c841051cf6998466426e4704 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:51:11 +0200 Subject: [PATCH 09/10] Task/use npm ci in Dockerfile (#8039) * Use npm ci * Update changelog --- CHANGELOG.md | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3280f51ca..6638c81f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Changed the installation of the dependencies from `npm install` to `npm ci` in the `Dockerfile` - Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` - Upgraded `nestjs` from version `11.2.3` to `11.2.6` diff --git a/Dockerfile b/Dockerfile index e73cd73e65..ebee4e60d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ COPY ./package.json package.json COPY ./package-lock.json package-lock.json COPY ./prisma/schema.prisma prisma/ -RUN npm install +RUN npm ci COPY ./apps apps/ COPY ./libs libs/ @@ -39,7 +39,7 @@ WORKDIR /ghostfolio/dist/apps/api # package-lock.json needs to be used to ensure the same versions COPY ./package-lock.json /ghostfolio/dist/apps/api/ -RUN npm install +RUN npm ci COPY .config /ghostfolio/dist/apps/api/.config/ COPY prisma /ghostfolio/dist/apps/api/prisma/ From b86034108a8ac22d32fc1c8835a3df8443cc23f5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:53:56 +0200 Subject: [PATCH 10/10] Feature/graceful shutdown (#8035) * Graceful shutdown of the server * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/main.ts | 9 +++++++++ apps/api/src/services/prisma/prisma.service.ts | 12 ++++++------ docker/docker-compose.yml | 1 + 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6638c81f4d..a9ffe2149d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Extended the _Public API_ with the liveness probe endpoint (`GET api/v1/health/liveness`) (experimental) +- Added the graceful shutdown of the server on `SIGINT` and `SIGTERM` +- Added `stop_grace_period` to the _Ghostfolio_ service in the `docker-compose` file (`docker-compose.yml`) ### Changed @@ -21,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the asset profile of the activities after a fee, an interest or a liability with the same symbol in the activities import +### Todo + +- Add `stop_grace_period: 1m` to the _Ghostfolio_ service in your `docker-compose` file (see `docker-compose.yml`) + ## 3.78.0 - 2026-10-03 ### Added diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 8b17cbca50..1282304c33 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -14,6 +14,7 @@ import { import { Logger, LogLevel, + ShutdownSignal, ValidationPipe, VersioningType } from '@nestjs/common'; @@ -65,6 +66,14 @@ async function bootstrap() { }); app.enableCors(); + + // Let the open requests and the active queue jobs finish on SIGINT and + // SIGTERM before the process stops. Exit explicitly because Linux ignores + // the re-raised signal if the process runs as PID 1 + app.enableShutdownHooks([ShutdownSignal.SIGINT, ShutdownSignal.SIGTERM], { + useProcessExit: true + }); + app.enableVersioning({ defaultVersion: '1', type: VersioningType.URI diff --git a/apps/api/src/services/prisma/prisma.service.ts b/apps/api/src/services/prisma/prisma.service.ts index df55e45f7c..fcf7c6e5ea 100644 --- a/apps/api/src/services/prisma/prisma.service.ts +++ b/apps/api/src/services/prisma/prisma.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger, LogLevel, - OnModuleDestroy, + OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -12,7 +12,7 @@ import { Prisma, PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient - implements OnModuleInit, OnModuleDestroy + implements OnApplicationShutdown, OnModuleInit { private readonly logger = new Logger(PrismaService.name); @@ -41,6 +41,10 @@ export class PrismaService }); } + public async onApplicationShutdown() { + await this.$disconnect(); + } + public async onModuleInit() { try { await this.$connect(); @@ -48,8 +52,4 @@ export class PrismaService this.logger.error(error); } } - - public async onModuleDestroy() { - await this.$disconnect(); - } } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8c69e5420e..cf58ea0143 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,6 +5,7 @@ services: container_name: ghostfolio restart: unless-stopped init: true + stop_grace_period: 1m cap_drop: - ALL security_opt: