diff --git a/src/modules/ownership/ownership.controllers.ts b/src/modules/ownership/ownership.controllers.ts index 5d59bbf..8d397c5 100644 --- a/src/modules/ownership/ownership.controllers.ts +++ b/src/modules/ownership/ownership.controllers.ts @@ -1,6 +1,7 @@ import { AsyncController } from '../../types/auth.types'; import { OwnershipQuerySchema } from './ownership.schemas'; import { fetchOwnership } from './ownership.service'; +import { calculateTotalPortfolioValue } from './ownership.utils'; import { sendSuccess, sendValidationError } from '../../utils/api-response.utils'; export const httpGetOwnership: AsyncController = async (req, res, next) => { @@ -13,8 +14,19 @@ export const httpGetOwnership: AsyncController = async (req, res, next) => { }))); } - const ownership = await fetchOwnership(parsed.data); - sendSuccess(res, ownership); + const records = await fetchOwnership(parsed.data); + const holdings = records.map(record => ({ + id: record.id, + ownerAddress: record.ownerAddress, + creatorId: record.creatorId, + balance: record.balance.toString(), + currentPrice: '0', + updatedAt: record.updatedAt, + })); + sendSuccess(res, { + holdings, + total_portfolio_value: calculateTotalPortfolioValue(holdings), + }); } catch (error) { next(error); } diff --git a/src/modules/ownership/ownership.schemas.ts b/src/modules/ownership/ownership.schemas.ts index 1a94f8c..26ff998 100644 --- a/src/modules/ownership/ownership.schemas.ts +++ b/src/modules/ownership/ownership.schemas.ts @@ -11,8 +11,22 @@ export const OwnershipItemSchema = z.object({ id: z.string(), ownerAddress: z.string(), creatorId: z.string(), - balance: z.string(), // Decimal is returned as string in Prisma Json/JsonValue but as Decimal object in real types. For API, string is safer. + balance: z.string(), updatedAt: z.date(), }); +export const HoldingItemSchema = z.object({ + id: z.string(), + ownerAddress: z.string(), + creatorId: z.string(), + balance: z.string(), + currentPrice: z.string(), + updatedAt: z.date(), +}); + +export const HoldingsResponseSchema = z.object({ + holdings: z.array(HoldingItemSchema), + total_portfolio_value: z.string(), +}); + export const OwnershipResponseSchema = z.array(OwnershipItemSchema); diff --git a/src/modules/ownership/ownership.utils.test.ts b/src/modules/ownership/ownership.utils.test.ts new file mode 100644 index 0000000..fa64b24 --- /dev/null +++ b/src/modules/ownership/ownership.utils.test.ts @@ -0,0 +1,39 @@ +import { calculateTotalPortfolioValue, HoldingEntry } from './ownership.utils'; + +describe('calculateTotalPortfolioValue', () => { + it('returns correct sum for a single holding', () => { + const entries: HoldingEntry[] = [ + { balance: '100', currentPrice: '5.50' }, + ]; + expect(calculateTotalPortfolioValue(entries)).toBe('550'); + }); + + it('returns correct sum for multiple holdings', () => { + const entries: HoldingEntry[] = [ + { balance: '100', currentPrice: '5.50' }, + { balance: '200', currentPrice: '10.00' }, + { balance: '50', currentPrice: '2.00' }, + ]; + expect(calculateTotalPortfolioValue(entries)).toBe('2650'); + }); + + it('returns zero when all prices are zero', () => { + const entries: HoldingEntry[] = [ + { balance: '100', currentPrice: '0' }, + { balance: '200', currentPrice: '0' }, + ]; + expect(calculateTotalPortfolioValue(entries)).toBe('0'); + }); + + it('returns zero when all balances are zero', () => { + const entries: HoldingEntry[] = [ + { balance: '0', currentPrice: '5.50' }, + { balance: '0', currentPrice: '10.00' }, + ]; + expect(calculateTotalPortfolioValue(entries)).toBe('0'); + }); + + it('returns zero for an empty array', () => { + expect(calculateTotalPortfolioValue([])).toBe('0'); + }); +}); diff --git a/src/modules/ownership/ownership.utils.ts b/src/modules/ownership/ownership.utils.ts new file mode 100644 index 0000000..587c5f3 --- /dev/null +++ b/src/modules/ownership/ownership.utils.ts @@ -0,0 +1,12 @@ +export interface HoldingEntry { + balance: string; + currentPrice: string; +} + +export function calculateTotalPortfolioValue(entries: HoldingEntry[]): string { + const total = entries.reduce( + (sum, entry) => sum + Number(entry.balance) * Number(entry.currentPrice), + 0, + ); + return String(total); +}