fix(coherence): Create centralized portfolio types (E-COH-004, ST1.3)

- Created apps/backend/src/modules/portfolio/types/portfolio.types.ts
- Consolidated all portfolio type definitions from DDL
- Updated repositories to import from centralized types
- Updated service to use centralized types (removed duplicates)
- Updated controller to import RiskProfile from types
- Removed 73 lines of duplicate type definitions

Fixes coherence gap E-COH-004 where portfolio types were absent in backend.
All types now align with DDL schema (portfolio.* enums and tables).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-26 17:02:04 -06:00
parent 3bb215b51b
commit 1be94f0c1f
6 changed files with 256 additions and 105 deletions

View File

@ -4,8 +4,9 @@
*/
import { Request, Response, NextFunction } from 'express';
import { portfolioService, RiskProfile } from '../services/portfolio.service';
import { portfolioService } from '../services/portfolio.service';
import { snapshotRepository } from '../repositories/snapshot.repository';
import type { RiskProfile } from '../types/portfolio.types';
// ============================================================================
// Types

View File

@ -4,13 +4,12 @@
*/
import { db } from '../../../shared/database';
import type { GoalStatus, PortfolioGoal } from '../types/portfolio.types';
// ============================================================================
// Types
// Database Row Types (snake_case from DB)
// ============================================================================
export type GoalStatus = 'active' | 'completed' | 'abandoned';
export interface GoalRow {
id: string;
user_id: string;
@ -31,25 +30,7 @@ export interface GoalRow {
updated_at: Date;
}
export interface PortfolioGoal {
id: string;
userId: string;
portfolioId: string | null;
name: string;
description: string | null;
targetAmount: number;
currentAmount: number;
targetDate: Date;
monthlyContribution: number;
progress: number;
projectedCompletionDate: Date | null;
monthsRemaining: number | null;
requiredMonthlyContribution: number | null;
status: GoalStatus;
completedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
// PortfolioGoal interface imported from types/portfolio.types.ts
export interface CreateGoalInput {
userId: string;

View File

@ -4,14 +4,17 @@
*/
import { db } from '../../../shared/database';
import type {
RiskProfile,
AllocationStatus,
Portfolio,
PortfolioAllocation,
} from '../types/portfolio.types';
// ============================================================================
// Types
// Database Row Types (snake_case from DB)
// ============================================================================
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
export type AllocationStatus = 'active' | 'inactive' | 'rebalancing';
export interface PortfolioRow {
id: string;
user_id: string;
@ -33,26 +36,7 @@ export interface PortfolioRow {
updated_at: Date;
}
export interface Portfolio {
id: string;
userId: string;
name: string;
description: string | null;
riskProfile: RiskProfile;
totalValue: number;
totalCost: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
dayChangePercent: number;
weekChangePercent: number;
monthChangePercent: number;
allTimeChangePercent: number;
isActive: boolean;
isPrimary: boolean;
lastRebalancedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
// Portfolio interface imported from types/portfolio.types.ts
export interface AllocationRow {
id: string;
@ -73,24 +57,7 @@ export interface AllocationRow {
created_at: Date;
}
export interface PortfolioAllocation {
id: string;
portfolioId: string;
asset: string;
targetPercent: number;
currentPercent: number;
deviation: number;
quantity: number;
avgCost: number;
currentPrice: number;
value: number;
cost: number;
pnl: number;
pnlPercent: number;
status: AllocationStatus;
lastUpdatedAt: Date;
createdAt: Date;
}
// PortfolioAllocation interface imported from types/portfolio.types.ts
export interface CreatePortfolioInput {
userId: string;

View File

@ -4,9 +4,10 @@
*/
import { db } from '../../../shared/database';
import type { PortfolioSnapshot, PerformanceDataPoint } from '../types/portfolio.types';
// ============================================================================
// Types
// Database Row Types (snake_case from DB)
// ============================================================================
export interface SnapshotRow {
@ -23,19 +24,7 @@ export interface SnapshotRow {
created_at: Date;
}
export interface PortfolioSnapshot {
id: string;
portfolioId: string;
snapshotDate: Date;
totalValue: number;
totalCost: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
dayChange: number;
dayChangePercent: number;
allocations: Record<string, unknown> | null;
createdAt: Date;
}
// PortfolioSnapshot interface imported from types/portfolio.types.ts
export interface CreateSnapshotInput {
portfolioId: string;
@ -49,14 +38,7 @@ export interface CreateSnapshotInput {
allocations?: Record<string, unknown>;
}
export interface PerformanceDataPoint {
date: string;
value: number;
pnl: number;
pnlPercent: number;
change: number;
changePercent: number;
}
// PerformanceDataPoint interface imported from types/portfolio.types.ts
// ============================================================================
// Helper Functions

View File

@ -7,23 +7,19 @@
import { v4 as uuidv4 } from 'uuid';
import { marketService } from '../../trading/services/market.service';
import {
portfolioRepository,
Portfolio as RepoPortfolio,
PortfolioAllocation as RepoAllocation,
RiskProfile as RepoRiskProfile,
} from '../repositories/portfolio.repository';
import {
goalRepository,
PortfolioGoal as RepoGoal,
} from '../repositories/goal.repository';
import { portfolioRepository } from '../repositories/portfolio.repository';
import { goalRepository } from '../repositories/goal.repository';
import type {
Portfolio as DBPortfolio,
PortfolioAllocation as DBAllocation,
PortfolioGoal as DBGoal,
RiskProfile,
} from '../types/portfolio.types';
// ============================================================================
// Types
// Service DTOs (with computed/additional fields)
// ============================================================================
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
export interface Portfolio {
id: string;
userId: string;
@ -125,8 +121,8 @@ const DEFAULT_ALLOCATIONS: Record<RiskProfile, { asset: string; percent: number
// ============================================================================
function mapRepoPortfolioToService(
repo: RepoPortfolio,
allocations: RepoAllocation[]
repo: DBPortfolio,
allocations: DBAllocation[]
): Portfolio {
return {
id: repo.id,
@ -145,7 +141,7 @@ function mapRepoPortfolioToService(
};
}
function mapRepoAllocationToService(repo: RepoAllocation): PortfolioAllocation {
function mapRepoAllocationToService(repo: DBAllocation): PortfolioAllocation {
return {
id: repo.id,
portfolioId: repo.portfolioId,
@ -161,7 +157,7 @@ function mapRepoAllocationToService(repo: RepoAllocation): PortfolioAllocation {
};
}
function mapRepoGoalToService(repo: RepoGoal): PortfolioGoal {
function mapRepoGoalToService(repo: DBGoal): PortfolioGoal {
// Map DB status to service status
let status: 'on_track' | 'at_risk' | 'behind' = 'on_track';
if (repo.status === 'completed') {
@ -223,7 +219,7 @@ class PortfolioService {
const repoPortfolio = await portfolioRepository.create({
userId,
name,
riskProfile: riskProfile as RepoRiskProfile,
riskProfile,
totalValue: initialValue,
totalCost: initialValue,
});

View File

@ -0,0 +1,224 @@
/**
* Portfolio Module Types
* Type definitions for portfolio management, allocations, goals, and rebalancing
* Aligned with portfolio schema DDL (00-enums.sql)
*/
// ============================================================================
// Portfolio Enums (Alineado con portfolio.* DDL)
// ============================================================================
// Alineado con portfolio.risk_profile (DDL)
// Nota: También existe en investment schema
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
export enum RiskProfileEnum {
CONSERVATIVE = 'conservative',
MODERATE = 'moderate',
AGGRESSIVE = 'aggressive',
}
// Alineado con portfolio.goal_status (DDL)
export type GoalStatus = 'active' | 'completed' | 'abandoned';
export enum GoalStatusEnum {
ACTIVE = 'active',
COMPLETED = 'completed',
ABANDONED = 'abandoned',
}
// Alineado con portfolio.rebalance_action (DDL)
export type RebalanceAction = 'buy' | 'sell' | 'hold';
export enum RebalanceActionEnum {
BUY = 'buy',
SELL = 'sell',
HOLD = 'hold',
}
// Alineado con portfolio.allocation_status (DDL)
export type AllocationStatus = 'active' | 'inactive' | 'rebalancing';
export enum AllocationStatusEnum {
ACTIVE = 'active',
INACTIVE = 'inactive',
REBALANCING = 'rebalancing',
}
// ============================================================================
// Portfolio Interfaces
// ============================================================================
export interface Portfolio {
id: string;
userId: string;
name: string;
description: string | null;
riskProfile: RiskProfile;
totalValue: number;
totalCost: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
dayChangePercent: number;
weekChangePercent: number;
monthChangePercent: number;
allTimeChangePercent: number;
isActive: boolean;
isPrimary: boolean;
lastRebalancedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface PortfolioAllocation {
id: string;
portfolioId: string;
asset: string;
targetPercent: number;
currentPercent: number;
deviation: number;
quantity: number;
avgCost: number;
currentPrice: number;
value: number;
cost: number;
pnl: number;
pnlPercent: number;
status: AllocationStatus;
lastUpdatedAt: Date;
createdAt: Date;
}
export interface PortfolioGoal {
id: string;
userId: string;
portfolioId: string | null;
name: string;
description: string | null;
targetAmount: number;
currentAmount: number;
targetDate: Date;
monthlyContribution: number;
progress: number;
projectedCompletionDate: Date | null;
monthsRemaining: number | null;
requiredMonthlyContribution: number | null;
status: GoalStatus;
completedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface RebalanceHistory {
id: string;
portfolioId: string;
asset: string;
action: RebalanceAction;
targetPercent: number;
actualPercentBefore: number;
actualPercentAfter: number;
quantityChange: number;
valueChange: number;
priceAtRebalance: number;
executedAt: Date;
executionStatus: string;
executionNotes: string | null;
createdAt: Date;
}
export interface PortfolioSnapshot {
id: string;
portfolioId: string;
snapshotDate: Date;
totalValue: number;
totalCost: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
dayChange: number;
dayChangePercent: number;
allocations: Record<string, unknown> | null;
createdAt: Date;
}
// ============================================================================
// Input/Output DTOs
// ============================================================================
export interface CreatePortfolioInput {
userId: string;
name: string;
description?: string;
riskProfile: RiskProfile;
}
export interface UpdatePortfolioInput {
name?: string;
description?: string;
riskProfile?: RiskProfile;
isActive?: boolean;
isPrimary?: boolean;
}
export interface CreateAllocationInput {
portfolioId: string;
asset: string;
targetPercent: number;
quantity?: number;
avgCost?: number;
}
export interface UpdateAllocationInput {
targetPercent?: number;
quantity?: number;
avgCost?: number;
status?: AllocationStatus;
}
export interface CreateGoalInput {
userId: string;
portfolioId?: string;
name: string;
description?: string;
targetAmount: number;
targetDate: Date;
monthlyContribution?: number;
}
export interface UpdateGoalInput {
name?: string;
description?: string;
targetAmount?: number;
targetDate?: Date;
monthlyContribution?: number;
currentAmount?: number;
status?: GoalStatus;
}
export interface RebalanceRecommendation {
asset: string;
action: RebalanceAction;
currentPercent: number;
targetPercent: number;
deviation: number;
quantityChange: number;
valueChange: number;
priority: number;
}
export interface PerformanceDataPoint {
date: string;
value: number;
pnl: number;
pnlPercent: number;
change: number;
changePercent: number;
}
export interface PortfolioSummary {
portfolio: Portfolio;
allocations: PortfolioAllocation[];
totalAllocated: number;
unallocatedPercent: number;
needsRebalance: boolean;
lastSnapshot: PortfolioSnapshot | null;
}