mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
Resolves #6291 The `POST api/v1/import` endpoint was missing the `valueInBaseCurrency` field in the activity response. Added `ExchangeRateDataService` to calculate this value using the same pattern as `OrderService.getOrders()`, converting the activity value from its currency to the user's base currency at the activity's date. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>pull/6294/head
3 changed files with 221 additions and 0 deletions
@ -0,0 +1,210 @@ |
|||
import { ImportService } from './import.service'; |
|||
|
|||
describe('ImportService', () => { |
|||
let importService: ImportService; |
|||
|
|||
let exchangeRateDataServiceMock: { toCurrencyAtDate: jest.Mock }; |
|||
let dataProviderServiceMock: { |
|||
getDataSourceForImport: jest.Mock; |
|||
getDataSources: jest.Mock; |
|||
getAssetProfiles: jest.Mock; |
|||
}; |
|||
let orderServiceMock: { getOrders: jest.Mock }; |
|||
let accountServiceMock: { getAccounts: jest.Mock }; |
|||
let tagServiceMock: { getTagsForUser: jest.Mock }; |
|||
let configurationServiceMock: { get: jest.Mock }; |
|||
|
|||
beforeEach(() => { |
|||
exchangeRateDataServiceMock = { |
|||
toCurrencyAtDate: jest.fn() |
|||
}; |
|||
|
|||
dataProviderServiceMock = { |
|||
getDataSourceForImport: jest.fn().mockReturnValue('YAHOO'), |
|||
getDataSources: jest.fn().mockResolvedValue(['MANUAL', 'YAHOO']), |
|||
getAssetProfiles: jest.fn() |
|||
}; |
|||
|
|||
orderServiceMock = { |
|||
getOrders: jest.fn().mockResolvedValue({ activities: [], count: 0 }) |
|||
}; |
|||
|
|||
accountServiceMock = { |
|||
getAccounts: jest.fn().mockResolvedValue([]) |
|||
}; |
|||
|
|||
tagServiceMock = { |
|||
getTagsForUser: jest.fn().mockResolvedValue([]) |
|||
}; |
|||
|
|||
configurationServiceMock = { |
|||
get: jest.fn().mockImplementation((key: string) => { |
|||
if (key === 'ENABLE_FEATURE_SUBSCRIPTION') { |
|||
return false; |
|||
} |
|||
|
|||
if (key === 'MAX_ACTIVITIES_TO_IMPORT') { |
|||
return Number.MAX_SAFE_INTEGER; |
|||
} |
|||
|
|||
return undefined; |
|||
}) |
|||
}; |
|||
|
|||
importService = new ImportService( |
|||
accountServiceMock as any, |
|||
null, |
|||
configurationServiceMock as any, |
|||
null, |
|||
dataProviderServiceMock as any, |
|||
exchangeRateDataServiceMock as any, |
|||
null, |
|||
orderServiceMock as any, |
|||
null, |
|||
null, |
|||
null, |
|||
tagServiceMock as any |
|||
); |
|||
}); |
|||
|
|||
describe('import', () => { |
|||
it('should include valueInBaseCurrency for a dry-run FEE activity', async () => { |
|||
const expectedValueInBaseCurrency = 15; |
|||
|
|||
exchangeRateDataServiceMock.toCurrencyAtDate.mockResolvedValue( |
|||
expectedValueInBaseCurrency |
|||
); |
|||
|
|||
const activities = await importService.import({ |
|||
isDryRun: true, |
|||
maxActivitiesToImport: Number.MAX_SAFE_INTEGER, |
|||
accountsWithBalancesDto: [], |
|||
activitiesDto: [ |
|||
{ |
|||
currency: 'USD', |
|||
dataSource: 'MANUAL', |
|||
date: '2024-01-15T00:00:00.000Z', |
|||
fee: 0, |
|||
quantity: 1, |
|||
symbol: 'Account Opening Fee', |
|||
type: 'FEE', |
|||
unitPrice: 15 |
|||
} as any |
|||
], |
|||
assetProfilesWithMarketDataDto: [], |
|||
tagsDto: [], |
|||
user: { |
|||
id: 'test-user-id', |
|||
settings: { settings: { baseCurrency: 'USD' } } |
|||
} as any |
|||
}); |
|||
|
|||
expect(activities).toHaveLength(1); |
|||
expect(activities[0].valueInBaseCurrency).toBe( |
|||
expectedValueInBaseCurrency |
|||
); |
|||
expect(activities[0].value).toBe(15); |
|||
expect(exchangeRateDataServiceMock.toCurrencyAtDate).toHaveBeenCalledWith( |
|||
15, |
|||
'USD', |
|||
'USD', |
|||
expect.any(Date) |
|||
); |
|||
}); |
|||
|
|||
it('should convert valueInBaseCurrency using the correct currencies', async () => { |
|||
exchangeRateDataServiceMock.toCurrencyAtDate.mockResolvedValue(1350.5); |
|||
|
|||
dataProviderServiceMock.getAssetProfiles.mockResolvedValue({ |
|||
MSFT: { |
|||
currency: 'USD', |
|||
dataSource: 'YAHOO', |
|||
name: 'Microsoft Corporation', |
|||
symbol: 'MSFT' |
|||
} |
|||
}); |
|||
|
|||
const activities = await importService.import({ |
|||
isDryRun: true, |
|||
maxActivitiesToImport: Number.MAX_SAFE_INTEGER, |
|||
accountsWithBalancesDto: [], |
|||
activitiesDto: [ |
|||
{ |
|||
currency: 'USD', |
|||
dataSource: 'YAHOO', |
|||
date: '2024-01-15T00:00:00.000Z', |
|||
fee: 19, |
|||
quantity: 5, |
|||
symbol: 'MSFT', |
|||
type: 'BUY', |
|||
unitPrice: 298.58 |
|||
} as any |
|||
], |
|||
assetProfilesWithMarketDataDto: [], |
|||
tagsDto: [], |
|||
user: { |
|||
id: 'test-user-id', |
|||
settings: { settings: { baseCurrency: 'EUR' } } |
|||
} as any |
|||
}); |
|||
|
|||
expect(activities).toHaveLength(1); |
|||
expect(activities[0].valueInBaseCurrency).toBe(1350.5); |
|||
expect(activities[0].value).toBeCloseTo(1492.9); |
|||
expect(exchangeRateDataServiceMock.toCurrencyAtDate).toHaveBeenCalledWith( |
|||
expect.closeTo(1492.9), |
|||
'USD', |
|||
'EUR', |
|||
expect.any(Date) |
|||
); |
|||
}); |
|||
|
|||
it('should fall back to asset profile currency when activity currency is not set', async () => { |
|||
exchangeRateDataServiceMock.toCurrencyAtDate.mockResolvedValue(450); |
|||
|
|||
dataProviderServiceMock.getAssetProfiles.mockResolvedValue({ |
|||
AAPL: { |
|||
currency: 'EUR', |
|||
dataSource: 'YAHOO', |
|||
name: 'Apple Inc.', |
|||
symbol: 'AAPL' |
|||
} |
|||
}); |
|||
|
|||
const activities = await importService.import({ |
|||
isDryRun: true, |
|||
maxActivitiesToImport: Number.MAX_SAFE_INTEGER, |
|||
accountsWithBalancesDto: [], |
|||
activitiesDto: [ |
|||
{ |
|||
currency: undefined, |
|||
dataSource: 'YAHOO', |
|||
date: '2024-06-01T00:00:00.000Z', |
|||
fee: 0, |
|||
quantity: 2, |
|||
symbol: 'AAPL', |
|||
type: 'BUY', |
|||
unitPrice: 250 |
|||
} as any |
|||
], |
|||
assetProfilesWithMarketDataDto: [], |
|||
tagsDto: [], |
|||
user: { |
|||
id: 'test-user-id', |
|||
settings: { settings: { baseCurrency: 'CHF' } } |
|||
} as any |
|||
}); |
|||
|
|||
expect(activities).toHaveLength(1); |
|||
expect(activities[0].valueInBaseCurrency).toBe(450); |
|||
|
|||
// Should fall back to asset profile currency (EUR) when activity currency is undefined
|
|||
expect(exchangeRateDataServiceMock.toCurrencyAtDate).toHaveBeenCalledWith( |
|||
500, |
|||
'EUR', |
|||
'CHF', |
|||
expect.any(Date) |
|||
); |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue