UNPKG

@accounter/shaam-uniform-format-generator

Version:

Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).

666 lines (634 loc) 24.9 kB
/** * Comprehensive integration test for full round-trip generation and parsing * including A000Sum summary records and full validation */ import { describe, expect, it } from 'vitest'; import { generateUniformFormatReport } from '../../src/api/generate-report.js'; import { parseUniformFormatFiles } from '../../src/api/parse-files.js'; import type { ReportInput } from '../../src/types/index'; describe('Comprehensive SHAAM Format Integration Test', () => { it('should generate complete SHAAM files with ALL available attributes and parse back correctly', () => { // Comprehensive ReportInput fixture with ALL available attributes const input: ReportInput = { business: { businessId: '12345', name: 'Test Company Ltd', taxId: '123456789', address: { street: 'Main Street', houseNumber: '123', city: 'Tel Aviv', zip: '12345', }, reportingPeriod: { startDate: '2024-01-01', endDate: '2024-12-31', }, }, documents: [ { id: 'DOC001', type: '320', // Receipt Tax Invoice date: '2024-03-15', amount: 1000.5, description: 'Consulting services with comprehensive details', }, { id: 'DOC002', type: '330', // Credit Tax Invoice date: '2024-03-20', amount: 250.75, description: 'Product return with full documentation', }, { id: 'DOC003', type: '305', // Tax Invoice date: '2024-04-10', amount: 5000.99, description: 'Large consulting project invoice', }, { id: 'DOC004', type: '500', // Supplier Invoice date: '2024-04-15', amount: 750.25, description: 'Office supplies purchase', }, ], journalEntries: [ { id: 'JE001', date: '2024-03-15', amount: 1000.5, accountId: '1100', description: 'Sales revenue entry', transactionNumber: 1001, transactionLineNumber: 1, batchNumber: 202_403, transactionType: 'SALE', referenceDocument: 'DOC001', referenceDocumentType: '320', referenceDocument2: 'ORDER001', referenceDocumentType2: '100', valueDate: '2024-03-15', counterAccountKey: '4100', debitCreditIndicator: '1', currencyCode: 'ILS', transactionAmount: 1000.5, foreignCurrencyAmount: 270.15, // USD equivalent quantityField: 10.5, matchingField1: 'MATCH001', matchingField2: 'BATCH202403', branchId: 'BR001', entryDate: '2024-03-15', operatorUsername: 'john_doe', reserved: 'RESERVED_DATA', }, { id: 'JE002', date: '2024-03-20', amount: -250.75, accountId: '1200', description: 'Returns and allowances with full tracking', transactionNumber: 1002, transactionLineNumber: 1, batchNumber: 202_403, transactionType: 'RETURN', referenceDocument: 'DOC002', referenceDocumentType: '330', valueDate: '2024-03-20', counterAccountKey: '4200', debitCreditIndicator: '2', currencyCode: 'USD', transactionAmount: -250.75, foreignCurrencyAmount: -67.57, // ILS equivalent quantityField: 2.5, matchingField1: 'MATCH002', matchingField2: 'RETURN_BATCH', branchId: 'BR002', entryDate: '2024-03-20', operatorUsername: 'jane_smith', }, { id: 'JE003', date: '2024-04-10', amount: 5000.99, accountId: '1300', description: 'Large project revenue recognition', transactionNumber: 1003, transactionLineNumber: 1, batchNumber: 202_404, transactionType: 'REVENUE', referenceDocument: 'DOC003', referenceDocumentType: '305', valueDate: '2024-04-10', counterAccountKey: '4300', debitCreditIndicator: '1', currencyCode: 'EUR', transactionAmount: 5000.99, foreignCurrencyAmount: 4543.18, // EUR equivalent quantityField: 100.0, matchingField1: 'PROJECT_X', branchId: 'BR001', entryDate: '2024-04-10', operatorUsername: 'proj_mgr', }, ], accounts: [ { id: '1100', name: 'Cash Account', sortCode: { key: 'Asset', name: 'Current Assets', }, address: { street: 'Bank Street', houseNumber: '456', city: 'Jerusalem', zip: '67890', country: 'Israel', }, countryCode: 'IL', parentAccountKey: '1000', vatId: ' 123 454 321 ', accountOpeningBalance: 5000.0, totalDebits: 15_000.5, totalCredits: 8500.25, accountingClassificationCode: 'A1100', branchId: 'BR001', openingBalanceForeignCurrency: 1351.35, // USD equivalent foreignCurrencyCode: 'USD', }, { id: '1200', name: 'Accounts Receivable', sortCode: { key: 'Asset', name: 'Current Assets', }, address: { street: 'Customer Avenue', houseNumber: '789', city: 'Haifa', zip: '54321', country: 'Israel', }, countryCode: 'IL', parentAccountKey: '1000', vatId: '987654321', accountOpeningBalance: 3000.0, totalDebits: 12_000.75, totalCredits: 4500.5, accountingClassificationCode: 'A1200', branchId: 'BR002', openingBalanceForeignCurrency: 810.81, // USD equivalent foreignCurrencyCode: 'USD', }, { id: '4000', name: 'Sales Revenue', sortCode: { key: 'Revenue', name: 'Operating Revenue', }, address: { street: 'Revenue Street', houseNumber: '321', city: 'Eilat', zip: '88000', country: 'Israel', }, countryCode: 'IL', parentAccountKey: '4000', accountOpeningBalance: 8000.0, totalDebits: 2000.0, totalCredits: 25_000.99, accountingClassificationCode: 'R4000', branchId: 'BR001', openingBalanceForeignCurrency: 2162.16, // USD equivalent foreignCurrencyCode: 'USD', }, { id: '2000', name: 'Accounts Payable', sortCode: { key: 'Liability', name: 'Current Liabilities', }, address: { street: 'Supplier Road', houseNumber: '555', city: 'Beer Sheva', zip: '84000', country: 'Israel', }, countryCode: 'IL', parentAccountKey: '2000', vatId: '555555555', accountOpeningBalance: -1500.0, totalDebits: 5000.0, totalCredits: 8500.0, accountingClassificationCode: 'L2000', branchId: 'BR002', openingBalanceForeignCurrency: -405.41, // USD equivalent foreignCurrencyCode: 'USD', }, ], inventory: [ { id: 'ITEM001', name: 'Professional Consulting Service Package A', quantity: 100, }, { id: 'ITEM002', name: 'Premium Software License Package B', quantity: 50, }, { id: 'ITEM003', name: 'Hardware Component X-Series', quantity: 25, }, { id: 'ITEM004', name: 'Training Materials and Documentation', quantity: 200, }, ], }; // Generate the report const result = generateUniformFormatReport(input); expect(result).toBeDefined(); expect(result.dataText).toBeDefined(); expect(result.iniText).toBeDefined(); expect(result.summary.totalRecords).toBeGreaterThan(0); // Parse the generated files back using parseUniformFormatFiles const parsedData = parseUniformFormatFiles(result.iniText, result.dataText); // Verify business metadata including address expect(parsedData.data.business).toBeDefined(); expect(parsedData.data.business.name).toBe(input.business.name); expect(parsedData.data.business.taxId).toBe(input.business.taxId); expect(parsedData.data.business.address).toBeDefined(); expect(parsedData.data.business.address?.street).toBe(input.business.address?.street); expect(parsedData.data.business.address?.city).toBe(input.business.address?.city); expect(parsedData.data.business.address?.zip).toBe(input.business.address?.zip); // Verify A000Sum summary records from parsed summary (no need for manual parsing) expect(parsedData.summary.perType.A100).toBe(1); expect(parsedData.summary.perType.C100).toBe(input.documents.length); expect(parsedData.summary.perType.D110).toBe(input.documents.length); expect(parsedData.summary.perType.D120).toBe(input.documents.length); expect(parsedData.summary.perType.B100).toBe(input.journalEntries.length); expect(parsedData.summary.perType.B110).toBe(input.accounts.length); expect(parsedData.summary.perType.M100).toBe(input.inventory.length); expect(parsedData.summary.perType.Z900).toBe(1); // Verify documents with comprehensive validation expect(parsedData.data.documents).toHaveLength(input.documents.length); for (let i = 0; i < input.documents.length; i++) { const original = input.documents[i]; const parsed = parsedData.data.documents[i]; expect(parsed.id).toBe(original.id); expect(parsed.type).toBe(original.type); expect(parsed.date).toBe(original.date); expect(parsed.amount).toBe(original.amount); expect(parsed.description).toBe(original.description?.substring(0, 30)); } // Verify journal entries with ALL extended fields expect(parsedData.data.journalEntries).toHaveLength(input.journalEntries.length); for (let i = 0; i < input.journalEntries.length; i++) { const original = input.journalEntries[i]; const parsed = parsedData.data.journalEntries[i]; expect(parsed.date).toBe(original.date); expect(parsed.amount).toBe(original.amount); expect(parsed.accountId).toBe(original.accountId); expect(parsed.description).toBe(original.description); // Validate extended B100 fields where available if (original.transactionNumber !== undefined) { expect(parsed.transactionNumber).toBeDefined(); } if (original.batchNumber !== undefined) { expect(parsed.batchNumber).toBeDefined(); } if (original.referenceDocument !== undefined) { expect(parsed.referenceDocument).toBe(original.referenceDocument); } if (original.referenceDocumentType !== undefined) { expect(parsed.referenceDocumentType).toBe(original.referenceDocumentType); } if (original.currencyCode !== undefined) { expect(parsed.currencyCode).toBe(original.currencyCode); } if (original.debitCreditIndicator !== undefined) { expect(parsed.debitCreditIndicator).toBe(original.debitCreditIndicator); } if (original.branchId !== undefined) { expect(parsed.branchId).toBe(original.branchId); } if (original.operatorUsername !== undefined) { expect(parsed.operatorUsername).toBe(original.operatorUsername.substring(0, 9)); } } // Verify accounts with ALL extended fields expect(parsedData.data.accounts).toHaveLength(input.accounts.length); for (let i = 0; i < input.accounts.length; i++) { const original = input.accounts[i]; const parsed = parsedData.data.accounts[i]; expect(parsed.id).toBe(original.id); expect(parsed.name).toBe(original.name); expect(parsed.sortCode.key).toBe(original.sortCode.key); expect(parsed.sortCode.name).toBe(original.sortCode.name); expect(parsed.accountOpeningBalance).toBe(original.accountOpeningBalance); // Validate extended B110 fields if (original.address !== undefined) { expect(parsed.address).toBeDefined(); expect(parsed.address?.street).toBe(original.address?.street); expect(parsed.address?.houseNumber).toBe(original.address?.houseNumber); expect(parsed.address?.city).toBe(original.address?.city); expect(parsed.address?.zip).toBe(original.address?.zip); expect(parsed.address?.country).toBe(original.address?.country); } if (original.countryCode !== undefined) { expect(parsed.countryCode).toBe(original.countryCode); } if (original.parentAccountKey !== undefined) { expect(parsed.parentAccountKey).toBe(original.parentAccountKey); } if (original.vatId !== undefined) { // SHAAM B110 format only supports numeric VAT IDs (Field 1419: max 9 digits) // Alphanumeric VAT IDs are automatically converted to numeric-only format const expectedVatId = original.vatId.replace(/\D/g, ''); expect(parsed.vatId).toBe(expectedVatId); } if (original.totalDebits !== undefined) { expect(parsed.totalDebits).toBeDefined(); } if (original.totalCredits !== undefined) { expect(parsed.totalCredits).toBeDefined(); } if (original.accountingClassificationCode !== undefined) { // SHAAM B110 format only supports numeric accounting codes (Field 1417: max 4 digits) // Alphanumeric codes are automatically converted to numeric-only format const expectedCode = original.accountingClassificationCode.replace(/\D/g, ''); expect(parsed.accountingClassificationCode).toBe(expectedCode); } if (original.branchId !== undefined) { expect(parsed.branchId).toBe(original.branchId); } if (original.foreignCurrencyCode !== undefined) { expect(parsed.foreignCurrencyCode).toBe(original.foreignCurrencyCode); } } // Verify inventory expect(parsedData.data.inventory).toHaveLength(input.inventory.length); for (let i = 0; i < input.inventory.length; i++) { const original = input.inventory[i]; const parsed = parsedData.data.inventory[i]; expect(parsed.id).toBe(original.id); expect(parsed.name).toBe(original.name); expect(parsed.quantity).toBeGreaterThanOrEqual(0); // M100 records might not preserve exact quantity } // Cross-verify: Summary record counts should match actual parsed data counts expect(parsedData.summary.perType.A100).toBe(1); // Always 1 A100 record expect(parsedData.summary.perType.C100).toBe(parsedData.data.documents.length); expect(parsedData.summary.perType.D110).toBe(parsedData.data.documents.length); // D110 records don't directly appear in parsed data expect(parsedData.summary.perType.D120).toBe(parsedData.data.documents.length); // D120 records don't directly appear in parsed data expect(parsedData.summary.perType.B100).toBe(parsedData.data.journalEntries.length); expect(parsedData.summary.perType.B110).toBe(parsedData.data.accounts.length); expect(parsedData.summary.perType.M100).toBe(parsedData.data.inventory.length); expect(parsedData.summary.perType.Z900).toBe(1); // Always 1 Z900 record // Verify overall record counts with comprehensive data const expectedTotalRecords = 1 + // A000 (INI file) 8 + // A000Sum records (one for each record type) 1 + // A100 input.documents.length + // C100 records input.documents.length + // D110 records input.documents.length + // D120 records input.journalEntries.length + // B100 records input.accounts.length + // B110 records input.inventory.length + // M100 records 1; // Z900 expect(result.summary.totalRecords).toBe(expectedTotalRecords); }); it('should validate comprehensive data integrity with complex edge cases', () => { // Test with various document types, currency codes, and edge cases const edgeCaseInput: ReportInput = { business: { businessId: '99999', name: 'Edge Case Testing Co Ltd', taxId: '999888777', address: { street: 'Test Street with special chars: åäö', houseNumber: '999A', city: 'Tel Aviv-Yafo', zip: '6789012', }, reportingPeriod: { startDate: '2024-01-01', endDate: '2024-12-31', }, }, documents: [ // Test various document types { id: 'ORDER_123', type: '100', // Order date: '2024-01-01', amount: 0.01, // Minimal amount description: 'Edge case order with minimal amount', }, { id: 'DELIVERY_456', type: '200', // Delivery Note date: '2024-06-15', amount: 999_999.99, // Maximum practical amount description: 'Large delivery note - testing high amounts', }, { id: 'CREDIT_CARD_789', type: '710', // Credit Card Receipt date: '2024-12-31', amount: 123.456, // Fractional amount with 3 decimal places description: 'Credit card transaction with precise amount', }, ], journalEntries: [ // Test with foreign currencies and complex fields { id: 'MULTI_CURRENCY_001', date: '2024-06-15', amount: 1234.56, accountId: 'USD_ACCOUNT', description: 'Multi-currency transaction test', transactionNumber: 999_999, transactionLineNumber: 999, batchNumber: 999_999, transactionType: 'MULTI_CURR', referenceDocument: 'REF_999', referenceDocumentType: '305', referenceDocument2: 'REF2_999', referenceDocumentType2: '320', valueDate: '2024-06-16', counterAccountKey: 'COUNTER_999', debitCreditIndicator: '2', currencyCode: 'EUR', transactionAmount: -1234.56, foreignCurrencyAmount: 1123.45, quantityField: 999.999, matchingField1: 'MATCH_FIELD_1_TEST', matchingField2: 'MATCH_FIELD_2_TEST', branchId: 'BRANCH_EDGE', entryDate: '2024-06-15', operatorUsername: 'edge_user', reserved: 'RESERVED_EDGE_DATA', }, ], accounts: [ // Test with various account types and all possible fields { id: 'USD_ACCOUNT', name: 'Foreign Currency Account - USD', sortCode: { key: 'Asset', name: 'Foreign Currency Assets', }, address: { street: 'International Ave', houseNumber: '1', city: 'Global City', zip: '00000', country: 'International Zone', }, countryCode: 'US', parentAccountKey: 'FOREIGN_ASSETS', vatId: ' 999 999 999 ', accountOpeningBalance: -999_999.99, // Negative balance totalDebits: 500_000.0, totalCredits: 1_500_000.0, accountingClassificationCode: 'FA_USD', branchId: 'INTERNATIONAL', openingBalanceForeignCurrency: 999_999.99, foreignCurrencyCode: 'EUR', }, // Test equity account { id: 'EQUITY_001', name: 'Owner Equity Account', sortCode: { key: 'Equity', name: 'Owner Equity', }, countryCode: 'IL', accountOpeningBalance: 1_000_000.0, // Large equity balance totalDebits: 0.0, totalCredits: 1_000_000.0, accountingClassificationCode: 'EQ_001', }, // Test expense account { id: 'EXPENSE_001', name: 'Operating Expenses', sortCode: { key: 'Expense', name: 'Operating Expenses', }, accountOpeningBalance: 0.0, totalDebits: 250_000.0, totalCredits: 10_000.0, // Some credits for returns accountingClassificationCode: 'EX_001', branchId: 'MAIN', foreignCurrencyCode: 'USD', openingBalanceForeignCurrency: 0.0, }, ], inventory: [ // Test with edge case inventory items { id: 'EXPENSIVE_ITEM_001', name: 'High-Value Asset Item', quantity: 1, }, { id: 'BULK_ITEM_001', name: 'Bulk Inventory Item', quantity: 10_000, }, { id: 'FRACTIONAL_001', name: 'Fractional Quantity Item', quantity: 0.5, }, ], }; const result = generateUniformFormatReport(edgeCaseInput); const parsedData = parseUniformFormatFiles(result.iniText, result.dataText); // Comprehensive validation expect(parsedData.data.business.name).toBe(edgeCaseInput.business.name); expect(parsedData.data.business.address?.street).toContain('special chars'); // Validate document type variety const documentTypes = parsedData.data.documents.map(d => d.type); expect(documentTypes).toContain('100'); // Order expect(documentTypes).toContain('200'); // Delivery Note expect(documentTypes).toContain('710'); // Credit Card Receipt // Validate amounts are preserved correctly expect(parsedData.data.documents[0].amount).toBe(0.01); // Minimal expect(parsedData.data.documents[1].amount).toBe(999_999.99); // Maximum // Validate complex journal entry const complexEntry = parsedData.data.journalEntries[0]; expect(complexEntry.currencyCode).toBe('EUR'); expect(complexEntry.debitCreditIndicator).toBe('2'); expect(complexEntry.referenceDocument).toBe('REF_999'); expect(complexEntry.operatorUsername).toBe('edge_user'); // Validate account variety and extended fields const accounts = parsedData.data.accounts; const usdAccount = accounts.find(a => a.id === 'USD_ACCOUNT'); expect(usdAccount).toBeDefined(); expect(usdAccount!.countryCode).toBe('US'); expect(usdAccount!.foreignCurrencyCode).toBe('EUR'); expect(usdAccount!.accountOpeningBalance).toBe(-999_999.99); // Negative balance const equityAccount = accounts.find(a => a.sortCode.key === 'Equity'); expect(equityAccount).toBeDefined(); expect(equityAccount!.accountOpeningBalance).toBe(1_000_000.0); // Validate inventory edge cases const expensiveItem = parsedData.data.inventory.find(i => i.id === 'EXPENSIVE_ITEM_001'); expect(expensiveItem).toBeDefined(); const bulkItem = parsedData.data.inventory.find(i => i.id === 'BULK_ITEM_001'); expect(bulkItem).toBeDefined(); expect(bulkItem!.quantity).toBeGreaterThanOrEqual(0); // M100 may not preserve exact quantity // Verify record counts for edge case data expect(result.summary.totalRecords).toBeGreaterThan(10); expect(parsedData.summary.perType.C100).toBe(edgeCaseInput.documents.length); expect(parsedData.summary.perType.B100).toBe(edgeCaseInput.journalEntries.length); expect(parsedData.summary.perType.B110).toBe(edgeCaseInput.accounts.length); expect(parsedData.summary.perType.M100).toBe(edgeCaseInput.inventory.length); }); it('should validate File objects are created correctly', () => { const minimalInput: ReportInput = { business: { businessId: '54321', name: 'Minimal Company', taxId: '987654321', reportingPeriod: { startDate: '2024-01-01', endDate: '2024-12-31', }, }, documents: [], journalEntries: [], accounts: [], inventory: [], }; const result = generateUniformFormatReport(minimalInput, { fileNameBase: 'test-report' }); // Verify File objects expect(result.iniFile).toBeInstanceOf(File); expect(result.dataFile).toBeInstanceOf(File); expect(result.iniFile.name).toBe('test-report.INI.TXT'); expect(result.dataFile.name).toBe('test-report.BKMVDATA.TXT'); expect(result.iniFile.type).toBe('text/plain;charset=windows-1255'); expect(result.dataFile.type).toBe('text/plain;charset=windows-1255'); // Verify file content matches text content expect(result.iniFile.size).toBe(result.iniText.length); expect(result.dataFile.size).toBe(result.dataText.length); }); });