trading-platform/apps/backend/src/modules/investment/services/account.service.ts

345 lines
8.9 KiB
TypeScript

/**
* 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<string, InvestmentAccount> = new Map();
// ============================================================================
// Account Service
// ============================================================================
class AccountService {
/**
* Get all accounts for a user
*/
async getUserAccounts(userId: string): Promise<InvestmentAccount[]> {
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<InvestmentAccount | null> {
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<InvestmentAccount | null> {
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<InvestmentAccount> {
// 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<InvestmentAccount> {
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<InvestmentAccount> {
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<InvestmentAccount> {
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<InvestmentAccount> {
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<InvestmentAccount> {
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<InvestmentAccount> {
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<AccountSummary> {
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();