@accounter/shaam-uniform-format-generator
Version:
Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).
685 lines (652 loc) • 24 kB
text/typescript
/**
* Integration test for round-trip generation and parsing
* This test ensures that data can be generated to SHAAM format and parsed back correctly
*/
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.js';
describe('SHAAM Format Round-trip Integration Test', () => {
it('should generate and parse back a complete report with ALL available attributes', () => {
// Comprehensive ReportInput fixture with ALL available attributes
const input: ReportInput = {
business: {
businessId: '12345',
name: 'Test Company Ltd',
taxId: '123456789',
address: {
street: 'Innovation Boulevard',
houseNumber: '42',
city: 'Tel Aviv',
zip: '69710',
},
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: 'Comprehensive consulting services package',
},
{
id: 'DOC002',
type: '330', // Credit Tax Invoice
date: '2024-03-20',
amount: 250.75,
description: 'Product return with detailed documentation',
},
{
id: 'DOC003',
type: '305', // Tax Invoice
date: '2024-04-10',
amount: 3500.0,
description: 'Software licensing and support',
},
{
id: 'DOC004',
type: '500', // Supplier Invoice
date: '2024-05-15',
amount: 899.99,
description: 'Office equipment procurement',
},
{
id: 'DOC005',
type: '100', // Order
date: '2024-06-01',
amount: 1250.0,
description: 'Purchase order for materials',
},
],
journalEntries: [
{
id: 'JE001',
date: '2024-03-15',
amount: 1000.5,
accountId: '1100',
description: 'Sales revenue with comprehensive tracking',
transactionNumber: 10_001,
transactionLineNumber: 1,
batchNumber: 202_403,
transactionType: 'SALES',
referenceDocument: 'DOC001',
referenceDocumentType: '320',
referenceDocument2: 'ORDER_001',
referenceDocumentType2: '100',
valueDate: '2024-03-15',
counterAccountKey: '4100',
debitCreditIndicator: '1',
currencyCode: 'ILS',
transactionAmount: 1000.5,
foreignCurrencyAmount: 270.41, // USD equivalent
quantityField: 10.0,
matchingField1: 'MATCH_001',
matchingField2: 'BATCH_A',
branchId: 'BR_MAIN',
entryDate: '2024-03-15',
operatorUsername: 'acct_mgr',
reserved: 'RESERVED_SALES',
},
{
id: 'JE002',
date: '2024-03-20',
amount: -250.75,
accountId: '1200',
description: 'Returns and allowances with full audit trail',
transactionNumber: 10_002,
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: -927.78, // ILS equivalent
quantityField: 2.5,
matchingField1: 'RET_001',
matchingField2: 'BATCH_B',
branchId: 'BR_RET',
entryDate: '2024-03-20',
operatorUsername: 'ret_clerk',
reserved: 'RESERVED_RETURN',
},
{
id: 'JE003',
date: '2024-04-10',
amount: 3500.0,
accountId: '1300',
description: 'Software revenue recognition',
transactionNumber: 10_003,
transactionLineNumber: 1,
batchNumber: 202_404,
transactionType: 'SOFTWARE_REV',
referenceDocument: 'DOC003',
referenceDocumentType: '305',
valueDate: '2024-04-10',
counterAccountKey: '4300',
debitCreditIndicator: '1',
currencyCode: 'EUR',
transactionAmount: 3500.0,
foreignCurrencyAmount: 3182.73, // EUR equivalent
quantityField: 1.0,
matchingField1: 'SOFT_001',
branchId: 'BR_SOFT',
entryDate: '2024-04-10',
operatorUsername: 'sw_acct',
},
],
accounts: [
{
id: '1100',
name: 'Primary Cash Account',
sortCode: {
key: 'Asset',
name: 'Current Assets',
},
address: {
street: 'Bank Street',
houseNumber: '123',
city: 'Jerusalem',
zip: '91999',
country: 'Israel',
},
countryCode: 'IL',
parentAccountKey: '1000',
vatId: '123456789',
accountOpeningBalance: 5000.0,
totalDebits: 25_000.0,
totalCredits: 18_500.0,
accountingClassificationCode: '1100',
branchId: 'BR_MAIN',
openingBalanceForeignCurrency: 1351.35,
foreignCurrencyCode: 'USD',
},
{
id: '1200',
name: 'Accounts Receivable - Trade',
sortCode: {
key: 'Asset',
name: 'Current Assets',
},
address: {
street: 'Customer Avenue',
houseNumber: '456',
city: 'Haifa',
zip: '31999',
country: 'Israel',
},
countryCode: 'IL',
parentAccountKey: '1000',
vatId: '987654321',
accountOpeningBalance: 3000.0,
totalDebits: 15_000.0,
totalCredits: 8500.0,
accountingClassificationCode: '1200',
branchId: 'BR_SALE',
openingBalanceForeignCurrency: 810.81,
foreignCurrencyCode: 'USD',
},
{
id: '4000',
name: 'Sales Revenue - Services',
sortCode: {
key: 'Revenue',
name: 'Operating Revenue',
},
address: {
street: 'Revenue Street',
houseNumber: '789',
city: 'Eilat',
zip: '88999',
country: 'Israel',
},
countryCode: 'IL',
parentAccountKey: '4000',
accountOpeningBalance: 8000.0,
totalDebits: 1000.0,
totalCredits: 35_000.0,
accountingClassificationCode: '4000',
branchId: 'BR_SVC',
openingBalanceForeignCurrency: 2162.16,
foreignCurrencyCode: 'USD',
},
{
id: '2100',
name: 'Accounts Payable - Trade',
sortCode: {
key: 'Liability',
name: 'Current Liabilities',
},
address: {
street: 'Supplier Road',
houseNumber: '321',
city: 'Beer Sheva',
zip: '84999',
country: 'Israel',
},
countryCode: 'IL',
parentAccountKey: '2000',
vatId: '555 666 777',
accountOpeningBalance: -2500.0,
totalDebits: 8000.0,
totalCredits: 12_500.0,
accountingClassificationCode: '2100',
branchId: 'BR_PUR',
openingBalanceForeignCurrency: -675.68,
foreignCurrencyCode: 'USD',
},
{
id: '3000',
name: 'Owner Equity',
sortCode: {
key: 'Equity',
name: 'Owner Equity',
},
countryCode: 'IL',
accountOpeningBalance: 50_000.0,
totalDebits: 5000.0,
totalCredits: 75_000.0,
accountingClassificationCode: '3000',
branchId: 'BR_MAIN',
openingBalanceForeignCurrency: 13_513.51,
foreignCurrencyCode: 'USD',
},
],
inventory: [
{
id: 'ITEM001',
name: 'Professional Service Package Alpha',
quantity: 100,
},
{
id: 'ITEM002',
name: 'Software License Premium Bundle',
quantity: 50,
},
{
id: 'ITEM003',
name: 'Hardware Component Enterprise Series',
quantity: 25,
},
{
id: 'ITEM004',
name: 'Training and Documentation Package',
quantity: 200,
},
{
id: 'ITEM005',
name: 'Support and Maintenance Contract',
quantity: 10,
},
],
};
// 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 the new parsing function
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.businessId).toBeDefined();
expect(parsedData.data.business.reportingPeriod.startDate).toBe(
input.business.reportingPeriod.startDate,
);
expect(parsedData.data.business.reportingPeriod.endDate).toBe(
input.business.reportingPeriod.endDate,
);
// Validate business address
expect(parsedData.data.business.address).toBeDefined();
expect(parsedData.data.business.address?.street).toBe(input.business.address?.street);
expect(parsedData.data.business.address?.houseNumber).toBe(input.business.address?.houseNumber);
expect(parsedData.data.business.address?.city).toBe(input.business.address?.city);
expect(parsedData.data.business.address?.zip).toBe(input.business.address?.zip);
// 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).toBeDefined(); // Description may be truncated or use generic value in C100
}
// Verify journal entries with ALL extended B100 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 ALL extended B100 fields
if (original.transactionNumber !== undefined) {
expect(parsed.transactionNumber).toBeDefined();
}
if (original.transactionLineNumber !== undefined) {
expect(parsed.transactionLineNumber).toBeDefined();
}
if (original.batchNumber !== undefined) {
expect(parsed.batchNumber).toBeDefined();
}
if (original.transactionType !== undefined) {
expect(parsed.transactionType).toBe(original.transactionType);
}
if (original.referenceDocument !== undefined) {
expect(parsed.referenceDocument).toBe(original.referenceDocument);
}
if (original.referenceDocumentType !== undefined) {
expect(parsed.referenceDocumentType).toBe(original.referenceDocumentType);
}
if (original.referenceDocument2 !== undefined) {
expect(parsed.referenceDocument2).toBe(original.referenceDocument2);
}
if (original.referenceDocumentType2 !== undefined) {
expect(parsed.referenceDocumentType2).toBe(original.referenceDocumentType2);
}
if (original.valueDate !== undefined) {
expect(parsed.valueDate).toBe(original.valueDate);
}
if (original.counterAccountKey !== undefined) {
expect(parsed.counterAccountKey).toBe(original.counterAccountKey);
}
if (original.debitCreditIndicator !== undefined) {
expect(parsed.debitCreditIndicator).toBe(original.debitCreditIndicator);
}
if (original.currencyCode !== undefined) {
expect(parsed.currencyCode).toBe(original.currencyCode);
}
if (original.transactionAmount !== undefined) {
expect(parsed.transactionAmount).toBe(original.transactionAmount);
}
if (original.foreignCurrencyAmount !== undefined) {
expect(parsed.foreignCurrencyAmount).toBeDefined();
}
if (original.quantityField !== undefined) {
expect(parsed.quantityField).toBeDefined();
}
if (original.matchingField1 !== undefined) {
expect(parsed.matchingField1).toBe(original.matchingField1);
}
if (original.matchingField2 !== undefined) {
expect(parsed.matchingField2).toBe(original.matchingField2);
}
if (original.branchId !== undefined) {
expect(parsed.branchId).toBe(original.branchId);
}
if (original.entryDate !== undefined) {
expect(parsed.entryDate).toBe(original.entryDate);
}
if (original.operatorUsername !== undefined) {
expect(parsed.operatorUsername).toBe(original.operatorUsername);
}
if (original.reserved !== undefined) {
expect(parsed.reserved).toBe(original.reserved);
}
}
// Verify accounts with ALL extended B110 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 ALL 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) {
expect(parsed.vatId).toBeDefined();
}
if (original.totalDebits !== undefined) {
expect(parsed.totalDebits).toBeDefined();
}
if (original.totalCredits !== undefined) {
expect(parsed.totalCredits).toBeDefined();
}
if (original.accountingClassificationCode !== undefined) {
expect(parsed.accountingClassificationCode).toBe(original.accountingClassificationCode);
}
if (original.branchId !== undefined) {
expect(parsed.branchId).toBe(original.branchId);
}
if (original.openingBalanceForeignCurrency !== undefined) {
expect(parsed.openingBalanceForeignCurrency).toBeDefined();
}
if (original.foreignCurrencyCode !== undefined) {
expect(parsed.foreignCurrencyCode).toBe(original.foreignCurrencyCode);
}
}
// Verify inventory with special characters and fractional quantities
const intlItem = parsedData.data.inventory[0];
expect(intlItem.name).toContain('Professional Service Package Alpha');
expect(intlItem.quantity).toBeGreaterThanOrEqual(0); // Fractional may be rounded in M100
});
it('should handle foreign currencies and special characters correctly in round-trip', () => {
const foreignCurrencyInput: ReportInput = {
business: {
businessId: 'INTL_001',
name: 'International Business Corp™ & Associates',
taxId: '999888777',
address: {
street: 'Γλώσσα Street with Spëcial Chars',
houseNumber: '1א',
city: 'Jerusalem-Tel Aviv',
zip: '12345-6789',
},
reportingPeriod: {
startDate: '2024-01-01',
endDate: '2024-12-31',
},
},
documents: [
{
id: 'INTL_DOC_001',
type: '305',
date: '2024-06-15',
amount: 1234.567, // 3 decimal places
description: 'International transaction with special chars: €$£¥',
},
],
journalEntries: [
{
id: 'FOREX_001',
date: '2024-06-15',
amount: 1234.567,
accountId: 'EUR_ACCOUNT',
description: 'Foreign exchange transaction: USD→EUR→ILS',
currencyCode: 'EUR',
transactionAmount: 1234.567,
foreignCurrencyAmount: 4567.89,
debitCreditIndicator: '1',
branchId: 'INTL_BR',
operatorUsername: 'forex_usr',
},
],
accounts: [
{
id: 'EUR_ACCOUNT',
name: 'Euro Account - €urope Operations',
sortCode: {
key: 'Asset',
name: 'Foreign Currency Assets',
},
address: {
street: 'Européenne Straße',
houseNumber: '123β',
city: 'International City',
zip: 'INTL-001',
country: 'European Union',
},
countryCode: 'DE',
accountOpeningBalance: -1234.56, // Negative balance
foreignCurrencyCode: 'EUR',
openingBalanceForeignCurrency: -1123.45,
vatId: ' 123 456 789 ',
},
],
inventory: [
{
id: 'INTL_ITEM_001',
name: 'International Product™ with Spëcial Characters',
quantity: 12.5, // Fractional quantity
},
],
};
const result = generateUniformFormatReport(foreignCurrencyInput);
const parsedData = parseUniformFormatFiles(result.iniText, result.dataText);
// Verify special characters in business name and address
expect(parsedData.data.business.name).toContain('Corp™');
expect(parsedData.data.business.address?.street).toContain('Spëcial');
expect(parsedData.data.business.address?.houseNumber).toBe('1א');
// Verify foreign currency handling
const forexEntry = parsedData.data.journalEntries[0];
expect(forexEntry.currencyCode).toBe('EUR');
expect(forexEntry.description).toContain('USD→EUR→ILS');
expect(forexEntry.operatorUsername).toBe('forex_usr');
// Verify account with foreign currency
const eurAccount = parsedData.data.accounts[0];
expect(eurAccount).toBeDefined();
if (eurAccount) {
expect(eurAccount.name).toContain('€urope');
expect(eurAccount.countryCode).toBe('DE');
expect(eurAccount.foreignCurrencyCode).toBe('EUR');
expect(eurAccount.accountOpeningBalance).toBe(-1234.56); // Negative balance preserved
}
// Verify inventory with special characters and fractional quantities
const intlItem = parsedData.data.inventory[0];
expect(intlItem.name).toContain('Spëcial');
expect(intlItem.quantity).toBeGreaterThanOrEqual(0); // Fractional may be rounded in M100
});
it('should handle empty data sections 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);
expect(result).toBeDefined();
expect(result.dataText).toBeDefined();
expect(result.summary.totalRecords).toBe(5); // A000 + 2 A000Sum (A100, Z900) + A100 + Z900
// Parse the generated files back
const parsedData = parseUniformFormatFiles(result.iniText, result.dataText);
// Verify business metadata
expect(parsedData.data.business).toBeDefined();
expect(parsedData.data.business.name).toBe(minimalInput.business.name);
expect(parsedData.data.business.taxId).toBe(minimalInput.business.taxId);
// Verify empty arrays
expect(parsedData.data.documents).toEqual([]);
expect(parsedData.data.journalEntries).toEqual([]);
expect(parsedData.data.accounts).toEqual([]);
expect(parsedData.data.inventory).toEqual([]);
});
it('should maintain data integrity for monetary values', () => {
const input: ReportInput = {
business: {
businessId: '99999',
name: 'Financial Test Co',
taxId: '111111111',
reportingPeriod: {
startDate: '2024-01-01',
endDate: '2024-12-31',
},
},
documents: [
{
id: 'HIGH-VAL',
type: '320',
date: '2024-06-15',
amount: 999_999.99,
description: 'High value transaction',
},
],
journalEntries: [
{
id: 'PRECISION-TEST',
date: '2024-06-15',
amount: 123.45,
accountId: '2000',
description: 'Precision test',
},
],
accounts: [
{
id: '2000',
name: 'Test Account',
sortCode: {
key: 'Liability',
name: 'Liabilities',
},
accountOpeningBalance: 123_456.78,
},
],
inventory: [
{
id: 'EXPENSIVE-ITEM',
name: 'Expensive Product',
quantity: 1,
},
],
};
const result = generateUniformFormatReport(input);
// Parse the generated files back
const parsedData = parseUniformFormatFiles(result.iniText, result.dataText);
// Verify business metadata
expect(parsedData.data.business).toBeDefined();
expect(parsedData.data.business.name).toBe(input.business.name);
expect(parsedData.data.business.taxId).toBe(input.business.taxId);
// Verify high-value document was parsed correctly
expect(parsedData.data.documents).toHaveLength(1);
expect(parsedData.data.documents[0].amount).toBe(999_999.99);
expect(parsedData.data.documents[0].id).toBe('HIGH-VAL');
// Verify journal entry with precise monetary value
expect(parsedData.data.journalEntries).toHaveLength(1);
expect(parsedData.data.journalEntries[0].amount).toBe(123.45);
expect(parsedData.data.journalEntries[0].accountId).toBe('2000');
// Verify account balance
expect(parsedData.data.accounts).toHaveLength(1);
expect(parsedData.data.accounts[0].accountOpeningBalance).toBe(123_456.78);
expect(parsedData.data.accounts[0].id).toBe('2000');
// Verify inventory item
expect(parsedData.data.inventory).toHaveLength(1);
expect(parsedData.data.inventory[0].id).toBe('EXPENSIVE-ITEM');
expect(parsedData.data.inventory[0].name).toBe('Expensive Product');
});
});