Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/modules/wallets/__tests__/wallet-holdings.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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 () => {
Expand Down
8 changes: 5 additions & 3 deletions src/modules/wallets/wallet-holdings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading