@accounter/server
Version:
Accounter GraphQL server
247 lines • 9.76 kB
JavaScript
import { describe, expect, it } from 'vitest';
import { isBalanced, parseAmount, validateDates, validateEntityBalance, validateForeignCurrency, validateLedgerRecords, validateNoOrphanedAmounts, validatePositiveAmounts, validateRecordInternalBalance, } from './ledger-validators.js';
const mockContext = {
useCaseId: 'test-case',
defaultCurrency: 'ILS',
tolerance: 0.005,
};
function createMockRecord(overrides = {}) {
return {
id: 'rec-1',
charge_id: 'charge-1',
owner_id: 'owner-1',
debit_entity1: null,
debit_local_amount1: null,
debit_foreign_amount1: null,
debit_entity2: null,
debit_local_amount2: null,
debit_foreign_amount2: null,
credit_entity1: null,
credit_local_amount1: null,
credit_foreign_amount1: null,
credit_entity2: null,
credit_local_amount2: null,
credit_foreign_amount2: null,
currency: 'ILS',
invoice_date: new Date('2024-01-01'),
value_date: new Date('2024-01-01'),
description: null,
reference1: null,
locked: false,
...overrides,
};
}
describe('parseAmount', () => {
it('should parse valid number strings', () => {
expect(parseAmount('100.50')).toBe(100.5);
expect(parseAmount('0')).toBe(0);
});
it('should handle null and undefined', () => {
expect(parseAmount(null)).toBe(0);
expect(parseAmount(undefined)).toBe(0);
});
it('should handle invalid strings', () => {
expect(parseAmount('invalid')).toBe(0);
expect(parseAmount('')).toBe(0);
});
});
describe('isBalanced', () => {
it('should return true for equal values', () => {
expect(isBalanced(100, 100)).toBe(true);
});
it('should return true for values within tolerance', () => {
expect(isBalanced(100, 100.005, 0.005)).toBe(true);
});
it('should return false for values outside tolerance', () => {
expect(isBalanced(100, 102, 0.005)).toBe(false);
});
});
describe('validateRecordInternalBalance', () => {
it('should pass for balanced record', () => {
const record = createMockRecord({
debit_entity1: 'entity-1',
debit_local_amount1: '100.00',
credit_entity1: 'entity-2',
credit_local_amount1: '100.00',
});
const errors = validateRecordInternalBalance([record], mockContext);
expect(errors).toHaveLength(0);
});
it('should fail for unbalanced record', () => {
const record = createMockRecord({
debit_local_amount1: '100.00',
credit_local_amount1: '99.00',
});
const errors = validateRecordInternalBalance([record], mockContext);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('internal imbalance');
expect(errors[0]).toContain('debit=100.00');
expect(errors[0]).toContain('credit=99.00');
});
it('should detect empty records', () => {
const record = createMockRecord({
// All amounts null/zero
});
const errors = validateRecordInternalBalance([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('empty record');
});
});
describe('validateEntityBalance', () => {
it('should pass when all entities balance to zero', () => {
const records = [
createMockRecord({
debit_entity1: 'entity-1',
debit_local_amount1: '100.00',
credit_entity1: 'entity-2',
credit_local_amount1: '100.00',
}),
createMockRecord({
id: 'rec-2',
debit_entity1: 'entity-2',
debit_local_amount1: '100.00',
credit_entity1: 'entity-1',
credit_local_amount1: '100.00',
}),
];
const errors = validateEntityBalance(records, mockContext);
expect(errors).toHaveLength(0);
});
it('should fail when entity has unbalanced position', () => {
const record = createMockRecord({
debit_entity1: 'entity-1',
debit_local_amount1: '100.00',
credit_entity1: 'entity-2',
credit_local_amount1: '100.00',
});
const errors = validateEntityBalance([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('unbalanced');
expect(errors[0]).toContain('entity-1');
});
});
describe('validateNoOrphanedAmounts', () => {
it('should fail when amount exists without entity', () => {
const record = createMockRecord({
debit_entity1: null,
debit_local_amount1: '100.00',
});
const errors = validateNoOrphanedAmounts([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('orphaned amount');
});
it('should pass when amounts have entities', () => {
const record = createMockRecord({
debit_entity1: 'entity-1',
debit_local_amount1: '100.00',
credit_entity1: 'entity-2',
credit_local_amount1: '100.00',
});
const errors = validateNoOrphanedAmounts([record], mockContext);
expect(errors).toHaveLength(0);
});
});
describe('validatePositiveAmounts', () => {
it('should fail for negative amounts', () => {
const record = createMockRecord({
debit_local_amount1: '-100.00',
});
const errors = validatePositiveAmounts([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('negative amount');
});
it('should pass for positive amounts', () => {
const record = createMockRecord({
debit_local_amount1: '100.00',
credit_local_amount1: '100.00',
});
const errors = validatePositiveAmounts([record], mockContext);
expect(errors).toHaveLength(0);
});
});
describe('validateForeignCurrency', () => {
it('should require foreign amounts for non-ILS currency', () => {
const record = createMockRecord({
currency: 'USD',
debit_local_amount1: '350.00',
debit_foreign_amount1: null,
});
const errors = validateForeignCurrency([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('no foreign amounts');
});
it('should reject foreign amounts for ILS currency', () => {
const record = createMockRecord({
currency: 'ILS',
debit_local_amount1: '100.00',
debit_foreign_amount1: '100.00',
});
const errors = validateForeignCurrency([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('has foreign amounts');
});
it('should detect suspicious exchange rates', () => {
const record = createMockRecord({
currency: 'USD',
debit_local_amount1: '1000.00',
debit_foreign_amount1: '10.00', // Rate = 100
});
const errors = validateForeignCurrency([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('suspicious exchange rate');
});
});
describe('validateDates', () => {
it('should fail for missing dates', () => {
const record = createMockRecord({
invoice_date: null,
});
const errors = validateDates([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('missing invoice_date');
});
it('should fail for dates out of range', () => {
const record = createMockRecord({
invoice_date: new Date('1999-01-01'),
});
const errors = validateDates([record], mockContext);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('out of range');
});
});
describe('validateLedgerRecords', () => {
it('should pass when all validations succeed', () => {
// Create two records that balance entities:
// Record 1: entity-1 debits 100, entity-2 credits 100
// Record 2: entity-1 credits 100, entity-2 debits 100
// This ensures each entity has equal debits and credits
const record1 = createMockRecord({
debit_entity1: 'entity-1',
debit_local_amount1: '100.00',
credit_entity1: 'entity-2',
credit_local_amount1: '100.00',
});
const record2 = createMockRecord({
debit_entity1: 'entity-2',
debit_local_amount1: '100.00',
credit_entity1: 'entity-1',
credit_local_amount1: '100.00',
});
const errors = validateLedgerRecords([record1, record2], 2, mockContext);
expect(errors).toHaveLength(0);
});
it('should collect errors from multiple validators', () => {
const record = createMockRecord({
debit_local_amount1: '100.00',
credit_local_amount1: '99.00', // Imbalance
debit_entity1: null, // Orphaned amount
invoice_date: null, // Missing date
});
const errors = validateLedgerRecords([record], 1, mockContext);
expect(errors.length).toBeGreaterThan(1); // Multiple errors
expect(errors.some(e => e.includes('imbalance'))).toBe(true);
expect(errors.some(e => e.includes('orphaned'))).toBe(true);
expect(errors.some(e => e.includes('missing invoice_date'))).toBe(true);
});
});
//# sourceMappingURL=ledger-validators.test.js.map