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