/** * 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); }); }); });