UNPKG

@accounter/server

Version:
486 lines • 23.4 kB
import { describe, expect, it } from 'vitest'; import { aggregateDocuments, } from '../providers/document-aggregator.js'; describe('Document Aggregator', () => { const USER_ID = 'user-123'; // Helper to create test documents const createDocument = (overrides = {}) => ({ id: 'doc-' + Math.random().toString(36).substr(2, 9), charge_id: 'charge-123', creditor_id: USER_ID, // Default: user is creditor debtor_id: 'business-1', // Default: business is debtor currency_code: 'USD', date: new Date('2024-01-15'), total_amount: 100, type: 'INVOICE', serial_number: null, image_url: null, file_url: null, ...overrides, }); describe('Single Document', () => { it('should return single document with normalized amount', () => { const document = createDocument({ total_amount: 150.5, currency_code: 'USD', creditor_id: USER_ID, debtor_id: 'business-1', type: 'INVOICE', date: new Date('2024-01-15'), serial_number: 'INV-001', }); const result = aggregateDocuments([document], USER_ID); expect(result.amount).toBe(150.5); // Positive: business is debtor expect(result.currency).toBe('USD'); expect(result.businessId).toBe('business-1'); expect(result.date).toEqual(new Date('2024-01-15')); expect(result.type).toBe('INVOICE'); expect(result.description).toBe('INV-001'); }); it('should normalize amount when business is creditor', () => { const document = createDocument({ total_amount: 200, creditor_id: 'business-1', // Business is creditor debtor_id: USER_ID, // User is debtor type: 'INVOICE', }); const result = aggregateDocuments([document], USER_ID); expect(result.amount).toBe(-200); // Negative: business is creditor expect(result.businessId).toBe('business-1'); }); it('should handle credit invoice normalization', () => { const document = createDocument({ total_amount: 100, creditor_id: 'business-1', // Business is creditor debtor_id: USER_ID, // User is debtor type: 'CREDIT_INVOICE', }); const result = aggregateDocuments([document], USER_ID); expect(result.amount).toBe(100); // Positive: double negation (-1 * -1) expect(result.type).toBe('CREDIT_INVOICE'); }); it('should use file_url as description when serial_number is null', () => { const document = createDocument({ serial_number: null, file_url: 'https://example.com/files/invoice_2024.pdf', }); const result = aggregateDocuments([document], USER_ID); expect(result.description).toBe('invoice_2024.pdf'); }); it('should use image_url as description when both serial_number and file_url are null', () => { const document = createDocument({ serial_number: null, file_url: null, image_url: 'https://example.com/images/scan_001.jpg', }); const result = aggregateDocuments([document], USER_ID); expect(result.description).toBe('scan_001.jpg'); }); it('should use document ID as fallback description', () => { const document = createDocument({ id: 'doc-abc123def456', serial_number: null, file_url: null, image_url: null, }); const result = aggregateDocuments([document], USER_ID); expect(result.description).toBe('Doc-doc-abc1'); }); }); describe('Multiple Invoices', () => { it('should sum normalized amounts correctly', () => { const documents = [ createDocument({ total_amount: 100, type: 'INVOICE' }), // +100 createDocument({ total_amount: 200, type: 'INVOICE' }), // +200 createDocument({ total_amount: 50, type: 'INVOICE' }), // +50 ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(350); expect(result.type).toBe('INVOICE'); }); it('should handle mixed regular and credit invoices', () => { const documents = [ createDocument({ total_amount: 500, type: 'INVOICE', creditor_id: USER_ID, debtor_id: 'business-1', }), // +500 createDocument({ total_amount: 100, type: 'CREDIT_INVOICE', creditor_id: USER_ID, debtor_id: 'business-1', }), // -100 ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(400); // 500 - 100 }); it('should handle invoices where business is creditor', () => { const documents = [ createDocument({ total_amount: 200, creditor_id: 'business-1', debtor_id: USER_ID, }), // -200 createDocument({ total_amount: 100, creditor_id: 'business-1', debtor_id: USER_ID, }), // -100 ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(-300); }); }); describe('Multiple Receipts', () => { it('should sum receipt amounts correctly', () => { const documents = [ createDocument({ total_amount: 50, type: 'RECEIPT' }), createDocument({ total_amount: 75, type: 'RECEIPT' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(125); expect(result.type).toBe('RECEIPT'); }); it('should handle invoice-receipts', () => { const documents = [ createDocument({ total_amount: 100, type: 'INVOICE_RECEIPT' }), createDocument({ total_amount: 150, type: 'INVOICE_RECEIPT' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(250); expect(result.type).toBe('INVOICE_RECEIPT'); }); }); describe('Type Priority Filtering', () => { it('should use only receipts when both invoices and receipts exist', () => { const documents = [ createDocument({ total_amount: 100, type: 'RECEIPT', serial_number: 'REC-001' }), createDocument({ total_amount: 200, type: 'INVOICE', serial_number: 'INV-001' }), createDocument({ total_amount: 50, type: 'RECEIPT', serial_number: 'REC-002' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(150); expect(result.type).toBe('RECEIPT'); expect(result.description).toContain('REC-001'); expect(result.description).toContain('REC-002'); expect(result.description).not.toContain('INV-001'); // Invoices excluded }); it('should use only receipts when both credit invoices and receipts exist', () => { const documents = [ createDocument({ total_amount: 100, type: 'RECEIPT' }), createDocument({ total_amount: 200, type: 'CREDIT_INVOICE' }), createDocument({ total_amount: 50, type: 'RECEIPT' }), ]; const result = aggregateDocuments(documents, USER_ID); // Credit invoices with user as creditor (business debtor): negate expect(result.amount).toBe(150); // 100 + 50 expect(result.type).toBe('RECEIPT'); }); it('should use only receipts/invoice-receipts when mixed with invoices and credit-invoices', () => { const documents = [ createDocument({ total_amount: 100, type: 'INVOICE' }), // Should be excluded createDocument({ total_amount: 200, type: 'INVOICE_RECEIPT' }), createDocument({ total_amount: 10, type: 'RECEIPT' }), createDocument({ total_amount: 50, type: 'CREDIT_INVOICE' }), // Should be excluded ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(210); // 200 + 10 (receipt and invoice-receipt) }); it('should use receipts when no invoices exist', () => { const documents = [ createDocument({ total_amount: 100, type: 'RECEIPT' }), createDocument({ total_amount: 50, type: 'INVOICE_RECEIPT' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(150); expect(result.type).toBe('RECEIPT'); }); }); describe('Currency Validation', () => { it('should throw error with multiple currencies', () => { const documents = [ createDocument({ currency_code: 'USD' }), createDocument({ currency_code: 'EUR' }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow('Cannot aggregate documents: multiple currencies found (USD, EUR)'); }); it('should throw error with three different currencies', () => { const documents = [ createDocument({ currency_code: 'USD' }), createDocument({ currency_code: 'EUR' }), createDocument({ currency_code: 'GBP' }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow(/multiple currencies found/); }); it('should not throw when all have same currency', () => { const documents = [ createDocument({ currency_code: 'ILS' }), createDocument({ currency_code: 'ILS' }), createDocument({ currency_code: 'ILS' }), ]; expect(() => aggregateDocuments(documents, USER_ID)).not.toThrow(); }); it('should throw error when all currencies are null', () => { const documents = [ createDocument({ currency_code: null }), createDocument({ currency_code: null }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow('Cannot aggregate documents: all documents have null currency_code'); }); it('should ignore currency of filtered-out invoices', () => { const documents = [ createDocument({ currency_code: 'USD', type: 'INVOICE' }), // Different currency but will be filtered createDocument({ currency_code: 'EUR', type: 'RECEIPT' }), createDocument({ currency_code: 'EUR', type: 'RECEIPT' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.currency).toBe('EUR'); }); }); describe('Business ID Validation', () => { it('should throw error with multiple different business IDs', () => { const documents = [ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }), createDocument({ debtor_id: 'business-2', creditor_id: USER_ID }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow('Cannot aggregate documents: multiple business IDs found (business-1, business-2)'); }); it('should return null when all business IDs are null', () => { const documents = [ createDocument({ debtor_id: null, creditor_id: USER_ID }), createDocument({ debtor_id: null, creditor_id: USER_ID }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.businessId).toBeNull(); }); it('should return single non-null business ID', () => { const documents = [ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }), createDocument({ debtor_id: null, creditor_id: USER_ID }), createDocument({ debtor_id: null, creditor_id: USER_ID }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.businessId).toBe('business-1'); }); it('should not throw when all have same business ID', () => { const documents = [ createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }), createDocument({ debtor_id: 'business-1', creditor_id: USER_ID }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.businessId).toBe('business-1'); }); }); describe('Date Selection', () => { it('should select latest date', () => { const documents = [ createDocument({ date: new Date('2024-01-10') }), createDocument({ date: new Date('2024-03-15') }), // Latest createDocument({ date: new Date('2024-02-20') }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.date).toEqual(new Date('2024-03-15')); }); it('should handle dates spanning years', () => { const documents = [ createDocument({ date: new Date('2023-12-31') }), createDocument({ date: new Date('2024-02-01') }), // Latest createDocument({ date: new Date('2024-01-15') }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.date).toEqual(new Date('2024-02-01')); }); it('should handle same date for all documents', () => { const documents = [ createDocument({ date: new Date('2024-01-15') }), createDocument({ date: new Date('2024-01-15') }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.date).toEqual(new Date('2024-01-15')); }); it('should throw error when all dates are null', () => { const documents = [ createDocument({ date: null }), createDocument({ date: null }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow('Cannot aggregate documents: all documents have null date'); }); it('should handle mix of null and valid dates', () => { const documents = [ createDocument({ date: null }), createDocument({ date: new Date('2024-01-15') }), createDocument({ date: new Date('2024-02-20') }), // Latest ]; const result = aggregateDocuments(documents, USER_ID); expect(result.date).toEqual(new Date('2024-02-20')); }); }); describe('Description Concatenation', () => { it('should concatenate serial numbers with line breaks', () => { const documents = [ createDocument({ serial_number: 'INV-001' }), createDocument({ serial_number: 'INV-002' }), createDocument({ serial_number: 'INV-003' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.description).toBe('INV-001\nINV-002\nINV-003'); }); it('should prefer serial_number over file URLs', () => { const documents = [ createDocument({ serial_number: 'INV-001', file_url: 'file1.pdf' }), createDocument({ serial_number: null, file_url: 'file2.pdf' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.description).toBe('INV-001\nfile2.pdf'); }); it('should use image URLs when no serial or file URL', () => { const documents = [ createDocument({ serial_number: null, file_url: null, image_url: 'img1.jpg' }), createDocument({ serial_number: null, file_url: null, image_url: 'img2.jpg' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.description).toBe('img1.jpg\nimg2.jpg'); }); it('should fallback to document ID when no other identifiers', () => { const documents = [ createDocument({ id: 'doc-abc123', serial_number: null, file_url: null, image_url: null, }), createDocument({ id: 'doc-def456', serial_number: 'INV-001' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.description).toContain('Doc-doc-abc1'); expect(result.description).toContain('INV-001'); }); it('should handle empty descriptions gracefully', () => { const documents = [ createDocument({ serial_number: ' ', file_url: null, image_url: null, id: 'doc-123' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.description).toBe('Doc-doc-123'); }); }); describe('Business Extraction Error Propagation', () => { it('should propagate error when both creditor and debtor are user', () => { const documents = [ createDocument({ creditor_id: USER_ID, debtor_id: USER_ID, // Invalid: both are user }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow(/both creditor_id and debtor_id equal to user ID/); }); it('should propagate error when neither creditor nor debtor is user', () => { const documents = [ createDocument({ creditor_id: 'business-1', debtor_id: 'business-2', // Invalid: neither is user }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow(/neither creditor_id nor debtor_id equal to user ID/); }); it('should propagate error when both IDs are null', () => { const documents = [ createDocument({ creditor_id: null, debtor_id: null, // Invalid: both null }), ]; expect(() => aggregateDocuments(documents, USER_ID)).toThrow(/both creditor_id and debtor_id as null/); }); }); describe('Input Validation', () => { it('should throw error for empty array', () => { expect(() => aggregateDocuments([], USER_ID)).toThrow('Cannot aggregate documents: array is empty'); }); it('should throw error for null input', () => { expect(() => aggregateDocuments(null, USER_ID)).toThrow('Cannot aggregate documents: array is empty'); }); it('should throw error for undefined input', () => { expect(() => aggregateDocuments(undefined, USER_ID)).toThrow('Cannot aggregate documents: array is empty'); }); }); describe('Complex Scenarios', () => { it('should handle real-world mixed document scenario', () => { const documents = [ createDocument({ total_amount: 1000, type: 'INVOICE', // Will be filtered out creditor_id: USER_ID, debtor_id: 'business-abc', date: new Date('2024-01-15'), serial_number: 'INV-2024-001', currency_code: 'USD', }), createDocument({ total_amount: 500, type: 'RECEIPT', creditor_id: USER_ID, debtor_id: 'business-abc', date: new Date('2024-01-20'), serial_number: 'REC-2024-001', currency_code: 'EUR', // Different currency but will be filtered }), createDocument({ total_amount: 200, type: 'CREDIT_INVOICE', // Will be filtered out creditor_id: USER_ID, debtor_id: 'business-abc', date: new Date('2024-01-25'), serial_number: 'CRD-2024-001', currency_code: 'USD', }), createDocument({ total_amount: 100, type: 'INVOICE_RECEIPT', creditor_id: USER_ID, debtor_id: 'business-abc', date: new Date('2024-01-23'), // Latest serial_number: 'REC-2024-002', currency_code: 'EUR', // Different currency but will be filtered }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(600); // 500 + 100 expect(result.currency).toBe('EUR'); expect(result.businessId).toBe('business-abc'); expect(result.date).toEqual(new Date('2024-01-23')); // Latest among receipts expect(result.type).toBe('RECEIPT'); // First receipt type expect(result.description).not.toContain('INV-2024-001'); // Invoice excluded expect(result.description).not.toContain('CRD-2024-001'); // Credit-Invoice excluded expect(result.description).toContain('REC-2024-001'); expect(result.description).toContain('REC-2024-002'); }); it('should handle documents with null business IDs', () => { const documents = [ createDocument({ total_amount: 100, creditor_id: USER_ID, debtor_id: null, // No business }), createDocument({ total_amount: 200, creditor_id: USER_ID, debtor_id: null, // No business }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(300); expect(result.businessId).toBeNull(); }); it('should handle cryptocurrency documents', () => { const documents = [ createDocument({ total_amount: 0.5, currency_code: 'ETH' }), createDocument({ total_amount: 0.3, currency_code: 'ETH' }), ]; const result = aggregateDocuments(documents, USER_ID); expect(result.amount).toBe(0.8); expect(result.currency).toBe('ETH'); }); }); }); //# sourceMappingURL=document-aggregator.test.js.map