diff --git a/src/modules/wallets/__tests__/wallet-holdings.integration.test.ts b/src/modules/wallets/__tests__/wallet-holdings.integration.test.ts index 51b3e90..f813f6a 100644 --- a/src/modules/wallets/__tests__/wallet-holdings.integration.test.ts +++ b/src/modules/wallets/__tests__/wallet-holdings.integration.test.ts @@ -104,6 +104,36 @@ describe('GET /wallets/:address/holdings', () => { }); }); + it('returns a holding with zero value when its creator has no price snapshot', async () => { + const holding = makeHolding({ + creator_id: 'new-creator', + creator_handle: 'new-creator-handle', + key_count: '5', + current_price: null, + total_value: '0', + }); + jest + .spyOn(walletHoldingsService, 'fetchWalletHoldings') + .mockResolvedValue([[holding], 1]); + + const req = makeReq({ address: VALID_ADDRESS }); + const res = makeRes(); + const next = makeNext(); + await httpGetWalletHoldings(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + const body = res.json.mock.calls[0][0]; + expect(body.data.items).toContainEqual( + expect.objectContaining({ + creator_id: 'new-creator', + key_count: '5', + current_price: null, + total_value: '0', + }) + ); + }); + it('returns 200 with empty items for a wallet with no holdings', async () => { jest .spyOn(walletHoldingsService, 'fetchWalletHoldings') diff --git a/src/modules/wallets/wallet-holdings-price-snapshot.integration.test.ts b/src/modules/wallets/wallet-holdings-price-snapshot.integration.test.ts index ec542b7..0ec2e89 100644 --- a/src/modules/wallets/wallet-holdings-price-snapshot.integration.test.ts +++ b/src/modules/wallets/wallet-holdings-price-snapshot.integration.test.ts @@ -1,7 +1,7 @@ // Integration test: wallet holdings total value recalculated after price snapshot update (#470) // // Covers: total_value and current_price reflect the current price snapshot, -// both update correctly when the snapshot price changes, null when no snapshot exists, +// both update correctly when the snapshot price changes, zero value when no snapshot exists, // and multi-holding aggregation is correct. // Uses Jest mocks — no database required. @@ -83,13 +83,14 @@ describe('Holdings total_value recalculated after price snapshot update', () => expect(updatedItems[0].total_value).not.toBe(initialTotalValue); }); - it('current_price and total_value are null when no snapshot exists for the creator', async () => { + it('returns zero total_value when no snapshot exists for the creator while preserving quantity', async () => { mockPrisma.creatorPriceSnapshot.findMany.mockResolvedValue([]); const [items] = await fetchWalletHoldings(WALLET_ADDRESS); expect(items[0].current_price).toBeNull(); - expect(items[0].total_value).toBeNull(); + expect(items[0].total_value).toBe('0'); + expect(items[0].key_count).toBe('5'); }); it('total_value is computed per-holding when wallet has multiple holdings', async () => { diff --git a/src/modules/wallets/wallet-holdings.service.ts b/src/modules/wallets/wallet-holdings.service.ts index 05eedcd..30b7fbc 100644 --- a/src/modules/wallets/wallet-holdings.service.ts +++ b/src/modules/wallets/wallet-holdings.service.ts @@ -74,9 +74,11 @@ export async function fetchWalletHoldings( const rawPrice = priceMap.get(row.creatorId) ?? null; const currentPrice = rawPrice !== null ? rawPrice.toString() : null; const totalValue = - rawPrice !== null && row.balance !== null - ? (Number(row.balance) * Number(rawPrice)).toString() - : null; + rawPrice === null + ? '0' + : row.balance !== null + ? (Number(row.balance) * Number(rawPrice)).toString() + : null; return { creator_id: row.creatorId, creator_handle: handleMap.get(row.creatorId) ?? null,