From ae3d64e59b8720e5b89a8ba5fcc7aee4d053d0e0 Mon Sep 17 00:00:00 2001 From: Divine Date: Mon, 22 Jun 2026 22:57:14 +0100 Subject: [PATCH 1/4] feat portfolio service --- src/investment/portfolio/dto/portfolio.dto.ts | 72 +++- .../portfolio/entities/portfolio.entity.ts | 45 +- .../portfolio/services/portfolio.service.ts | 224 +++++++--- test/portfolio/portfolio.service.spec.ts | 404 +++++++++++++----- 4 files changed, 560 insertions(+), 185 deletions(-) diff --git a/src/investment/portfolio/dto/portfolio.dto.ts b/src/investment/portfolio/dto/portfolio.dto.ts index d8964f5..15078ec 100644 --- a/src/investment/portfolio/dto/portfolio.dto.ts +++ b/src/investment/portfolio/dto/portfolio.dto.ts @@ -1,19 +1,50 @@ -import { IsString, IsOptional, IsEnum, IsNumber, IsBoolean, IsJSON } from 'class-validator'; -import { PortfolioStatus } from '../entities/portfolio.entity'; +import { + IsString, + IsOptional, + IsEnum, + IsNumber, + IsBoolean, + IsObject, + Min, + Max, +} from "class-validator"; +import { + PortfolioStatus, + PortfolioType, + AllocationStrategy, +} from "../entities/portfolio.entity"; + export class CreatePortfolioDto { @IsString() name: string; + @IsOptional() + @IsEnum(PortfolioType) + type?: PortfolioType; + @IsOptional() @IsString() description?: string; + @IsOptional() + @IsObject() + initialAllocation?: Record; + + @IsOptional() + @IsObject() + targetAllocation?: Record; + + @IsOptional() + @IsEnum(AllocationStrategy) + allocationStrategy?: AllocationStrategy; + @IsOptional() @IsNumber() + @Min(0) totalValue?: number; @IsOptional() - @IsJSON() + @IsObject() metadata?: Record; @IsOptional() @@ -22,10 +53,12 @@ export class CreatePortfolioDto { @IsOptional() @IsString() - rebalanceFrequency?: 'daily' | 'weekly' | 'monthly' | 'quarterly'; + rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; @IsOptional() @IsNumber() + @Min(0) + @Max(100) rebalanceThreshold?: number; } @@ -34,6 +67,10 @@ export class UpdatePortfolioDto { @IsString() name?: string; + @IsOptional() + @IsEnum(PortfolioType) + type?: PortfolioType; + @IsOptional() @IsString() description?: string; @@ -42,31 +79,52 @@ export class UpdatePortfolioDto { @IsEnum(PortfolioStatus) status?: PortfolioStatus; + @IsOptional() + @IsObject() + initialAllocation?: Record; + + @IsOptional() + @IsObject() + currentAllocation?: Record; + + @IsOptional() + @IsObject() + targetAllocation?: Record; + + @IsOptional() + @IsEnum(AllocationStrategy) + allocationStrategy?: AllocationStrategy; + @IsOptional() @IsBoolean() autoRebalanceEnabled?: boolean; @IsOptional() @IsString() - rebalanceFrequency?: 'daily' | 'weekly' | 'monthly' | 'quarterly'; + rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; @IsOptional() @IsNumber() + @Min(0) + @Max(100) rebalanceThreshold?: number; @IsOptional() - @IsJSON() + @IsObject() metadata?: Record; } export class PortfolioResponseDto { id: string; name: string; + type: PortfolioType; description?: string; status: PortfolioStatus; - totalValue: number; + initialAllocation: Record; currentAllocation: Record; targetAllocation?: Record; + allocationStrategy?: AllocationStrategy; + totalValue: number; autoRebalanceEnabled: boolean; rebalanceFrequency?: string; rebalanceThreshold: number; diff --git a/src/investment/portfolio/entities/portfolio.entity.ts b/src/investment/portfolio/entities/portfolio.entity.ts index 9b0e72b..51ac545 100644 --- a/src/investment/portfolio/entities/portfolio.entity.ts +++ b/src/investment/portfolio/entities/portfolio.entity.ts @@ -4,6 +4,7 @@ import { Column, CreateDateColumn, UpdateDateColumn, + DeleteDateColumn, Index, ManyToOne, OneToMany, @@ -21,6 +22,26 @@ export enum PortfolioStatus { ARCHIVED = "archived", } +export enum PortfolioType { + CONSERVATIVE = "conservative", + BALANCED = "balanced", + AGGRESSIVE = "aggressive", + INCOME = "income", + GROWTH = "growth", + RETIREMENT = "retirement", + CUSTOM = "custom", +} + +export enum AllocationStrategy { + MANUAL = "manual", + MODERN_PORTFOLIO_THEORY = "modern_portfolio_theory", + BLACK_LITTERMAN = "black_litterman", + RISK_PARITY = "risk_parity", + MIN_VARIANCE = "min_variance", + MAX_SHARPE = "max_sharpe", + CUSTOM = "custom", +} + @Entity("portfolios") @Index(["userId", "status"]) export class Portfolio { @@ -30,6 +51,13 @@ export class Portfolio { @Column({ unique: true }) name: string; + @Column({ + type: "enum", + enum: PortfolioType, + default: PortfolioType.CUSTOM, + }) + type: PortfolioType; + @Column({ type: "text", nullable: true }) description: string; @@ -40,23 +68,24 @@ export class Portfolio { }) status: PortfolioStatus; - // Total portfolio value + @Column({ type: "jsonb", default: {} }) + initialAllocation: Record; + + @Column({ type: "enum", enum: AllocationStrategy, default: AllocationStrategy.MANUAL, nullable: true }) + allocationStrategy: AllocationStrategy; + @Column({ type: "decimal", precision: 18, scale: 2, default: 0 }) totalValue: number; - // Current allocation in JSON format @Column({ type: "jsonb", default: {} }) currentAllocation: Record; - // Target allocation (from optimization) @Column({ type: "jsonb", nullable: true }) targetAllocation: Record; - // Portfolio metadata @Column({ type: "jsonb", nullable: true }) metadata: Record; - // Rebalancing configuration @Column({ type: "boolean", default: false }) autoRebalanceEnabled: boolean; @@ -64,7 +93,7 @@ export class Portfolio { rebalanceFrequency: "daily" | "weekly" | "monthly" | "quarterly" | null; @Column({ type: "decimal", precision: 5, scale: 2, default: 5 }) - rebalanceThreshold: number; // Percentage threshold for rebalancing + rebalanceThreshold: number; @CreateDateColumn() createdAt: Date; @@ -72,10 +101,12 @@ export class Portfolio { @UpdateDateColumn() updatedAt: Date; + @DeleteDateColumn({ nullable: true }) + deletedAt: Date; + @Column({ nullable: true }) lastRebalanceDate: Date; - // Relations @ManyToOne(() => User, { onDelete: "CASCADE" }) @JoinColumn({ name: "userId" }) user: User; diff --git a/src/investment/portfolio/services/portfolio.service.ts b/src/investment/portfolio/services/portfolio.service.ts index 09e2cf4..cbe4f64 100644 --- a/src/investment/portfolio/services/portfolio.service.ts +++ b/src/investment/portfolio/services/portfolio.service.ts @@ -1,4 +1,9 @@ -import { Injectable, Logger, BadRequestException } from "@nestjs/common"; +import { + Injectable, + Logger, + BadRequestException, + NotFoundException, +} from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { Portfolio } from "../entities/portfolio.entity"; @@ -11,7 +16,11 @@ import { import { RiskProfile } from "../entities/risk-profile.entity"; import { CreatePortfolioDto, UpdatePortfolioDto } from "../dto/portfolio.dto"; import { CreateOptimizationDto } from "../dto/optimization.dto"; -import { PortfolioStatus } from "../entities/portfolio.entity"; +import { + PortfolioStatus, + PortfolioType, + AllocationStrategy, +} from "../entities/portfolio.entity"; import { ModernPortfolioTheory } from "../algorithms/modern-portfolio-theory"; import { BlackLittermanModel } from "../algorithms/black-litterman"; import { ConstraintOptimizer } from "../algorithms/constraint-optimizer"; @@ -31,43 +40,47 @@ export class PortfolioService { private riskProfileRepository: Repository, ) {} - /** - * Create a new portfolio for a user - */ - async createPortfolio( - userId: string, - dto: CreatePortfolioDto, - ): Promise { + async createPortfolio(userId: string, dto: CreatePortfolioDto): Promise { + this.validatePortfolioName(dto.name); + this.validateAllocation(dto.initialAllocation); + + const existingPortfolio = await this.portfolioRepository.findOne({ + where: { name: dto.name, userId }, + }); + + if (existingPortfolio && !existingPortfolio.deletedAt) { + throw new BadRequestException( + "Portfolio with this name already exists", + ); + } + const portfolio = this.portfolioRepository.create({ ...dto, userId, status: PortfolioStatus.ACTIVE, currentAllocation: {}, targetAllocation: {}, + totalValue: dto.totalValue || 0, + autoRebalanceEnabled: dto.autoRebalanceEnabled || false, + rebalanceThreshold: dto.rebalanceThreshold || 5, }); return this.portfolioRepository.save(portfolio); } - /** - * Get portfolio by ID - */ async getPortfolio(portfolioId: string): Promise { const portfolio = await this.portfolioRepository.findOne({ where: { id: portfolioId }, relations: ["assets", "optimizationHistory", "performanceMetrics"], }); - if (!portfolio) { - throw new BadRequestException("Portfolio not found"); + if (!portfolio || portfolio.deletedAt) { + throw new NotFoundException("Portfolio not found"); } return portfolio; } - /** - * Get all portfolios for user - */ async getUserPortfolios(userId: string): Promise { return this.portfolioRepository.find({ where: { userId }, @@ -76,23 +89,115 @@ export class PortfolioService { }); } - /** - * Update portfolio - */ async updatePortfolio( portfolioId: string, dto: UpdatePortfolioDto, ): Promise { const portfolio = await this.getPortfolio(portfolioId); + if (dto.name && dto.name !== portfolio.name) { + this.validatePortfolioName(dto.name); + } + + if (dto.initialAllocation) { + this.validateAllocation(dto.initialAllocation); + portfolio.initialAllocation = dto.initialAllocation; + } + + if (dto.currentAllocation) { + this.validateAllocation(dto.currentAllocation); + portfolio.currentAllocation = dto.currentAllocation; + } + + if (dto.targetAllocation) { + this.validateAllocation(dto.targetAllocation); + portfolio.targetAllocation = dto.targetAllocation; + } + + if (dto.status === PortfolioStatus.ARCHIVED) { + portfolio.status = PortfolioStatus.ARCHIVED; + portfolio.deletedAt = new Date(); + } else if (dto.status) { + portfolio.status = dto.status; + if (dto.status === PortfolioStatus.ACTIVE) { + portfolio.deletedAt = null; + } + } + Object.assign(portfolio, dto); return this.portfolioRepository.save(portfolio); } - /** - * Add asset to portfolio - */ + async deletePortfolio(portfolioId: string): Promise { + const portfolio = await this.getPortfolio(portfolioId); + portfolio.status = PortfolioStatus.ARCHIVED; + portfolio.deletedAt = new Date(); + await this.portfolioRepository.save(portfolio); + } + + async restorePortfolio(portfolioId: string): Promise { + const portfolio = await this.portfolioRepository.findOne({ + where: { id: portfolioId }, + }); + + if (!portfolio || !portfolio.deletedAt) { + throw new NotFoundException("Archived portfolio not found"); + } + + portfolio.status = PortfolioStatus.ACTIVE; + portfolio.deletedAt = null; + + return this.portfolioRepository.save(portfolio); + } + + private validatePortfolioName(name: string): void { + if (!name || name.trim().length === 0) { + throw new BadRequestException("Portfolio name cannot be empty"); + } + + if (name.length > 100) { + throw new BadRequestException( + "Portfolio name cannot exceed 100 characters", + ); + } + } + + private validateAllocation(allocation: Record): void { + if (!allocation) return; + + const tickers = Object.keys(allocation); + + if (tickers.length === 0) { + throw new BadRequestException("Allocation cannot be empty"); + } + + if (tickers.length > 50) { + throw new BadRequestException( + "Allocation cannot contain more than 50 assets", + ); + } + + const totalPercentage = Object.values(allocation).reduce( + (sum, val) => sum + val, + 0, + ); + + if (Math.abs(totalPercentage - 100) > 0.01) { + throw new BadRequestException( + `Allocation percentages must sum to 100%, got ${totalPercentage.toFixed(2)}%`, + ); + } + + for (const [ticker, percentage] of Object.entries(allocation)) { + if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) { + throw new BadRequestException( + `Invalid allocation percentage ${percentage} for ${ticker}. Must be between 0 and 100`, + ); + } + } + } + async addAsset( portfolioId: string, ticker: string, @@ -103,7 +208,14 @@ export class PortfolioService { ): Promise { const portfolio = await this.getPortfolio(portfolioId); - // Check if asset already exists + if (quantity <= 0) { + throw new BadRequestException("Quantity must be positive"); + } + + if (currentPrice < 0) { + throw new BadRequestException("Current price cannot be negative"); + } + let asset = await this.portfolioAssetRepository.findOne({ where: { portfolioId, ticker }, }); @@ -121,22 +233,17 @@ export class PortfolioService { }); } - // Update asset asset.quantity = quantity; asset.currentPrice = currentPrice; asset.value = quantity * currentPrice; asset = await this.portfolioAssetRepository.save(asset); - // Update portfolio allocation await this.updatePortfolioAllocation(portfolioId); return asset; } - /** - * Update asset price and calculate allocation - */ async updateAssetPrice( assetId: string, currentPrice: number, @@ -146,7 +253,11 @@ export class PortfolioService { }); if (!asset) { - throw new BadRequestException("Asset not found"); + throw new NotFoundException("Asset not found"); + } + + if (currentPrice < 0) { + throw new BadRequestException("Price cannot be negative"); } asset.currentPrice = currentPrice; @@ -155,15 +266,11 @@ export class PortfolioService { const updated = await this.portfolioAssetRepository.save(asset); - // Recalculate allocation await this.updatePortfolioAllocation(asset.portfolioId); return updated; } - /** - * Update portfolio allocation percentages - */ async updatePortfolioAllocation(portfolioId: string): Promise { const portfolio = await this.getPortfolio(portfolioId); const assets = await this.portfolioAssetRepository.find({ @@ -191,9 +298,6 @@ export class PortfolioService { await this.portfolioAssetRepository.save(assets); } - /** - * Run portfolio optimization - */ async runOptimization( portfolioId: string, dto: CreateOptimizationDto, @@ -207,7 +311,6 @@ export class PortfolioService { throw new BadRequestException("Portfolio has no assets to optimize"); } - // Create optimization history record const optimization = this.optimizationRepository.create({ portfolioId, method: dto.method, @@ -220,11 +323,9 @@ export class PortfolioService { let result = await this.optimizationRepository.save(optimization); try { - // Prepare data const expectedReturns = assets.map((a) => a.expectedReturn || 0.07); const volatilities = assets.map((a) => a.volatility || 0.15); - // Simple correlation matrix (could be enhanced with historical data) const correlationMatrix = this.generateCorrelationMatrix(assets.length); const covarianceMatrix = ModernPortfolioTheory.calculateCovarianceMatrix( @@ -234,7 +335,6 @@ export class PortfolioService { let suggestedWeights: number[] = []; - // Run optimization based on method switch (dto.method) { case OptimizationMethod.MEAN_VARIANCE: suggestedWeights = ModernPortfolioTheory.meanVarianceOptimization( @@ -266,21 +366,18 @@ export class PortfolioService { suggestedWeights = new Array(assets.length).fill(1 / assets.length); } - // Build allocation const suggestedAllocation: Record = {}; for (let i = 0; i < assets.length; i++) { suggestedAllocation[assets[i].ticker] = suggestedWeights[i] * 100; assets[i].suggestedAllocation = suggestedWeights[i] * 100; } - // Calculate metrics const metrics = ModernPortfolioTheory.calculatePortfolioMetrics( suggestedWeights, expectedReturns, covarianceMatrix, ); - // Calculate improvement score const currentReturn = 0; const currentVolatility = 0; @@ -301,7 +398,6 @@ export class PortfolioService { 100 : 0; - // Update optimization result result.status = OptimizationStatus.COMPLETED; result.suggestedAllocation = suggestedAllocation; result.expectedReturn = metrics.expectedReturn; @@ -312,7 +408,6 @@ export class PortfolioService { result = await this.optimizationRepository.save(result); - // Save suggested allocation to assets await this.portfolioAssetRepository.save(assets); this.logger.log(`Optimization completed for portfolio ${portfolioId}`); @@ -327,9 +422,6 @@ export class PortfolioService { } } - /** - * Generate simple correlation matrix - */ private generateCorrelationMatrix(size: number): number[][] { const matrix: number[][] = []; @@ -339,7 +431,6 @@ export class PortfolioService { if (i === j) { matrix[i][j] = 1; } else { - // Simplified correlation matrix[i][j] = 0.5 + Math.random() * 0.2; } } @@ -348,9 +439,6 @@ export class PortfolioService { return matrix; } - /** - * Approve optimization - */ async approveOptimization( optimizationId: string, notes?: string, @@ -360,7 +448,13 @@ export class PortfolioService { }); if (!optimization) { - throw new BadRequestException("Optimization not found"); + throw new NotFoundException("Optimization not found"); + } + + if (optimization.status !== OptimizationStatus.COMPLETED) { + throw new BadRequestException( + "Only completed optimizations can be approved", + ); } optimization.status = OptimizationStatus.APPROVED; @@ -369,21 +463,23 @@ export class PortfolioService { return this.optimizationRepository.save(optimization); } - /** - * Implement optimization (apply to portfolio) - */ async implementOptimization(optimizationId: string): Promise { const optimization = await this.optimizationRepository.findOne({ where: { id: optimizationId }, }); if (!optimization) { - throw new BadRequestException("Optimization not found"); + throw new NotFoundException("Optimization not found"); + } + + if (optimization.status !== OptimizationStatus.APPROVED) { + throw new BadRequestException( + "Only approved optimizations can be implemented", + ); } const portfolio = await this.getPortfolio(optimization.portfolioId); - // Apply suggested allocation portfolio.targetAllocation = optimization.suggestedAllocation; portfolio.lastRebalanceDate = new Date(); @@ -395,9 +491,6 @@ export class PortfolioService { return this.portfolioRepository.save(portfolio); } - /** - * Get optimization history - */ async getOptimizationHistory( portfolioId: string, limit: number = 10, @@ -408,11 +501,4 @@ export class PortfolioService { take: limit, }); } - - /** - * Delete portfolio - */ - async deletePortfolio(portfolioId: string): Promise { - await this.portfolioRepository.delete(portfolioId); - } } diff --git a/test/portfolio/portfolio.service.spec.ts b/test/portfolio/portfolio.service.spec.ts index 8b1d453..0da1866 100644 --- a/test/portfolio/portfolio.service.spec.ts +++ b/test/portfolio/portfolio.service.spec.ts @@ -1,14 +1,15 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { PortfolioService } from '../../src/investment/portfolio/services/portfolio.service'; -import { Portfolio } from '../../src/investment/portfolio/entities/portfolio.entity'; -import { PortfolioAsset } from '../../src/investment/portfolio/entities/portfolio-asset.entity'; -import { OptimizationHistory } from '../../src/investment/portfolio/entities/optimization-history.entity'; -import { RiskProfile } from '../../src/investment/portfolio/entities/risk-profile.entity'; -import { CreatePortfolioDto } from '../../src/investment/portfolio/dto/portfolio.dto'; -import { OptimizationMethod } from '../../src/investment/portfolio/entities/optimization-history.entity'; - -describe('PortfolioService', () => { +import { Test, TestingModule } from "@nestjs/testing"; +import { getRepositoryToken } from "@nestjs/typeorm"; +import { PortfolioService } from "../../src/investment/portfolio/services/portfolio.service"; +import { Portfolio } from "../../src/investment/portfolio/entities/portfolio.entity"; +import { PortfolioAsset } from "../../src/investment/portfolio/entities/portfolio-asset.entity"; +import { OptimizationHistory } from "../../src/investment/portfolio/entities/optimization-history.entity"; +import { RiskProfile } from "../../src/investment/portfolio/entities/risk-profile.entity"; +import { CreatePortfolioDto } from "../../src/investment/portfolio/dto/portfolio.dto"; +import { OptimizationMethod } from "../../src/investment/portfolio/entities/optimization-history.entity"; +import { NotFoundException, BadRequestException } from "@nestjs/common"; + +describe("PortfolioService", () => { let service: PortfolioService; let portfolioRepository: any; let assetRepository: any; @@ -16,28 +17,46 @@ describe('PortfolioService', () => { let riskProfileRepository: any; const mockPortfolio = { - id: 'test-portfolio-1', - userId: 'test-user-1', - name: 'Test Portfolio', - status: 'active', + id: "test-portfolio-1", + userId: "test-user-1", + name: "Test Portfolio", + type: "custom", + status: "active", totalValue: 100000, currentAllocation: { AAPL: 30, MSFT: 70 }, targetAllocation: null, - assets: [], + initialAllocation: {}, + allocationStrategy: "manual", autoRebalanceEnabled: false, rebalanceThreshold: 5, + assets: [], + deletedAt: null, save: jest.fn(), }; const mockAsset = { - id: 'asset-1', - ticker: 'AAPL', - name: 'Apple', + id: "asset-1", + ticker: "AAPL", + name: "Apple", quantity: 100, currentPrice: 150, value: 15000, allocationPercentage: 15, - portfolioId: 'test-portfolio-1', + portfolioId: "test-portfolio-1", + save: jest.fn(), + }; + + const mockOptimization = { + id: "opt-1", + portfolioId: "test-portfolio-1", + method: OptimizationMethod.MEAN_VARIANCE, + status: "completed", + suggestedAllocation: { AAPL: 40, MSFT: 60 }, + expectedReturn: 0.08, + expectedVolatility: 0.15, + expectedSharpeRatio: 0.5, + improvementScore: 10, + currentAllocation: mockPortfolio.currentAllocation, save: jest.fn(), }; @@ -45,19 +64,20 @@ describe('PortfolioService', () => { portfolioRepository = { create: jest.fn().mockReturnValue(mockPortfolio), save: jest.fn().mockResolvedValue(mockPortfolio), - findOne: jest.fn().mockResolvedValue(mockPortfolio), + findOne: jest.fn(), find: jest.fn().mockResolvedValue([mockPortfolio]), + delete: jest.fn(), }; assetRepository = { create: jest.fn().mockReturnValue(mockAsset), save: jest.fn().mockResolvedValue(mockAsset), find: jest.fn().mockResolvedValue([mockAsset]), - findOne: jest.fn().mockResolvedValue(mockAsset), + findOne: jest.fn(), }; optimizationRepository = { - create: jest.fn(), + create: jest.fn().mockReturnValue(mockOptimization), save: jest.fn(), find: jest.fn(), findOne: jest.fn(), @@ -92,63 +112,93 @@ describe('PortfolioService', () => { service = module.get(PortfolioService); }); - it('should be defined', () => { + it("should be defined", () => { expect(service).toBeDefined(); }); - describe('createPortfolio', () => { - it('should create a new portfolio', async () => { + describe("createPortfolio", () => { + it("should create a new portfolio", async () => { + portfolioRepository.findOne.mockResolvedValue(null); + const dto: CreatePortfolioDto = { - name: 'Test Portfolio', - description: 'Test description', + name: "Test Portfolio", + type: "custom", }; - const result = await service.createPortfolio( - 'test-user-1', - dto, - ); + const result = await service.createPortfolio("test-user-1", dto); expect(portfolioRepository.create).toHaveBeenCalledWith( expect.objectContaining({ name: dto.name, - userId: 'test-user-1', + userId: "test-user-1", + type: "custom", + status: "active", }), ); expect(portfolioRepository.save).toHaveBeenCalled(); expect(result).toEqual(mockPortfolio); }); + + it("should throw if portfolio name already exists", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const dto: CreatePortfolioDto = { + name: "Test Portfolio", + }; + + await expect( + service.createPortfolio("test-user-1", dto), + ).rejects.toThrow("Portfolio with this name already exists"); + }); + + it("should throw for empty name", async () => { + const dto: CreatePortfolioDto = { + name: " ", + }; + + await expect( + service.createPortfolio("test-user-1", dto), + ).rejects.toThrow(BadRequestException); + }); }); - describe('getPortfolio', () => { - it('should return a portfolio by ID', async () => { - const result = await service.getPortfolio( - 'test-portfolio-1', - ); + describe("getPortfolio", () => { + it("should return a portfolio by ID", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const result = await service.getPortfolio("test-portfolio-1"); expect(portfolioRepository.findOne).toHaveBeenCalledWith({ - where: { id: 'test-portfolio-1' }, + where: { id: "test-portfolio-1" }, relations: expect.any(Array), }); expect(result).toEqual(mockPortfolio); }); - it('should throw error if portfolio not found', async () => { + it("should throw NotFoundException if portfolio not found", async () => { portfolioRepository.findOne.mockResolvedValue(null); await expect( - service.getPortfolio('non-existent'), - ).rejects.toThrow('Portfolio not found'); + service.getPortfolio("non-existent"), + ).rejects.toThrow(NotFoundException); + }); + + it("should throw NotFoundException if portfolio is deleted", async () => { + const deletedPortfolio = { ...mockPortfolio, deletedAt: new Date() }; + portfolioRepository.findOne.mockResolvedValue(deletedPortfolio); + + await expect( + service.getPortfolio("test-portfolio-1"), + ).rejects.toThrow(NotFoundException); }); }); - describe('getUserPortfolios', () => { - it('should return all portfolios for a user', async () => { - const result = await service.getUserPortfolios( - 'test-user-1', - ); + describe("getUserPortfolios", () => { + it("should return all portfolios for a user", async () => { + const result = await service.getUserPortfolios("test-user-1"); expect(portfolioRepository.find).toHaveBeenCalledWith({ - where: { userId: 'test-user-1' }, + where: { userId: "test-user-1" }, relations: expect.any(Array), order: expect.any(Object), }); @@ -156,83 +206,233 @@ describe('PortfolioService', () => { }); }); - describe('addAsset', () => { - it('should add an asset to portfolio', async () => { + describe("updatePortfolio", () => { + it("should update portfolio properties", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const updateDto = { + name: "Updated Portfolio", + description: "Updated description", + }; + + const result = await service.updatePortfolio( + "test-portfolio-1", + updateDto, + ); + + expect(portfolioRepository.save).toHaveBeenCalled(); + expect(result.name).toBe("Updated Portfolio"); + }); + + it("should archive portfolio when status is ARCHIVED", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const updateDto = { + status: "archived", + }; + + const result = await service.updatePortfolio( + "test-portfolio-1", + updateDto, + ); + + expect(result.status).toBe("archived"); + expect(result.deletedAt).toBeDefined(); + }); + + it("should validate allocation percentages", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const updateDto = { + currentAllocation: { AAPL: 60, MSFT: 50 }, + }; + + await expect( + service.updatePortfolio("test-portfolio-1", updateDto), + ).rejects.toThrow(BadRequestException); + }); + }); + + describe("deletePortfolio", () => { + it("should soft delete portfolio", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + await service.deletePortfolio("test-portfolio-1"); + + expect(mockPortfolio.status).toBe("archived"); + expect(mockPortfolio.deletedAt).toBeDefined(); + expect(portfolioRepository.save).toHaveBeenCalled(); + }); + + it("should throw NotFoundException for deleted portfolio", async () => { + const deletedPortfolio = { ...mockPortfolio, deletedAt: new Date() }; + portfolioRepository.findOne.mockResolvedValue(deletedPortfolio); + + await expect( + service.deletePortfolio("test-portfolio-1"), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe("restorePortfolio", () => { + it("should restore archived portfolio", async () => { + const archivedPortfolio = { + ...mockPortfolio, + status: "archived", + deletedAt: new Date(), + }; + portfolioRepository.findOne.mockResolvedValue(archivedPortfolio); + + const result = await service.restorePortfolio("test-portfolio-1"); + + expect(result.status).toBe("active"); + expect(result.deletedAt).toBeNull(); + expect(portfolioRepository.save).toHaveBeenCalled(); + }); + + it("should throw NotFoundException when restoring non-archived portfolio", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + await expect( + service.restorePortfolio("test-portfolio-1"), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe("addAsset", () => { + it("should add an asset to portfolio", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + assetRepository.findOne.mockResolvedValue(null); + const result = await service.addAsset( - 'test-portfolio-1', - 'AAPL', - 'Apple', + "test-portfolio-1", + "AAPL", + "Apple", 100, 150, 0, ); - expect(assetRepository.findOne).toHaveBeenCalled(); + expect(assetRepository.create).toHaveBeenCalled(); + expect(assetRepository.save).toHaveBeenCalled(); expect(result).toEqual(mockAsset); }); - }); - describe('updateAssetPrice', () => { - it('should update asset price', async () => { - const newPrice = 160; + it("should throw for invalid quantity", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); - const result = await service.updateAssetPrice( - 'asset-1', - newPrice, - ); + await expect( + service.addAsset("test-portfolio-1", "AAPL", "Apple", -10, 150, 0), + ).rejects.toThrow(BadRequestException); + }); - expect(assetRepository.findOne).toHaveBeenCalledWith({ - where: { id: 'asset-1' }, - }); - expect(result.currentPrice).toBeDefined(); + it("should throw for negative price", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + await expect( + service.addAsset("test-portfolio-1", "AAPL", "Apple", 100, -150, 0), + ).rejects.toThrow(BadRequestException); }); }); - describe('runOptimization', () => { - it('should run portfolio optimization', async () => { + describe("runOptimization", () => { + it("should run portfolio optimization", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + assetRepository.find.mockResolvedValue([mockAsset]); optimizationRepository.create.mockReturnValue({ - portfolioId: 'test-portfolio-1', - method: OptimizationMethod.MEAN_VARIANCE, - status: 'pending', - parameters: {}, - suggestedAllocation: {}, - currentAllocation: mockPortfolio.currentAllocation, + ...mockOptimization, + status: "in_progress", save: jest.fn(), }); - optimizationRepository.save .mockResolvedValueOnce({ - id: 'opt-1', - portfolioId: 'test-portfolio-1', - method: OptimizationMethod.MEAN_VARIANCE, - status: 'in_progress', - suggestedAllocation: {}, - parameters: {}, - currentAllocation: mockPortfolio.currentAllocation, - save: jest.fn(), + ...mockOptimization, + status: "in_progress", }) - .mockResolvedValueOnce({ - id: 'opt-1', - status: 'completed', - suggestedAllocation: { AAPL: 40, MSFT: 60 }, - expectedReturn: 0.08, - expectedVolatility: 0.15, - expectedSharpeRatio: 0.5, - improvementScore: 10, - completedAt: new Date(), - }); - - assetRepository.save.mockResolvedValue([mockAsset]); - - const result = await service.runOptimization( - 'test-portfolio-1', - { + .mockResolvedValueOnce(mockOptimization); + + const result = await service.runOptimization("test-portfolio-1", { + method: OptimizationMethod.MEAN_VARIANCE, + }); + + expect(result.status).toBe("completed"); + expect(optimizationRepository.save).toHaveBeenCalled(); + }); + + it("should throw if portfolio has no assets", async () => { + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + assetRepository.find.mockResolvedValue([]); + + await expect( + service.runOptimization("test-portfolio-1", { method: OptimizationMethod.MEAN_VARIANCE, - portfolioId: 'test-portfolio-1', - }, + }), + ).rejects.toThrow("Portfolio has no assets to optimize"); + }); + }); + + describe("approveOptimization", () => { + it("should approve completed optimization", async () => { + optimizationRepository.findOne.mockResolvedValue(mockOptimization); + + const result = await service.approveOptimization("opt-1", "Looks good"); + + expect(result.status).toBe("approved"); + expect(optimizationRepository.save).toHaveBeenCalled(); + }); + + it("should throw if optimization not found", async () => { + optimizationRepository.findOne.mockResolvedValue(null); + + await expect( + service.approveOptimization("non-existent"), + ).rejects.toThrow(NotFoundException); + }); + + it("should throw if optimization is not completed", async () => { + const pendingOptimization = { + ...mockOptimization, + status: "in_progress", + }; + optimizationRepository.findOne.mockResolvedValue(pendingOptimization); + + await expect( + service.approveOptimization("opt-1"), + ).rejects.toThrow(BadRequestException); + }); + }); + + describe("implementOptimization", () => { + it("should implement approved optimization", async () => { + const approvedOptimization = { + ...mockOptimization, + status: "approved", + }; + optimizationRepository.findOne.mockResolvedValue(approvedOptimization); + portfolioRepository.findOne.mockResolvedValue(mockPortfolio); + + const result = await service.implementOptimization("opt-1"); + + expect(result.targetAllocation).toEqual( + approvedOptimization.suggestedAllocation, ); + expect(optimizationRepository.save).toHaveBeenCalled(); + }); + + it("should throw if optimization not found", async () => { + optimizationRepository.findOne.mockResolvedValue(null); - expect(result.status).toBe('completed'); + await expect( + service.implementOptimization("non-existent"), + ).rejects.toThrow(NotFoundException); + }); + + it("should throw if optimization is not approved", async () => { + optimizationRepository.findOne.mockResolvedValue(mockOptimization); + + await expect( + service.implementOptimization("opt-1"), + ).rejects.toThrow(BadRequestException); }); }); }); From 3bc2ad9f65df3d5e72d23ae297e5a47e14adee1b Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 27 Jun 2026 05:37:26 -0500 Subject: [PATCH 2/4] build fix --- .github/prompts/autonomy-version.json | 3 + .github/prompts/autonomy.manifest.json | 8 +++ .kilo/kilo.jsonc | 4 ++ src/investment/portfolio/dto/portfolio.dto.ts | 5 +- .../portfolio/entities/portfolio.entity.ts | 19 +++--- .../portfolio/services/portfolio.service.ts | 65 +++++++++++++++---- 6 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 .github/prompts/autonomy-version.json create mode 100644 .github/prompts/autonomy.manifest.json create mode 100644 .kilo/kilo.jsonc diff --git a/.github/prompts/autonomy-version.json b/.github/prompts/autonomy-version.json new file mode 100644 index 0000000..39afc29 --- /dev/null +++ b/.github/prompts/autonomy-version.json @@ -0,0 +1,3 @@ +{ + "version": "2025.11" +} diff --git a/.github/prompts/autonomy.manifest.json b/.github/prompts/autonomy.manifest.json new file mode 100644 index 0000000..25f7f88 --- /dev/null +++ b/.github/prompts/autonomy.manifest.json @@ -0,0 +1,8 @@ +{ + "version": "2025.11", + "consent": { + "phrase": "", + "expiresMinutes": 0 + }, + "actions": [] +} \ No newline at end of file diff --git a/.kilo/kilo.jsonc b/.kilo/kilo.jsonc new file mode 100644 index 0000000..0603291 --- /dev/null +++ b/.kilo/kilo.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "https://app.kilo.ai/config.json", + "snapshot": false +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/portfolio.dto.ts b/src/investment/portfolio/dto/portfolio.dto.ts index 4253df6..e96ef09 100644 --- a/src/investment/portfolio/dto/portfolio.dto.ts +++ b/src/investment/portfolio/dto/portfolio.dto.ts @@ -5,8 +5,11 @@ import { IsNumber, IsBoolean, IsJSON, + IsObject, + Min, + Max, } from "class-validator"; -import { PortfolioStatus } from "../entities/portfolio.entity"; +import { PortfolioType, PortfolioStatus, AllocationStrategy } from "../entities/portfolio.entity"; export class CreatePortfolioDto { @IsString() name: string; diff --git a/src/investment/portfolio/entities/portfolio.entity.ts b/src/investment/portfolio/entities/portfolio.entity.ts index 1a3ad6e..8515d8e 100644 --- a/src/investment/portfolio/entities/portfolio.entity.ts +++ b/src/investment/portfolio/entities/portfolio.entity.ts @@ -31,6 +31,15 @@ export enum PortfolioType { OTHER = "other", } +export enum AllocationStrategy { + MANUAL = "manual", + EQUAL_WEIGHT = "equal_weight", + MARKET_CAP = "market_cap", + RISK_PARITY = "risk_parity", + MVO = "mvo", + CUSTOM = "custom", +} + @Entity("portfolios") @Index(["userId", "status"]) @Index(["userId", "createdAt"]) @@ -44,20 +53,12 @@ export class Portfolio { @Column({ type: "enum", enum: PortfolioType, - default: PortfolioType.CUSTOM, + default: PortfolioType.MANUAL, }) type: PortfolioType; - @Column({ type: "text", nullable: true }) description: string; - @Column({ - type: "enum", - enum: PortfolioType, - default: PortfolioType.MANUAL, - }) - type: PortfolioType; - @Column({ type: "enum", enum: PortfolioStatus, diff --git a/src/investment/portfolio/services/portfolio.service.ts b/src/investment/portfolio/services/portfolio.service.ts index 52b6a06..f08273b 100644 --- a/src/investment/portfolio/services/portfolio.service.ts +++ b/src/investment/portfolio/services/portfolio.service.ts @@ -47,6 +47,48 @@ export class PortfolioService { private auditLogService: AuditLogService, ) {} + private validatePortfolioName(name: string): void { + if (!name || name.trim().length === 0) { + throw new BadRequestException("Portfolio name cannot be empty"); + } + } + + private validateAllocation(allocation?: Record): void { + if (!allocation) return; + const sum = Object.values(allocation).reduce((acc, val) => acc + (val || 0), 0); + if (Math.abs(sum - 100) > 0.1) { + throw new BadRequestException( + `Allocation percentages must sum to 100, got ${sum}`, + ); + } + } + + async deletePortfolio(portfolioId: string): Promise { + const portfolio = await this.getPortfolio(portfolioId); + await this.portfolioRepository.remove(portfolio); + } + + async archivePortfolio( + portfolioId: string, + status: PortfolioStatus, + ): Promise { + const portfolio = await this.getPortfolio(portfolioId); + if (status === PortfolioStatus.ARCHIVED) { + portfolio.status = PortfolioStatus.ARCHIVED; + portfolio.deletedAt = new Date(); + } + return this.portfolioRepository.save(portfolio); + } + + async setTargetAllocation( + portfolioId: string, + allocations: { [ticker: string]: number }, + ): Promise { + const portfolio = await this.getPortfolio(portfolioId); + portfolio.targetAllocation = allocations; + return this.portfolioRepository.save(portfolio); + } + async createPortfolio(userId: string, dto: CreatePortfolioDto): Promise { this.validatePortfolioName(dto.name); this.validateAllocation(dto.initialAllocation); @@ -145,38 +187,39 @@ export class PortfolioService { ): Promise { const portfolio = await this.getPortfolio(portfolioId); - if (quantity <= 0) { + if (dto.quantity <= 0) { throw new BadRequestException("Quantity must be positive"); } - if (currentPrice < 0) { + if (dto.currentPrice !== undefined && dto.currentPrice < 0) { throw new BadRequestException("Current price cannot be negative"); } - let asset = await this.portfolioAssetRepository.findOne({ - where: { portfolioId, ticker }, + const existingAsset = await this.portfolioAssetRepository.findOne({ + where: { portfolioId, ticker: dto.ticker }, }); - if (existing) { + if (existingAsset) { throw new BadRequestException( "Holding with same ticker and chain already exists", ); } - asset.quantity = quantity; - asset.currentPrice = currentPrice; - asset.value = quantity * currentPrice; + const asset = this.portfolioAssetRepository.create({ + ...dto, + portfolioId, + value: dto.quantity * (dto.currentPrice || 0), + }); await this.validatePortfolioConstraints( portfolio, - [...(portfolio.assets || []), holding as PortfolioAsset], + [...(portfolio.assets || []), asset], dto, "ADD_HOLDING", ); - const saved = await this.portfolioAssetRepository.save(holding); + const saved = await this.portfolioAssetRepository.save(asset); - // Update portfolio metrics await this.updatePortfolioMetrics(portfolioId); return saved; From 740151a65f2aba5cec8709c7f76d2ccd1906d43f Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 27 Jun 2026 06:22:08 -0500 Subject: [PATCH 3/4] build fix --- package-lock.json | 16 ++++++++-------- package.json | 2 +- src/config/swagger.config.ts | 2 +- src/config/tracing.ts | 2 -- .../websocket/filters/ws-exception.filter.ts | 2 +- .../services/connection-pool.service.ts | 4 ++-- .../services/dashboard-client.service.ts | 4 ++-- .../websocket/services/reconnection.service.ts | 4 ++-- .../risk-management/risk-management.health.ts | 5 +++-- 9 files changed, 20 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce4c221..89f3ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,7 +85,7 @@ "@types/bull": "^3.15.9", "@types/express": "^4.17.25", "@types/jest": "^29.5.14", - "@types/node": "^20.19.30", + "@types/node": "^26.0.1", "@types/nodemailer": "^6.4.14", "@types/passport-jwt": "^3.0.8", "@types/serve-favicon": "^2.5.7", @@ -6729,12 +6729,12 @@ } }, "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/nodemailer": { @@ -18897,9 +18897,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index db6964d..a4d4743 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "@types/bull": "^3.15.9", "@types/express": "^4.17.25", "@types/jest": "^29.5.14", - "@types/node": "^20.19.30", + "@types/node": "^26.0.1", "@types/nodemailer": "^6.4.14", "@types/passport-jwt": "^3.0.8", "@types/serve-favicon": "^2.5.7", diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index a590b1e..37177b7 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -4,7 +4,7 @@ import { ConfigService } from "@nestjs/config"; export function setupSwagger(app: INestApplication): void { const configService = app.get(ConfigService); - const port = process.env.PORT || configService.get("PORT", 3001); + const port = Number(process.env.PORT || configService.get("PORT", 3001)); const config = new DocumentBuilder() .setTitle("alian-structure Backend API") diff --git a/src/config/tracing.ts b/src/config/tracing.ts index 332f1cd..4318326 100644 --- a/src/config/tracing.ts +++ b/src/config/tracing.ts @@ -64,8 +64,6 @@ function buildSdk(): NodeSDK { textMapPropagator: new W3CTraceContextPropagator(), instrumentations: [ getNodeAutoInstrumentations({ - // fs instrumentation is noisy; disable it - "@opentelemetry/instrumentation-fs": { enabled: false }, // Ensure HTTP instrumentation captures request/response headers "@opentelemetry/instrumentation-http": { headersToSpanAttributes: { diff --git a/src/dashboard/websocket/filters/ws-exception.filter.ts b/src/dashboard/websocket/filters/ws-exception.filter.ts index aed848c..5036ce5 100644 --- a/src/dashboard/websocket/filters/ws-exception.filter.ts +++ b/src/dashboard/websocket/filters/ws-exception.filter.ts @@ -16,7 +16,7 @@ export class WsExceptionFilter implements ExceptionFilter { let errorResponse: WsErrorResponse; if (exception instanceof WsException) { - const error = exception.getError(); + const error = (exception as WsException).getError(); if (typeof error === "string") { errorResponse = { diff --git a/src/dashboard/websocket/services/connection-pool.service.ts b/src/dashboard/websocket/services/connection-pool.service.ts index 32790a1..2262ff9 100644 --- a/src/dashboard/websocket/services/connection-pool.service.ts +++ b/src/dashboard/websocket/services/connection-pool.service.ts @@ -1,12 +1,12 @@ import { Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; -import { io, Socket } from "socket.io-client"; +import io from "socket.io-client"; import { v4 as uuidv4 } from "uuid"; export interface UpstreamConnection { id: string; serviceName: string; url: string; - socket: Socket; + socket: any; connectedAt: Date; lastHeartbeat: Date; isConnected: boolean; diff --git a/src/dashboard/websocket/services/dashboard-client.service.ts b/src/dashboard/websocket/services/dashboard-client.service.ts index 496e3d0..20a57dc 100644 --- a/src/dashboard/websocket/services/dashboard-client.service.ts +++ b/src/dashboard/websocket/services/dashboard-client.service.ts @@ -4,7 +4,7 @@ */ import { Injectable, Logger } from "@nestjs/common"; -import { io, Socket } from "socket.io-client"; +import io from "socket.io-client"; import { DashboardEvent, BufferedEvent, @@ -36,7 +36,7 @@ export interface EventHandler { export class DashboardClientService { private readonly logger = new Logger(DashboardClientService.name); - private socket: Socket | null = null; + private socket: any = null; private config: ClientConfig; private eventHandlers: Map> = new Map(); diff --git a/src/dashboard/websocket/services/reconnection.service.ts b/src/dashboard/websocket/services/reconnection.service.ts index 7b94dd9..ef2fc78 100644 --- a/src/dashboard/websocket/services/reconnection.service.ts +++ b/src/dashboard/websocket/services/reconnection.service.ts @@ -233,9 +233,9 @@ export class WebSocketClientManager { try { // Dynamic import for socket.io-client - const { io } = await import("socket.io-client"); + const socketIo = await import("socket.io-client"); - this.socket = io(url, { + this.socket = socketIo.default(url, { transports: ["websocket"], auth: { token }, query: { userId }, diff --git a/src/investment/risk-management/risk-management.health.ts b/src/investment/risk-management/risk-management.health.ts index 44e4ff7..93602b6 100644 --- a/src/investment/risk-management/risk-management.health.ts +++ b/src/investment/risk-management/risk-management.health.ts @@ -16,11 +16,12 @@ export class RiskManagementHealthIndicator extends HealthIndicator { const status = this.circuitBreaker.getStatus("default"); const isHealthy = status.state !== "OPEN"; - const result = this.getStatus(key, isHealthy, { + const result: HealthIndicatorResult = { + status: isHealthy ? "up" : "down", circuitBreakerState: status.state, failureCount: status.failureCount, lastFailureTime: status.lastFailureTime, - }); + }; if (isHealthy) return result; throw new HealthCheckError( From f4f5ffb7d95830245734389c27e4b68785063fae Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 27 Jun 2026 07:31:30 -0500 Subject: [PATCH 4/4] build fix --- find_types.js | 18 +++++++++++++ .../risk-management/risk-management.health.ts | 26 +++++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 find_types.js diff --git a/find_types.js b/find_types.js new file mode 100644 index 0000000..a7e23b0 --- /dev/null +++ b/find_types.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const path = require('path'); +const root = 'C:/Users/User/Desktop/alian_structure-api/node_modules/@nestjs'; +let found = []; +function walk(d) { + if (!fs.existsSync(d)) return; + for (const f of fs.readdirSync(d)) { + if (f.startsWith('.')) continue; + const fp = path.join(d, f); + if (fs.statSync(fp).isDirectory()) walk(fp); + else if (fp.endsWith('.d.ts') || fp.endsWith('.js')) { + const c = fs.readFileSync(fp, 'utf8'); + if (c.includes('HealthIndicatorStatus')) found.push(fp); + } + } +} +walk(root); +console.log(found.join('\n')); diff --git a/src/investment/risk-management/risk-management.health.ts b/src/investment/risk-management/risk-management.health.ts index 93602b6..fb5dc93 100644 --- a/src/investment/risk-management/risk-management.health.ts +++ b/src/investment/risk-management/risk-management.health.ts @@ -16,17 +16,27 @@ export class RiskManagementHealthIndicator extends HealthIndicator { const status = this.circuitBreaker.getStatus("default"); const isHealthy = status.state !== "OPEN"; - const result: HealthIndicatorResult = { - status: isHealthy ? "up" : "down", - circuitBreakerState: status.state, - failureCount: status.failureCount, - lastFailureTime: status.lastFailureTime, - }; + if (isHealthy) { + return { + [key]: { + status: "up", + circuitBreakerState: status.state, + failureCount: status.failureCount, + lastFailureTime: status.lastFailureTime, + }, + }; + } - if (isHealthy) return result; throw new HealthCheckError( "Risk management circuit breaker is open", - result, + { + [key]: { + status: "down", + circuitBreakerState: status.state, + failureCount: status.failureCount, + lastFailureTime: status.lastFailureTime, + }, + }, ); } }