diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 5226c3c12..6b4b11797 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -43,6 +43,11 @@ import { import { isNumber } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; +import { + isSameTopHoldingName, + normalizeTopHoldingName +} from './top-holdings.util'; + @Component({ imports: [ GfPortfolioProportionChartComponent, @@ -408,14 +413,17 @@ export class GfAllocationsPageComponent implements OnInit { for (const holding of position.holdings) { const { allocationInPercentage, name, valueInBaseCurrency } = holding; + const topHoldingKey = normalizeTopHoldingName(name); - if (this.topHoldingsMap[name]?.value) { - this.topHoldingsMap[name].value += isNumber(valueInBaseCurrency) + if (this.topHoldingsMap[topHoldingKey]?.value) { + this.topHoldingsMap[topHoldingKey].value += isNumber( + valueInBaseCurrency + ) ? valueInBaseCurrency : allocationInPercentage * this.portfolioDetails.holdings[symbol].valueInPercentage; } else { - this.topHoldingsMap[name] = { + this.topHoldingsMap[topHoldingKey] = { name, value: isNumber(valueInBaseCurrency) ? valueInBaseCurrency @@ -518,7 +526,7 @@ export class GfAllocationsPageComponent implements OnInit { if (holding.holdings.length > 0) { const currentParentHolding = holding.holdings.find( (parentHolding) => { - return parentHolding.name === name; + return isSameTopHoldingName(parentHolding.name, name); } ); diff --git a/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.spec.ts b/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.spec.ts new file mode 100644 index 000000000..71fa2ab5c --- /dev/null +++ b/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.spec.ts @@ -0,0 +1,40 @@ +import { + isSameTopHoldingName, + normalizeTopHoldingName +} from './top-holdings.util'; + +describe('Top Holdings Utility', () => { + it('normalizes top holding names case-insensitively', () => { + expect(normalizeTopHoldingName('NVIDIA Corp')).toEqual('nvidia corp'); + expect(normalizeTopHoldingName('NVIDIA CORP')).toEqual('nvidia corp'); + }); + + it('matches top holding names case-insensitively', () => { + expect(isSameTopHoldingName('NVIDIA Corp', 'NVIDIA CORP')).toBe(true); + expect(isSameTopHoldingName('Apple Inc', 'Microsoft Corp')).toBe(false); + }); + + it('supports aggregation of mixed-case holding names into one entry', () => { + const holdings = [ + { name: 'NVIDIA Corp', valueInBaseCurrency: 50 }, + { name: 'NVIDIA CORP', valueInBaseCurrency: 40 } + ]; + const topHoldingsMap: Record = {}; + + for (const { name, valueInBaseCurrency } of holdings) { + const topHoldingKey = normalizeTopHoldingName(name); + + if (topHoldingsMap[topHoldingKey]) { + topHoldingsMap[topHoldingKey].value += valueInBaseCurrency; + } else { + topHoldingsMap[topHoldingKey] = { + name, + value: valueInBaseCurrency + }; + } + } + + expect(Object.values(topHoldingsMap)).toHaveLength(1); + expect(Object.values(topHoldingsMap)[0].value).toEqual(90); + }); +}); diff --git a/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.ts b/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.ts new file mode 100644 index 000000000..a02587aa8 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/allocations/top-holdings.util.ts @@ -0,0 +1,7 @@ +export function normalizeTopHoldingName(name: string) { + return name.trim().toLocaleLowerCase(); +} + +export function isSameTopHoldingName(aName: string, bName: string) { + return normalizeTopHoldingName(aName) === normalizeTopHoldingName(bName); +}