345 lines
8.9 KiB
TypeScript
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();
|