From 727cb4ba5fa1cf370ebca0bd38ffd9c7d1d7b503 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:34:01 +0200 Subject: [PATCH] Remove Premium check from matching of assets --- .../src/app/endpoints/ai/ai.service.spec.ts | 57 +------------------ apps/api/src/app/endpoints/ai/ai.service.ts | 57 ++++++------------- 2 files changed, 19 insertions(+), 95 deletions(-) diff --git a/apps/api/src/app/endpoints/ai/ai.service.spec.ts b/apps/api/src/app/endpoints/ai/ai.service.spec.ts index 69636a19fb..bc9f6cd6ce 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.spec.ts @@ -12,7 +12,6 @@ import { PROPERTY_OPENROUTER_MODEL_AGENT, PROPERTY_OPENROUTER_MODEL_VISION } from '@ghostfolio/common/config'; -import { SubscriptionType } from '@ghostfolio/common/enums'; import { isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; import type { UserWithSettings } from '@ghostfolio/common/types'; @@ -100,8 +99,7 @@ describe('AiService', () => { dataProviderService = { getDataSources: jest .fn() - .mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO]), - isPremiumRequired: jest.fn().mockReturnValue(false) + .mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO]) } as unknown as DataProviderService; mcpService = { @@ -357,8 +355,7 @@ describe('AiService', () => { const user = { accounts: [{ currency: 'CHF', id: 'account-id', name: 'Brokerage' }], id: 'user-id', - settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } }, - subscription: { type: SubscriptionType.Basic } + settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } } } as unknown as UserWithSettings; it('Reads the rows of the file and matches them with the accounts, the base currency and the id of the user', async () => { @@ -402,7 +399,6 @@ describe('AiService', () => { rows, accounts: user.accounts, broker: 'Swissquote', - subscription: user.subscription, userCurrency: 'EUR', userId: 'user-id' }); @@ -975,55 +971,6 @@ describe('AiService', () => { ], rowsWithoutMatch: [] }); - - expect(dataProviderService.isPremiumRequired).toHaveBeenCalledWith({ - dataSource: DataSource.YAHOO, - subscription: undefined - }); - }); - - it('Passes no holding with a data source which requires Premium to the model and gives a row whose asset match is such a holding as a row without a match', async () => { - jest.spyOn(Logger.prototype, 'warn').mockImplementation(); - - jest.mocked(dataProviderService.isPremiumRequired).mockReturnValue(true); - - const row = createActivityRow({ - accountName: null, - isin: 'IE00BK5BQT80', - symbol: null - }); - - const subscription = { - type: SubscriptionType.Basic - } as UserWithSettings['subscription']; - - mockRowMatches({ - assetProfilesOfSearch: [], - assets: [ - { currency: 'USD', dataSource: 'YAHOO', id: 0, symbol: 'VWRA.SW' } - ] - }); - - mockHoldings(); - - await expect( - aiService.getActivitiesFromRows({ - accounts, - subscription, - userCurrency, - userId, - rows: [row] - }) - ).resolves.toEqual({ activities: [], rowsWithoutMatch: [row] }); - - expect(dataProviderService.isPremiumRequired).toHaveBeenCalledWith({ - subscription, - dataSource: DataSource.YAHOO - }); - - const { prompt } = jest.mocked(generateText).mock.calls[0][0]; - - expect(JSON.parse(prompt as string)).toHaveProperty('holdings', []); }); it('Passes no holding with a data source which is not configured to the model and gives a row whose asset match is such a holding as a row without a match', async () => { diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index 516e93ad81..02af5d8d0a 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -124,7 +124,6 @@ export class AiService { broker, rows, accounts: user.accounts, - subscription: user.subscription, userCurrency: user.settings.settings.baseCurrency, userId: user.id }); @@ -152,7 +151,6 @@ export class AiService { accounts, broker, rows, - subscription, userCurrency, userId }: { @@ -160,7 +158,6 @@ export class AiService { accounts: AccountToMatch[]; broker?: string; rows: ActivityRow[]; - subscription?: UserWithSettings['subscription']; userCurrency: string; userId: string; }) { @@ -211,7 +208,6 @@ export class AiService { abortSignal, accountHints, assets, - subscription, userId }) : { accounts: [], assets: [] }; @@ -524,15 +520,9 @@ export class AiService { /** * Gives the holdings of the user with the tool get-portfolio of the server * of the Model Context Protocol, without the holdings whose data source the - * import refuses for the user + * import refuses */ - private async getHoldingsToMatch({ - subscription, - userId - }: { - subscription?: UserWithSettings['subscription']; - userId: string; - }) { + private async getHoldingsToMatch({ userId }: { userId: string }) { const [{ content }, dataSources] = await Promise.all([ this.mcpService.getPortfolio({ userId, withValues: false }), this.dataProviderService.getDataSources() @@ -540,11 +530,7 @@ export class AiService { return this.getHoldingsFromTable(content[0].text).filter( ({ dataSource }) => { - return this.isValidDataSourceForImport({ - dataSource, - dataSources, - subscription - }); + return this.isValidDataSourceForImport({ dataSource, dataSources }); } ); } @@ -608,8 +594,8 @@ export class AiService { * symbol only. The holdings let the model select an asset profile which the * user already has, also if no search gives it, for example as no data * provider finds its ISIN. A holding whose data source the import refuses - * for the user is not given to the model, so that the model selects an - * asset profile of the search instead and the whole import does not fail. + * is not given to the model, so that the model selects an asset profile of + * the search instead and the whole import does not fail. * If get-accounts fails, the model gets no accounts, so that only the * account hints have no match. If get-portfolio or the data sources fail, * the model gets no holdings. The reasoning is disabled, as it can add @@ -625,13 +611,11 @@ export class AiService { abortSignal, accountHints, assets, - subscription, userId }: { abortSignal?: AbortSignal; accountHints: AccountHint[]; assets: AssetToMatch[]; - subscription?: UserWithSettings['subscription']; userId: string; }) { const startTime = performance.now(); @@ -650,7 +634,7 @@ export class AiService { }) : undefined, assets.length > 0 - ? this.getHoldingsToMatch({ subscription, userId }).catch( + ? this.getHoldingsToMatch({ userId }).catch( (error: Error): undefined => { this.logger.warn(`Could not get the holdings: ${error.message}`); @@ -751,31 +735,24 @@ export class AiService { /** * Checks without a request to a data provider if the import takes a data - * source for the user: it is configured and does not require Ghostfolio - * Premium, for example after the Premium subscription has expired. The data - * source is first transformed as POST api/v1/import transforms it. A symbol - * which the data source cannot resolve, for example a delisted one, is not - * found, as this needs a request to the data provider. + * source of a holding: it is configured. The data source is first + * transformed as POST api/v1/import transforms it. A data source which + * requires Ghostfolio Premium is taken, as the import takes it for an asset + * profile which the user already has. A symbol which the data source cannot + * resolve, for example a delisted one, is not found, as this needs a + * request to the data provider. */ private isValidDataSourceForImport({ dataSource, - dataSources, - subscription + dataSources }: { dataSource: string; dataSources: DataSource[]; - subscription?: UserWithSettings['subscription']; }) { - const dataSourceOfImport = transformDataSourceInRequest({ - dataSource, - configurationService: this.configurationService - }); - - return ( - dataSources.includes(dataSourceOfImport) && - !this.dataProviderService.isPremiumRequired({ - subscription, - dataSource: dataSourceOfImport + return dataSources.includes( + transformDataSourceInRequest({ + dataSource, + configurationService: this.configurationService }) ); }