From 8217b028bbb4fb4bece760c01d363bcf895ec16c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 23:28:33 +0100 Subject: [PATCH 1/3] feat(ownership): add calculateTotalPortfolioValue helper Pure helper that accepts an array of holding entries (balance + currentPrice) and returns the summed total portfolio value as a string. Designed for reuse across the holdings endpoint and any future summary endpoints. --- src/modules/ownership/ownership.utils.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/modules/ownership/ownership.utils.ts 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); +} From 58f8384a0de74d78ab9f969ae5d491e9cf2cc143 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 23:29:00 +0100 Subject: [PATCH 2/3] feat(ownership): integrate total_portfolio_value into holdings endpoint - Add HoldingItemSchema and HoldingsResponseSchema to ownership schemas - Update controller to transform records, inject currentPrice, and include total_portfolio_value in the response --- src/modules/ownership/ownership.controllers.ts | 16 ++++++++++++++-- src/modules/ownership/ownership.schemas.ts | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) 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); From 76a74b29e7708bd023fe0ca12b882d71b07f4211 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Jun 2026 23:59:57 +0100 Subject: [PATCH 3/3] test(ownership): add unit tests for calculateTotalPortfolioValue Covers single entry, multiple holdings, zero prices, zero balances, and empty array edge cases. --- src/modules/ownership/ownership.utils.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/modules/ownership/ownership.utils.test.ts 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'); + }); +});