Skip to main content
Glama
report-profit-loss.test.ts19 kB
/** * Tests for report_profit_loss tool */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { profitLossReportTool } from '../../../src/tools/report/report-profit-loss.js'; import { createMockClientWrapper } from '../../mocks/client.js'; import { mockProfitLossReportResponse, mockZeroProfitLossReportResponse, mockProfitLossReportWithLossResponse, } from '../../mocks/responses/report.js'; import { mockUnauthorizedError, mockRateLimitError, mockServerError, mockNetworkTimeoutError, mockInvalidAccountError, } from '../../mocks/errors/freshbooks-errors.js'; describe('report_profit_loss tool', () => { let mockClient: ReturnType<typeof createMockClientWrapper>; beforeEach(() => { mockClient = createMockClientWrapper(); vi.clearAllMocks(); }); describe('successful operations', () => { it('should return profit/loss report with positive net income', async () => { const mockResponse = mockProfitLossReportResponse(5000, 3000, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.startDate).toBe('2024-01-01'); expect(result.endDate).toBe('2024-01-31'); expect(result.revenue.amount).toBe('5000.00'); expect(result.expenses.amount).toBe('3000.00'); expect(result.netIncome.amount).toBe('2000.00'); expect(result.revenue.code).toBe('USD'); }); it('should return profit/loss report with negative net income (loss)', async () => { const mockResponse = mockProfitLossReportWithLossResponse(2000, 3000, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.amount).toBe('2000.00'); expect(result.expenses.amount).toBe('3000.00'); expect(result.netIncome.amount).toBe('-1000.00'); }); it('should return zero profit/loss when no activity', async () => { const mockResponse = mockZeroProfitLossReportResponse('2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.amount).toBe('0.00'); expect(result.expenses.amount).toBe('0.00'); expect(result.netIncome.amount).toBe('0.00'); }); it('should include detailed line items when available', async () => { const mockResponse = mockProfitLossReportResponse(10000, 7000, '2024-01-01', '2024-01-31', true); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.lines).toBeDefined(); expect(result.lines!.length).toBeGreaterThan(0); // Check for revenue category const revenueCategory = result.lines!.find((line: any) => line.category === 'Revenue'); expect(revenueCategory).toBeDefined(); expect(revenueCategory?.children).toBeDefined(); // Check for expenses category const expensesCategory = result.lines!.find((line: any) => line.category === 'Expenses'); expect(expensesCategory).toBeDefined(); expect(expensesCategory?.children).toBeDefined(); }); it('should handle single day date range', async () => { const mockResponse = mockProfitLossReportResponse(1000, 500, '2024-01-15', '2024-01-15'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-15', endDate: '2024-01-15', }, mockClient as any ); expect(result.startDate).toBe('2024-01-15'); expect(result.endDate).toBe('2024-01-15'); expect(result.revenue.amount).toBe('1000.00'); }); it('should handle full year date range', async () => { const mockResponse = mockProfitLossReportResponse(120000, 85000, '2024-01-01', '2024-12-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-12-31', }, mockClient as any ); expect(result.startDate).toBe('2024-01-01'); expect(result.endDate).toBe('2024-12-31'); expect(result.revenue.amount).toBe('120000.00'); expect(result.netIncome.amount).toBe('35000.00'); }); it('should handle quarterly date range', async () => { const mockResponse = mockProfitLossReportResponse(30000, 20000, '2024-01-01', '2024-03-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-03-31', }, mockClient as any ); expect(result.revenue.amount).toBe('30000.00'); expect(result.expenses.amount).toBe('20000.00'); expect(result.netIncome.amount).toBe('10000.00'); }); it('should handle revenue-only scenario (no expenses)', async () => { const mockResponse = mockProfitLossReportResponse(5000, 0, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.amount).toBe('5000.00'); expect(result.expenses.amount).toBe('0.00'); expect(result.netIncome.amount).toBe('5000.00'); }); it('should handle expenses-only scenario (no revenue)', async () => { const mockResponse = mockProfitLossReportResponse(0, 3000, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.amount).toBe('0.00'); expect(result.expenses.amount).toBe('3000.00'); expect(result.netIncome.amount).toBe('-3000.00'); }); }); describe('error handling', () => { it('should handle unauthorized error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockUnauthorizedError()), }, }; return apiCall(client); }); await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); it('should handle rate limit error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockRateLimitError(60)), }, }; return apiCall(client); }); await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); it('should handle server error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockServerError()), }, }; return apiCall(client); }); await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); it('should handle network timeout', async () => { mockClient.executeWithRetry.mockRejectedValueOnce(mockNetworkTimeoutError()); await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); it('should handle invalid account error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockInvalidAccountError('ABC123')), }, }; return apiCall(client); }); await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); }); describe('input validation', () => { it('should require accountId', async () => { await expect( profitLossReportTool.execute( { startDate: '2024-01-01', endDate: '2024-01-31', } as any, mockClient as any ) ).rejects.toThrow(); }); it('should require startDate', async () => { await expect( profitLossReportTool.execute( { accountId: 'ABC123', endDate: '2024-01-31', } as any, mockClient as any ) ).rejects.toThrow(); }); it('should require endDate', async () => { await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', } as any, mockClient as any ) ).rejects.toThrow(); }); it('should reject invalid date format for startDate', async () => { await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: 'invalid-date', endDate: '2024-01-31', }, mockClient as any ) ).rejects.toThrow(); }); it('should reject invalid date format for endDate', async () => { await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: 'not-a-date', }, mockClient as any ) ).rejects.toThrow(); }); it('should reject startDate after endDate', async () => { await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-12-31', endDate: '2024-01-01', }, mockClient as any ) ).rejects.toThrow(/start date must be before or equal to end date/i); }); it('should accept dates in correct YYYY-MM-DD format', async () => { const mockResponse = mockProfitLossReportResponse(1000, 500, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); // Should not throw await expect( profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ) ).resolves.toBeDefined(); }); }); describe('edge cases', () => { it('should handle leap year dates', async () => { const mockResponse = mockProfitLossReportResponse(1000, 500, '2024-02-29', '2024-02-29'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-02-29', endDate: '2024-02-29', }, mockClient as any ); expect(result.startDate).toBe('2024-02-29'); }); it('should handle year boundary dates', async () => { const mockResponse = mockProfitLossReportResponse(5000, 3000, '2023-12-31', '2024-01-01'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2023-12-31', endDate: '2024-01-01', }, mockClient as any ); expect(result.revenue.amount).toBe('5000.00'); }); it('should handle very large revenue amounts', async () => { const mockResponse = mockProfitLossReportResponse(9999999, 5000000, '2024-01-01', '2024-12-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-12-31', }, mockClient as any ); expect(result.revenue.amount).toBe('9999999.00'); expect(result.netIncome.amount).toBe('4999999.00'); }); it('should handle very large loss amounts', async () => { const mockResponse = mockProfitLossReportWithLossResponse(100000, 5000000, '2024-01-01', '2024-12-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-12-31', }, mockClient as any ); expect(result.expenses.amount).toBe('5000000.00'); expect(result.netIncome.amount).toBe('-4900000.00'); }); it('should handle different currency codes', async () => { const mockResponse = mockProfitLossReportResponse(5000, 3000, '2024-01-01', '2024-01-31'); mockResponse.data.revenue.code = 'CAD'; mockResponse.data.expenses.code = 'CAD'; mockResponse.data.netIncome.code = 'CAD'; mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.code).toBe('CAD'); expect(result.expenses.code).toBe('CAD'); expect(result.netIncome.code).toBe('CAD'); }); it('should handle break-even scenario (revenue equals expenses)', async () => { const mockResponse = mockProfitLossReportResponse(5000, 5000, '2024-01-01', '2024-01-31'); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { reports: { profitLoss: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await profitLossReportTool.execute( { accountId: 'ABC123', startDate: '2024-01-01', endDate: '2024-01-31', }, mockClient as any ); expect(result.revenue.amount).toBe('5000.00'); expect(result.expenses.amount).toBe('5000.00'); expect(result.netIncome.amount).toBe('0.00'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Good-Samaritan-Software-LLC/freshbooks-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server