/** * Investment Account Service * Manages user investment accounts */ import { v4 as uuidv4 } from 'uuid'; import { productService, InvestmentProduct } from './product.service'; // ============================================================================ // Types // ============================================================================ export type AccountStatus = 'active' | 'suspended' | 'closed'; export interface InvestmentAccount { id: string; userId: string; productId: string; product?: InvestmentProduct; status: AccountStatus; balance: number; initialInvestment: number; totalDeposited: number; totalWithdrawn: number; totalEarnings: number; totalFeesPaid: number; unrealizedPnl: number; unrealizedPnlPercent: number; openedAt: Date; closedAt: Date | null; updatedAt: Date; } export interface CreateAccountInput { userId: string; productId: string; initialDeposit: number; } export interface AccountSummary { totalBalance: number; totalEarnings: number; totalDeposited: number; totalWithdrawn: number; overallReturn: number; overallReturnPercent: number; accounts: InvestmentAccount[]; } // ============================================================================ // In-Memory Storage // ============================================================================ const accounts: Map = new Map(); // ============================================================================ // Account Service // ============================================================================ class AccountService { /** * Get all accounts for a user */ async getUserAccounts(userId: string): Promise { const userAccounts = Array.from(accounts.values()).filter((a) => a.userId === userId); // Attach product info for (const account of userAccounts) { account.product = (await productService.getProductById(account.productId)) || undefined; } return userAccounts; } /** * Get account by ID */ async getAccountById(accountId: string): Promise { const account = accounts.get(accountId); if (!account) return null; account.product = (await productService.getProductById(account.productId)) || undefined; return account; } /** * Get account by user and product */ async getAccountByUserAndProduct( userId: string, productId: string ): Promise { const account = Array.from(accounts.values()).find( (a) => a.userId === userId && a.productId === productId && a.status !== 'closed' ); if (!account) return null; account.product = (await productService.getProductById(account.productId)) || undefined; return account; } /** * Create a new investment account */ async createAccount(input: CreateAccountInput): Promise { // Validate product exists const product = await productService.getProductById(input.productId); if (!product) { throw new Error(`Product not found: ${input.productId}`); } // Check minimum investment if (input.initialDeposit < product.minInvestment) { throw new Error( `Minimum investment for ${product.name} is $${product.minInvestment}` ); } // Check if user already has an account with this product const existingAccount = await this.getAccountByUserAndProduct( input.userId, input.productId ); if (existingAccount) { throw new Error(`User already has an account with ${product.name}`); } const account: InvestmentAccount = { id: uuidv4(), userId: input.userId, productId: input.productId, product, status: 'active', balance: input.initialDeposit, initialInvestment: input.initialDeposit, totalDeposited: input.initialDeposit, totalWithdrawn: 0, totalEarnings: 0, totalFeesPaid: 0, unrealizedPnl: 0, unrealizedPnlPercent: 0, openedAt: new Date(), closedAt: null, updatedAt: new Date(), }; accounts.set(account.id, account); return account; } /** * Deposit funds to an account */ async deposit(accountId: string, amount: number): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } if (account.status !== 'active') { throw new Error(`Cannot deposit to ${account.status} account`); } if (amount <= 0) { throw new Error('Deposit amount must be positive'); } account.balance += amount; account.totalDeposited += amount; account.updatedAt = new Date(); return account; } /** * Record earnings for an account */ async recordEarnings( accountId: string, grossEarnings: number, performanceFee: number ): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } const netEarnings = grossEarnings - performanceFee; account.balance += netEarnings; account.totalEarnings += netEarnings; account.totalFeesPaid += performanceFee; account.updatedAt = new Date(); return account; } /** * Update unrealized P&L */ async updateUnrealizedPnl( accountId: string, unrealizedPnl: number ): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } account.unrealizedPnl = unrealizedPnl; account.unrealizedPnlPercent = account.totalDeposited > 0 ? (unrealizedPnl / account.totalDeposited) * 100 : 0; account.updatedAt = new Date(); return account; } /** * Close an account */ async closeAccount(accountId: string): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } if (account.status === 'closed') { throw new Error('Account is already closed'); } account.status = 'closed'; account.closedAt = new Date(); account.updatedAt = new Date(); return account; } /** * Suspend an account */ async suspendAccount(accountId: string): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } account.status = 'suspended'; account.updatedAt = new Date(); return account; } /** * Reactivate a suspended account */ async reactivateAccount(accountId: string): Promise { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } if (account.status !== 'suspended') { throw new Error('Only suspended accounts can be reactivated'); } account.status = 'active'; account.updatedAt = new Date(); return account; } /** * Get account summary for a user */ async getAccountSummary(userId: string): Promise { const userAccounts = await this.getUserAccounts(userId); const summary: AccountSummary = { totalBalance: 0, totalEarnings: 0, totalDeposited: 0, totalWithdrawn: 0, overallReturn: 0, overallReturnPercent: 0, accounts: userAccounts, }; for (const account of userAccounts) { if (account.status !== 'closed') { summary.totalBalance += account.balance; summary.totalEarnings += account.totalEarnings; summary.totalDeposited += account.totalDeposited; summary.totalWithdrawn += account.totalWithdrawn; } } summary.overallReturn = summary.totalBalance - summary.totalDeposited + summary.totalWithdrawn; summary.overallReturnPercent = summary.totalDeposited > 0 ? (summary.overallReturn / summary.totalDeposited) * 100 : 0; return summary; } /** * Get account performance history */ async getAccountPerformance( accountId: string, days: number = 30 ): Promise<{ date: string; balance: number; pnl: number }[]> { const account = accounts.get(accountId); if (!account) { throw new Error(`Account not found: ${accountId}`); } // Generate mock performance data const performance: { date: string; balance: number; pnl: number }[] = []; let balance = account.initialInvestment; for (let i = days; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const dailyChange = balance * ((Math.random() - 0.3) * 0.02); balance += dailyChange; performance.push({ date: date.toISOString().split('T')[0], balance, pnl: balance - account.initialInvestment, }); } return performance; } } // Export singleton instance export const accountService = new AccountService();