mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Fix repeated historical market data gathering on weekends * Update changelogpull/7708/head^2
committed by
GitHub
12 changed files with 707 additions and 72 deletions
@ -0,0 +1,118 @@ |
|||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { MarketDataService } from './market-data.service'; |
||||
|
|
||||
|
describe('MarketDataService', () => { |
||||
|
let marketDataService: MarketDataService; |
||||
|
let prismaService: { |
||||
|
$transaction: jest.Mock; |
||||
|
marketData: { |
||||
|
createMany: jest.Mock; |
||||
|
deleteMany: jest.Mock; |
||||
|
upsert: jest.Mock; |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
prismaService = { |
||||
|
$transaction: jest.fn(), |
||||
|
marketData: { |
||||
|
createMany: jest.fn(), |
||||
|
deleteMany: jest.fn(), |
||||
|
upsert: jest.fn().mockResolvedValue({}) |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
marketDataService = new MarketDataService(prismaService as any); |
||||
|
}); |
||||
|
|
||||
|
describe('updateMany', () => { |
||||
|
it('does not drop isCarriedForward', async () => { |
||||
|
prismaService.$transaction.mockImplementation((promises) => { |
||||
|
return Promise.all(promises); |
||||
|
}); |
||||
|
|
||||
|
await marketDataService.updateMany({ |
||||
|
data: [ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-22'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 100, |
||||
|
state: 'CLOSE', |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
expect(prismaService.marketData.upsert).toHaveBeenCalledWith( |
||||
|
expect.objectContaining({ |
||||
|
create: expect.objectContaining({ isCarriedForward: true }), |
||||
|
update: expect.objectContaining({ isCarriedForward: true }) |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('resets isCarriedForward if it is omitted by the caller', async () => { |
||||
|
prismaService.$transaction.mockImplementation((promises) => { |
||||
|
return Promise.all(promises); |
||||
|
}); |
||||
|
|
||||
|
await marketDataService.updateMany({ |
||||
|
data: [ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-22'), |
||||
|
marketPrice: 100, |
||||
|
state: 'CLOSE', |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
expect(prismaService.marketData.upsert).toHaveBeenCalledWith( |
||||
|
expect.objectContaining({ |
||||
|
update: expect.objectContaining({ isCarriedForward: false }) |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('replaceForSymbol', () => { |
||||
|
it('does not drop isCarriedForward', async () => { |
||||
|
prismaService.$transaction.mockImplementation((callback) => { |
||||
|
return callback(prismaService); |
||||
|
}); |
||||
|
|
||||
|
await marketDataService.replaceForSymbol({ |
||||
|
data: [ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-21'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 100, |
||||
|
state: 'CLOSE', |
||||
|
symbol: 'AAPL' |
||||
|
}, |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-22'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 100, |
||||
|
state: 'CLOSE', |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
], |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL' |
||||
|
}); |
||||
|
|
||||
|
const { data } = prismaService.marketData.createMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data).toEqual([ |
||||
|
expect.objectContaining({ isCarriedForward: false }), |
||||
|
expect.objectContaining({ isCarriedForward: true }) |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,266 @@ |
|||||
|
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; |
||||
|
import { |
||||
|
getAssetProfileIdentifier, |
||||
|
parseDate |
||||
|
} from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { DataSource } from '@prisma/client'; |
||||
|
import { Job } from 'bull'; |
||||
|
|
||||
|
import { DataGatheringProcessor } from './data-gathering.processor'; |
||||
|
|
||||
|
describe('DataGatheringProcessor', () => { |
||||
|
let dataGatheringProcessor: DataGatheringProcessor; |
||||
|
let dataProviderService: { getHistoricalRaw: jest.Mock }; |
||||
|
let marketDataService: { replaceForSymbol: jest.Mock; updateMany: jest.Mock }; |
||||
|
|
||||
|
const createJob = ({ |
||||
|
dataSource, |
||||
|
date, |
||||
|
symbol |
||||
|
}: { |
||||
|
dataSource: DataSource; |
||||
|
date: string; |
||||
|
symbol: string; |
||||
|
}) => { |
||||
|
return { |
||||
|
data: { |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
date: parseDate(date).toISOString() |
||||
|
} |
||||
|
} as unknown as Job<DataGatheringItem>; |
||||
|
}; |
||||
|
|
||||
|
const mockHistoricalData = ({ |
||||
|
dataSource, |
||||
|
prices, |
||||
|
symbol |
||||
|
}: { |
||||
|
dataSource: DataSource; |
||||
|
prices: { [date: string]: number }; |
||||
|
symbol: string; |
||||
|
}) => { |
||||
|
const assetProfileIdentifier = getAssetProfileIdentifier({ |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}); |
||||
|
|
||||
|
const historicalData: { |
||||
|
[symbol: string]: { [date: string]: { marketPrice: number } }; |
||||
|
} = { [assetProfileIdentifier]: {} }; |
||||
|
|
||||
|
for (const [date, marketPrice] of Object.entries(prices)) { |
||||
|
historicalData[assetProfileIdentifier][date] = { marketPrice }; |
||||
|
} |
||||
|
|
||||
|
dataProviderService.getHistoricalRaw.mockResolvedValue(historicalData); |
||||
|
}; |
||||
|
|
||||
|
beforeAll(() => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-24').getTime()); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
dataProviderService = { getHistoricalRaw: jest.fn() }; |
||||
|
marketDataService = { |
||||
|
replaceForSymbol: jest.fn(), |
||||
|
updateMany: jest.fn() |
||||
|
}; |
||||
|
|
||||
|
dataGatheringProcessor = new DataGatheringProcessor( |
||||
|
null, |
||||
|
dataProviderService as any, |
||||
|
marketDataService as any, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
afterAll(() => { |
||||
|
jest.useRealTimers(); |
||||
|
}); |
||||
|
|
||||
|
it('writes an all-real series without carried forward market prices', async () => { |
||||
|
mockHistoricalData({ |
||||
|
dataSource: 'COINGECKO', |
||||
|
symbol: 'bitcoin', |
||||
|
prices: { |
||||
|
'2026-08-17': 1, |
||||
|
'2026-08-18': 2, |
||||
|
'2026-08-19': 3, |
||||
|
'2026-08-20': 4, |
||||
|
'2026-08-21': 5, |
||||
|
'2026-08-22': 6, |
||||
|
'2026-08-23': 7 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringProcessor.gatherHistoricalMarketData( |
||||
|
createJob({ |
||||
|
dataSource: 'COINGECKO', |
||||
|
date: '2026-08-17', |
||||
|
symbol: 'bitcoin' |
||||
|
}) |
||||
|
); |
||||
|
|
||||
|
const { data } = marketDataService.updateMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data).toHaveLength(7); |
||||
|
expect( |
||||
|
data.every(({ isCarriedForward }) => { |
||||
|
return isCarriedForward === false; |
||||
|
}) |
||||
|
).toBe(true); |
||||
|
}); |
||||
|
|
||||
|
it('fills an interior gap with carried forward market prices', async () => { |
||||
|
mockHistoricalData({ |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL', |
||||
|
prices: { |
||||
|
'2026-08-17': 1, |
||||
|
'2026-08-18': 2, |
||||
|
'2026-08-21': 5, |
||||
|
'2026-08-22': 6, |
||||
|
'2026-08-23': 7 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringProcessor.gatherHistoricalMarketData( |
||||
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' }) |
||||
|
); |
||||
|
|
||||
|
const { data } = marketDataService.updateMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data).toEqual([ |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-17'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 1 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-18'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 2 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-19'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 2 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-20'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 2 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-21'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 5 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-22'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 6 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-23'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 7 |
||||
|
}) |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('fills a trailing gap with carried forward market prices', async () => { |
||||
|
mockHistoricalData({ |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL', |
||||
|
prices: { |
||||
|
'2026-08-17': 1, |
||||
|
'2026-08-18': 2, |
||||
|
'2026-08-19': 3, |
||||
|
'2026-08-20': 4, |
||||
|
'2026-08-21': 5 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringProcessor.gatherHistoricalMarketData( |
||||
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' }) |
||||
|
); |
||||
|
|
||||
|
const { data } = marketDataService.updateMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data).toHaveLength(7); |
||||
|
expect(data.slice(5)).toEqual([ |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-22'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 5 |
||||
|
}), |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-23'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 5 |
||||
|
}) |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('does not fill a leading gap', async () => { |
||||
|
mockHistoricalData({ |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL', |
||||
|
prices: { |
||||
|
'2026-08-19': 3, |
||||
|
'2026-08-20': 4, |
||||
|
'2026-08-21': 5, |
||||
|
'2026-08-22': 6, |
||||
|
'2026-08-23': 7 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringProcessor.gatherHistoricalMarketData( |
||||
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' }) |
||||
|
); |
||||
|
|
||||
|
const { data } = marketDataService.updateMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data).toHaveLength(5); |
||||
|
expect(data[0]).toEqual( |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-19'), |
||||
|
isCarriedForward: false, |
||||
|
marketPrice: 3 |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('labels a market price of 0 from the data provider as carried forward', async () => { |
||||
|
mockHistoricalData({ |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL', |
||||
|
prices: { |
||||
|
'2026-08-17': 1, |
||||
|
'2026-08-18': 2, |
||||
|
'2026-08-19': 0, |
||||
|
'2026-08-20': 4, |
||||
|
'2026-08-21': 5, |
||||
|
'2026-08-22': 6, |
||||
|
'2026-08-23': 7 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringProcessor.gatherHistoricalMarketData( |
||||
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' }) |
||||
|
); |
||||
|
|
||||
|
const { data } = marketDataService.updateMany.mock.calls[0][0]; |
||||
|
|
||||
|
expect(data[2]).toEqual( |
||||
|
expect.objectContaining({ |
||||
|
date: parseDate('2026-08-19'), |
||||
|
isCarriedForward: true, |
||||
|
marketPrice: 2 |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,223 @@ |
|||||
|
import { |
||||
|
DATA_GATHERING_QUEUE_PRIORITY_HIGH, |
||||
|
GATHER_HISTORICAL_MARKET_DATA_COOLDOWN_IN_MS |
||||
|
} from '@ghostfolio/common/config'; |
||||
|
import { parseDate } from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { DataGatheringService } from './data-gathering.service'; |
||||
|
|
||||
|
describe('DataGatheringService', () => { |
||||
|
let dataGatheringQueue: { addBulk: jest.Mock; clean: jest.Mock }; |
||||
|
let dataGatheringService: DataGatheringService; |
||||
|
let dataProviderService: { getHistoricalRaw: jest.Mock }; |
||||
|
let prismaService: { marketData: { groupBy: jest.Mock; upsert: jest.Mock } }; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
dataGatheringQueue = { |
||||
|
addBulk: jest.fn().mockResolvedValue([]), |
||||
|
clean: jest.fn().mockResolvedValue([]) |
||||
|
}; |
||||
|
dataProviderService = { getHistoricalRaw: jest.fn() }; |
||||
|
prismaService = { |
||||
|
marketData: { |
||||
|
groupBy: jest.fn().mockResolvedValue([]), |
||||
|
upsert: jest.fn().mockResolvedValue({}) |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
dataGatheringService = new DataGatheringService( |
||||
|
null, |
||||
|
dataGatheringQueue as any, |
||||
|
dataProviderService as any, |
||||
|
null, |
||||
|
null, |
||||
|
prismaService as any, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
jest.useRealTimers(); |
||||
|
}); |
||||
|
|
||||
|
describe('getAssetProfileIdentifiersWithRecentMarketData', () => { |
||||
|
it('excludes carried forward market prices from the query', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-23').getTime()); |
||||
|
|
||||
|
await dataGatheringService[ |
||||
|
'getAssetProfileIdentifiersWithRecentMarketData' |
||||
|
](); |
||||
|
|
||||
|
expect(prismaService.marketData.groupBy).toHaveBeenCalledWith( |
||||
|
expect.objectContaining({ |
||||
|
where: expect.objectContaining({ |
||||
|
isCarriedForward: false, |
||||
|
state: 'CLOSE' |
||||
|
}) |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('keeps a cryptocurrency with a real market price of yesterday on Sunday', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-23').getTime()); |
||||
|
|
||||
|
prismaService.marketData.groupBy.mockResolvedValue([ |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-22') }, |
||||
|
dataSource: 'COINGECKO', |
||||
|
symbol: 'bitcoin' |
||||
|
}, |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-21') }, |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
const assetProfileIdentifiers = |
||||
|
await dataGatheringService[ |
||||
|
'getAssetProfileIdentifiersWithRecentMarketData' |
||||
|
](); |
||||
|
|
||||
|
expect(assetProfileIdentifiers).toEqual([ |
||||
|
{ dataSource: 'COINGECKO', symbol: 'bitcoin' } |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('drops a stock with a real market price of Friday on Monday', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-24').getTime()); |
||||
|
|
||||
|
prismaService.marketData.groupBy.mockResolvedValue([ |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-23') }, |
||||
|
dataSource: 'COINGECKO', |
||||
|
symbol: 'bitcoin' |
||||
|
}, |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-21') }, |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
const assetProfileIdentifiers = |
||||
|
await dataGatheringService[ |
||||
|
'getAssetProfileIdentifiersWithRecentMarketData' |
||||
|
](); |
||||
|
|
||||
|
expect(assetProfileIdentifiers).toEqual([ |
||||
|
{ dataSource: 'COINGECKO', symbol: 'bitcoin' } |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('drops a stock with a late Friday close on Saturday', async () => { |
||||
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-22').getTime()); |
||||
|
|
||||
|
prismaService.marketData.groupBy.mockResolvedValue([ |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-20') }, |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'AAPL' |
||||
|
}, |
||||
|
{ |
||||
|
_max: { date: parseDate('2026-08-21') }, |
||||
|
dataSource: 'YAHOO', |
||||
|
symbol: 'MSFT' |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
const assetProfileIdentifiers = |
||||
|
await dataGatheringService[ |
||||
|
'getAssetProfileIdentifiersWithRecentMarketData' |
||||
|
](); |
||||
|
|
||||
|
expect(assetProfileIdentifiers).toEqual([ |
||||
|
{ dataSource: 'YAHOO', symbol: 'MSFT' } |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('gatherRecentMarketData', () => { |
||||
|
it('expires completed jobs which are older than the cooldown', async () => { |
||||
|
jest |
||||
|
.spyOn(dataGatheringService as any, 'getCurrencies7D') |
||||
|
.mockResolvedValue([]); |
||||
|
jest |
||||
|
.spyOn(dataGatheringService as any, 'getSymbols7D') |
||||
|
.mockResolvedValue([]); |
||||
|
|
||||
|
await dataGatheringService.gatherRecentMarketData(); |
||||
|
|
||||
|
expect(dataGatheringQueue.clean).toHaveBeenCalledWith( |
||||
|
GATHER_HISTORICAL_MARKET_DATA_COOLDOWN_IN_MS, |
||||
|
'completed' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('retains its completed jobs for the duration of the cooldown', async () => { |
||||
|
jest |
||||
|
.spyOn(dataGatheringService as any, 'getCurrencies7D') |
||||
|
.mockResolvedValue([ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-01'), |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
]); |
||||
|
jest |
||||
|
.spyOn(dataGatheringService as any, 'getSymbols7D') |
||||
|
.mockResolvedValue([]); |
||||
|
|
||||
|
await dataGatheringService.gatherRecentMarketData(); |
||||
|
|
||||
|
const [jobs] = dataGatheringQueue.addBulk.mock.calls[0]; |
||||
|
|
||||
|
expect(jobs[0].opts.removeOnComplete).toEqual({ |
||||
|
age: GATHER_HISTORICAL_MARKET_DATA_COOLDOWN_IN_MS / 1000 |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('gatherSymbols', () => { |
||||
|
it('does not apply the cooldown to a manually triggered gathering', async () => { |
||||
|
await dataGatheringService.gatherSymbols({ |
||||
|
dataGatheringItems: [ |
||||
|
{ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-01'), |
||||
|
symbol: 'AAPL' |
||||
|
} |
||||
|
], |
||||
|
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH |
||||
|
}); |
||||
|
|
||||
|
const [jobs] = dataGatheringQueue.addBulk.mock.calls[0]; |
||||
|
|
||||
|
expect(jobs[0].opts.removeOnComplete).toBe(true); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('gatherSymbolForDate', () => { |
||||
|
it('resets isCarriedForward on a previously carried forward market price', async () => { |
||||
|
dataProviderService.getHistoricalRaw.mockResolvedValue({ |
||||
|
'YAHOO-AAPL': { |
||||
|
'2026-08-22': { marketPrice: 100 } |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
await dataGatheringService.gatherSymbolForDate({ |
||||
|
dataSource: 'YAHOO', |
||||
|
date: parseDate('2026-08-22'), |
||||
|
symbol: 'AAPL' |
||||
|
}); |
||||
|
|
||||
|
expect(prismaService.marketData.upsert).toHaveBeenCalledWith( |
||||
|
expect.objectContaining({ |
||||
|
create: expect.objectContaining({ isCarriedForward: false }), |
||||
|
update: { marketPrice: 100, isCarriedForward: false } |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,2 @@ |
|||||
|
-- AlterTable |
||||
|
ALTER TABLE "MarketData" ADD COLUMN "isCarriedForward" BOOLEAN NOT NULL DEFAULT false; |
||||
Loading…
Reference in new issue