Skip to main content
Glama
journalentry-create.test.ts28.6 kB
/** * Tests for journalentry_create tool */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { journalEntryCreateTool } from '../../../src/tools/journal-entry/journalentry-create.js'; import { createMockClientWrapper } from '../../mocks/client.js'; import { mockJournalEntryCreateResponse, mockJournalEntryUnbalancedError, mockJournalEntryInsufficientDetailsError, mockJournalEntryInvalidSubAccountError, mockJournalEntryInvalidDateError, mockJournalEntryMissingAmountError, mockJournalEntryNegativeAmountError, mockJournalEntryArchivedAccountError, mockJournalEntryPermissionError, } from '../../mocks/responses/journal-entry.js'; import { mockUnauthorizedError, mockRateLimitError, mockServerError, mockNetworkTimeoutError, } from '../../mocks/errors/freshbooks-errors.js'; describe('journalentry_create tool', () => { let mockClient: ReturnType<typeof createMockClientWrapper>; const validInput = { accountId: 'ABC123', name: 'Monthly Depreciation', date: '2024-01-31', description: 'Record monthly equipment depreciation', details: [ { subAccountId: 100, debit: '1000.00', description: 'Depreciation expense', }, { subAccountId: 200, credit: '1000.00', description: 'Accumulated depreciation', }, ], }; beforeEach(() => { mockClient = createMockClientWrapper(); vi.clearAllMocks(); }); describe('successful operations', () => { it('should create a journal entry with required fields', async () => { const mockResponse = mockJournalEntryCreateResponse({ name: 'Monthly Depreciation', date: '2024-01-31', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute(validInput, mockClient as any); expect(result.id).toBe(99999); expect(result.name).toBe('Monthly Depreciation'); expect(result.details).toHaveLength(2); }); it('should create a journal entry with balanced debits and credits', async () => { const mockResponse = mockJournalEntryCreateResponse({ details: [ { subAccountId: 100, debit: '500.00', credit: null, description: 'Debit 1' }, { subAccountId: 101, debit: '300.00', credit: null, description: 'Debit 2' }, { subAccountId: 200, debit: null, credit: '800.00', description: 'Credit' }, ], }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '500.00', description: 'Debit 1' }, { subAccountId: 101, debit: '300.00', description: 'Debit 2' }, { subAccountId: 200, credit: '800.00', description: 'Credit' }, ], }, mockClient as any ); expect(result.details).toHaveLength(3); }); it('should create a journal entry with multiple detail lines', async () => { const mockResponse = mockJournalEntryCreateResponse({ details: [ { subAccountId: 100, debit: '250.00', credit: null }, { subAccountId: 101, debit: '250.00', credit: null }, { subAccountId: 200, debit: null, credit: '300.00' }, { subAccountId: 201, debit: null, credit: '200.00' }, ], }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '250.00' }, { subAccountId: 101, debit: '250.00' }, { subAccountId: 200, credit: '300.00' }, { subAccountId: 201, credit: '200.00' }, ], }, mockClient as any ); expect(result.details).toHaveLength(4); }); it('should create a journal entry with optional description', async () => { const mockResponse = mockJournalEntryCreateResponse({ description: 'Detailed accounting adjustment explanation', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, description: 'Detailed accounting adjustment explanation', }, mockClient as any ); expect(result.description).toBe('Detailed accounting adjustment explanation'); }); it('should create a journal entry with custom currency', async () => { const mockResponse = mockJournalEntryCreateResponse({ currencyCode: 'EUR', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, currencyCode: 'EUR', }, mockClient as any ); expect(result.currencyCode).toBe('EUR'); }); it('should create a journal entry with detail line descriptions', async () => { const mockResponse = mockJournalEntryCreateResponse({ details: [ { subAccountId: 100, debit: '1000.00', credit: null, description: 'Equipment depreciation expense', }, { subAccountId: 200, debit: null, credit: '1000.00', description: 'Accumulated depreciation - Equipment', }, ], }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00', description: 'Equipment depreciation expense', }, { subAccountId: 200, credit: '1000.00', description: 'Accumulated depreciation - Equipment', }, ], }, mockClient as any ); expect(result.details[0].description).toBe('Equipment depreciation expense'); expect(result.details[1].description).toBe('Accumulated depreciation - Equipment'); }); it('should create a journal entry with decimal precision', async () => { const mockResponse = mockJournalEntryCreateResponse({ details: [ { subAccountId: 100, debit: '123.45', credit: null }, { subAccountId: 200, debit: null, credit: '123.45' }, ], }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '123.45' }, { subAccountId: 200, credit: '123.45' }, ], }, mockClient as any ); expect(result.details[0].debit).toBe('123.45'); expect(result.details[1].credit).toBe('123.45'); }); it('should create a journal entry with large amounts', async () => { const mockResponse = mockJournalEntryCreateResponse({ details: [ { subAccountId: 100, debit: '99999999.99', credit: null }, { subAccountId: 200, debit: null, credit: '99999999.99' }, ], }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '99999999.99' }, { subAccountId: 200, credit: '99999999.99' }, ], }, mockClient as any ); expect(result.details[0].debit).toBe('99999999.99'); }); }); describe('error handling', () => { it('should handle unauthorized error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockUnauthorizedError()), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle rate limit error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockRateLimitError(60)), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle server error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockServerError()), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle network timeout', async () => { mockClient.executeWithRetry.mockRejectedValueOnce(mockNetworkTimeoutError()); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle unbalanced journal entry', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '500.00' }, // Unbalanced! ], }, mockClient as any ) ).rejects.toThrow(/must balance/i); }); it('should handle insufficient detail lines', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, // Only 1 line - need minimum 2 ], }, mockClient as any ) ).rejects.toThrow(); }); it('should handle invalid sub-account ID', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockJournalEntryInvalidSubAccountError(99999)), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle archived account error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockJournalEntryArchivedAccountError('ABC123')), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); it('should handle permission error', async () => { mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockJournalEntryPermissionError()), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute(validInput, mockClient as any) ).rejects.toThrow(); }); }); describe('input validation', () => { it('should require accountId', async () => { const { accountId, ...inputWithoutAccount } = validInput; await expect( journalEntryCreateTool.execute(inputWithoutAccount as any, mockClient as any) ).rejects.toThrow(); }); it('should require name', async () => { const { name, ...inputWithoutName } = validInput; await expect( journalEntryCreateTool.execute(inputWithoutName as any, mockClient as any) ).rejects.toThrow(); }); it('should reject empty name', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, name: '' }, mockClient as any ) ).rejects.toThrow(); }); it('should require date', async () => { const { date, ...inputWithoutDate } = validInput; await expect( journalEntryCreateTool.execute(inputWithoutDate as any, mockClient as any) ).rejects.toThrow(); }); it('should reject invalid date format', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, date: '2024/01/31' }, // Wrong format mockClient as any ) ).rejects.toThrow(); }); it('should reject invalid date format (ISO 8601)', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, date: '2024-01-31T10:00:00Z' }, // ISO format not accepted mockClient as any ) ).rejects.toThrow(); }); it('should reject invalid date values', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, date: '2024-13-31' }, // Invalid month mockClient as any ) ).rejects.toThrow(); }); it('should require details array', async () => { const { details, ...inputWithoutDetails } = validInput; await expect( journalEntryCreateTool.execute(inputWithoutDetails as any, mockClient as any) ).rejects.toThrow(); }); it('should require at least 2 detail lines', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [{ subAccountId: 100, debit: '1000.00' }], }, mockClient as any ) ).rejects.toThrow(); }); it('should require subAccountId in each detail', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { debit: '1000.00' } as any, { subAccountId: 200, credit: '1000.00' }, ], }, mockClient as any ) ).rejects.toThrow(); }); it('should accept details with only debit', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '1000.00' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should accept details with only credit', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '1000.00' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); }); describe('debit/credit balance validation', () => { it('should reject when debits exceed credits', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '500.00' }, ], }, mockClient as any ) ).rejects.toThrow(/must balance/i); }); it('should reject when credits exceed debits', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '500.00' }, { subAccountId: 200, credit: '1000.00' }, ], }, mockClient as any ) ).rejects.toThrow(/must balance/i); }); it('should accept balanced entry with multiple debits and credits', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '600.00' }, { subAccountId: 101, debit: '400.00' }, { subAccountId: 200, credit: '700.00' }, { subAccountId: 201, credit: '300.00' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should handle floating point precision in balance calculation', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); // 0.1 + 0.2 = 0.30000000000000004 in floating point await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '0.10' }, { subAccountId: 101, debit: '0.20' }, { subAccountId: 200, credit: '0.30' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should reject imbalance beyond tolerance threshold', async () => { // Difference of 0.02 should fail (> 0.01 tolerance) await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '1000.02' }, ], }, mockClient as any ) ).rejects.toThrow(/must balance/i); }); it('should accept balance within tolerance threshold', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); // Difference of 0.01 should pass (exactly at tolerance) await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1000.00' }, { subAccountId: 200, credit: '1000.01' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should validate balance with complex entry', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, name: 'Complex adjustment', details: [ { subAccountId: 100, debit: '1234.56' }, { subAccountId: 101, debit: '678.90' }, { subAccountId: 102, debit: '100.00' }, { subAccountId: 200, credit: '1500.00' }, { subAccountId: 201, credit: '513.46' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should provide detailed error message for unbalanced entry', async () => { await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '1500.00' }, { subAccountId: 200, credit: '1000.00' }, ], }, mockClient as any ) ).rejects.toThrow(/Debits: 1500\.00, Credits: 1000\.00, Difference: 500\.00/); }); }); describe('edge cases', () => { it('should handle zero amounts', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '0.00' }, { subAccountId: 200, credit: '0.00' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should handle very small amounts', async () => { const mockResponse = mockJournalEntryCreateResponse(); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); await expect( journalEntryCreateTool.execute( { ...validInput, details: [ { subAccountId: 100, debit: '0.01' }, { subAccountId: 200, credit: '0.01' }, ], }, mockClient as any ) ).resolves.toBeDefined(); }); it('should handle unicode in name', async () => { const mockResponse = mockJournalEntryCreateResponse({ name: '日本語テスト Entry 🔢', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, name: '日本語テスト Entry 🔢', }, mockClient as any ); expect(result.name).toBe('日本語テスト Entry 🔢'); }); it('should handle unicode in descriptions', async () => { const mockResponse = mockJournalEntryCreateResponse({ description: 'Entrée comptable avec caractères spéciaux: €, £, ¥', }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, description: 'Entrée comptable avec caractères spéciaux: €, £, ¥', }, mockClient as any ); expect(result.description).toBe('Entrée comptable avec caractères spéciaux: €, £, ¥'); }); it('should handle long names', async () => { const longName = 'A'.repeat(500); const mockResponse = mockJournalEntryCreateResponse({ name: longName }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, name: longName, }, mockClient as any ); expect(result.name).toBe(longName); }); it('should handle maximum detail lines', async () => { const manyDetails = Array.from({ length: 50 }, (_, i) => ({ subAccountId: 100 + i, debit: i % 2 === 0 ? '20.00' : undefined, credit: i % 2 === 1 ? '20.00' : undefined, })); const mockResponse = mockJournalEntryCreateResponse({ details: manyDetails }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, details: manyDetails, }, mockClient as any ); expect(result.details).toHaveLength(50); }); it('should handle entries on leap year date', async () => { const mockResponse = mockJournalEntryCreateResponse({ date: '2024-02-29' }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, date: '2024-02-29', }, mockClient as any ); expect(result.date).toBe('2024-02-29'); }); it('should handle entries on fiscal year end', async () => { const mockResponse = mockJournalEntryCreateResponse({ date: '2024-12-31' }); mockClient.executeWithRetry.mockImplementation(async (operation, apiCall) => { const client = { journalEntries: { create: vi.fn().mockResolvedValue(mockResponse), }, }; return apiCall(client); }); const result = await journalEntryCreateTool.execute( { ...validInput, date: '2024-12-31', }, mockClient as any ); expect(result.date).toBe('2024-12-31'); }); }); });

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