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

548 lines
15 KiB
TypeScript

/**
* Investment Account Service Unit Tests
*
* Tests for investment account service including:
* - Account creation and management
* - Balance tracking
* - Performance calculations
* - Account status management
*/
import { mockDb, createMockQueryResult, resetDatabaseMocks } from '../../../../__tests__/mocks/database.mock';
// Mock database (account service uses in-memory storage)
jest.mock('../../../../shared/database', () => ({
db: mockDb,
}));
// Mock logger
jest.mock('../../../../shared/utils/logger', () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
},
}));
// Mock product service
const mockGetProductById = jest.fn();
jest.mock('../product.service', () => ({
productService: {
getProductById: mockGetProductById,
getAllProducts: jest.fn(),
},
}));
// Import service after mocks
import { accountService } from '../account.service';
describe('AccountService', () => {
beforeEach(() => {
resetDatabaseMocks();
mockGetProductById.mockReset();
jest.clearAllMocks();
});
describe('createAccount', () => {
it('should create a new investment account', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas - El Guardián',
riskProfile: 'conservative',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValueOnce(mockProduct);
const result = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
expect(result.userId).toBe('user-123');
expect(result.productId).toBe('product-123');
expect(result.balance).toBe(1000);
expect(result.initialInvestment).toBe(1000);
expect(result.status).toBe('active');
});
it('should validate minimum investment amount', async () => {
const mockProduct = {
id: 'product-123',
code: 'orion',
name: 'Orion - El Explorador',
riskProfile: 'moderate',
minInvestment: 500,
isActive: true,
};
mockGetProductById.mockResolvedValueOnce(mockProduct);
await expect(
accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 100,
})
).rejects.toThrow('Minimum investment is 500');
});
it('should reject inactive products', async () => {
const mockProduct = {
id: 'product-124',
code: 'inactive',
name: 'Inactive Product',
riskProfile: 'moderate',
minInvestment: 100,
isActive: false,
};
mockGetProductById.mockResolvedValueOnce(mockProduct);
await expect(
accountService.createAccount({
userId: 'user-123',
productId: 'product-124',
initialDeposit: 1000,
})
).rejects.toThrow('Product is not active');
});
it('should prevent duplicate accounts for same product', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
riskProfile: 'conservative',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await expect(
accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 500,
})
).rejects.toThrow('Account already exists for this product');
});
it('should handle non-existent product', async () => {
mockGetProductById.mockResolvedValueOnce(null);
await expect(
accountService.createAccount({
userId: 'user-123',
productId: 'non-existent',
initialDeposit: 1000,
})
).rejects.toThrow('Product not found');
});
});
describe('getUserAccounts', () => {
it('should retrieve all accounts for a user', async () => {
const mockProducts = [
{ id: 'product-1', code: 'atlas', name: 'Atlas' },
{ id: 'product-2', code: 'orion', name: 'Orion' },
];
mockGetProductById.mockImplementation((id) =>
Promise.resolve(mockProducts.find(p => p.id === id))
);
await accountService.createAccount({
userId: 'user-123',
productId: 'product-1',
initialDeposit: 1000,
});
await accountService.createAccount({
userId: 'user-123',
productId: 'product-2',
initialDeposit: 2000,
});
const result = await accountService.getUserAccounts('user-123');
expect(result).toHaveLength(2);
expect(result[0].userId).toBe('user-123');
expect(result[1].userId).toBe('user-123');
expect(result[0].product).toBeDefined();
});
it('should return empty array for user with no accounts', async () => {
const result = await accountService.getUserAccounts('user-999');
expect(result).toEqual([]);
});
it('should include product information', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas - El Guardián',
riskProfile: 'conservative',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.getUserAccounts('user-123');
expect(result[0].product).toEqual(mockProduct);
});
});
describe('getAccountById', () => {
it('should retrieve an account by ID', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const created = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.getAccountById(created.id);
expect(result).toBeDefined();
expect(result?.id).toBe(created.id);
expect(result?.product).toBeDefined();
});
it('should return null for non-existent account', async () => {
const result = await accountService.getAccountById('non-existent');
expect(result).toBeNull();
});
});
describe('getAccountByUserAndProduct', () => {
it('should retrieve account by user and product', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.getAccountByUserAndProduct('user-123', 'product-123');
expect(result).toBeDefined();
expect(result?.userId).toBe('user-123');
expect(result?.productId).toBe('product-123');
});
it('should return null if account does not exist', async () => {
const result = await accountService.getAccountByUserAndProduct('user-999', 'product-999');
expect(result).toBeNull();
});
it('should exclude closed accounts', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await accountService.closeAccount(account.id);
const result = await accountService.getAccountByUserAndProduct('user-123', 'product-123');
expect(result).toBeNull();
});
});
describe('updateAccountBalance', () => {
it('should update account balance', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.updateAccountBalance(account.id, 1500);
expect(result.balance).toBe(1500);
expect(result.updatedAt).toBeDefined();
});
it('should calculate unrealized P&L', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.updateAccountBalance(account.id, 1200);
expect(result.unrealizedPnl).toBe(200);
expect(result.unrealizedPnlPercent).toBe(20);
});
it('should handle account not found', async () => {
await expect(
accountService.updateAccountBalance('non-existent', 1000)
).rejects.toThrow('Account not found');
});
});
describe('closeAccount', () => {
it('should close an active account', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.closeAccount(account.id);
expect(result.status).toBe('closed');
expect(result.closedAt).toBeDefined();
});
it('should require zero balance to close', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await expect(accountService.closeAccount(account.id)).rejects.toThrow(
'Cannot close account with non-zero balance'
);
});
it('should prevent closing already closed account', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await accountService.updateAccountBalance(account.id, 0);
await accountService.closeAccount(account.id);
await expect(accountService.closeAccount(account.id)).rejects.toThrow(
'Account is already closed'
);
});
});
describe('suspendAccount', () => {
it('should suspend an active account', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const result = await accountService.suspendAccount(account.id);
expect(result.status).toBe('suspended');
});
it('should prevent operations on suspended account', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await accountService.suspendAccount(account.id);
await expect(
accountService.updateAccountBalance(account.id, 1500)
).rejects.toThrow('Account is suspended');
});
});
describe('getAccountSummary', () => {
it('should calculate account summary for user', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
const account2 = await accountService.createAccount({
userId: 'user-123',
productId: 'product-124',
initialDeposit: 2000,
});
await accountService.updateAccountBalance(account2.id, 2500);
const result = await accountService.getAccountSummary('user-123');
expect(result.totalBalance).toBeGreaterThan(0);
expect(result.totalDeposited).toBe(3000);
expect(result.totalEarnings).toBeGreaterThan(0);
expect(result.overallReturn).toBeGreaterThan(0);
expect(result.accounts).toHaveLength(2);
});
it('should handle user with no accounts', async () => {
const result = await accountService.getAccountSummary('user-999');
expect(result.totalBalance).toBe(0);
expect(result.totalDeposited).toBe(0);
expect(result.accounts).toEqual([]);
});
it('should exclude closed accounts from summary', async () => {
const mockProduct = {
id: 'product-123',
code: 'atlas',
name: 'Atlas',
minInvestment: 100,
isActive: true,
};
mockGetProductById.mockResolvedValue(mockProduct);
const account = await accountService.createAccount({
userId: 'user-123',
productId: 'product-123',
initialDeposit: 1000,
});
await accountService.updateAccountBalance(account.id, 0);
await accountService.closeAccount(account.id);
const result = await accountService.getAccountSummary('user-123');
expect(result.accounts).toHaveLength(0);
expect(result.totalBalance).toBe(0);
});
});
});