feat(validation): Implement EN16931 compliance validation types and VAT categories
- Added validation types for EN16931 compliance in `validation.types.ts`, including interfaces for `ValidationResult`, `ValidationOptions`, and `ValidationReport`. - Introduced `VATCategoriesValidator` in `vat-categories.validator.ts` to validate VAT categories according to EN16931 rules, including detailed checks for standard, zero-rated, exempt, reverse charge, intra-community, export, and out-of-scope services. - Enhanced `IEInvoiceMetadata` interface in `en16931-metadata.ts` to include additional fields required for full standards compliance, such as delivery information, payment information, allowances, and charges. - Implemented helper methods for VAT calculations and validation logic to ensure accurate compliance with EN16931 standards.
This commit is contained in:
172
test/test.conformance-harness.ts
Normal file
172
test/test.conformance-harness.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle/index.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Import conformance harness
|
||||
import { ConformanceTestHarness, runConformanceTests } from '../ts/formats/validation/conformance.harness.js';
|
||||
|
||||
tap.test('Conformance Test Harness - initialization', async () => {
|
||||
const harness = new ConformanceTestHarness();
|
||||
expect(harness).toBeInstanceOf(ConformanceTestHarness);
|
||||
});
|
||||
|
||||
tap.test('Conformance Test Harness - load test samples', async () => {
|
||||
const harness = new ConformanceTestHarness();
|
||||
|
||||
// Check if test-samples directory exists
|
||||
const samplesDir = path.join(process.cwd(), 'test-samples');
|
||||
if (fs.existsSync(samplesDir)) {
|
||||
await harness.loadTestSamples(samplesDir);
|
||||
console.log('Test samples loaded successfully');
|
||||
} else {
|
||||
console.log('Test samples directory not found - skipping');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Conformance Test Harness - run minimal test', async (tools) => {
|
||||
const harness = new ConformanceTestHarness();
|
||||
|
||||
// Create a minimal test sample
|
||||
const minimalUBL = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017</cbc:CustomizationID>
|
||||
<cbc:ID>TEST-001</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-11</cbc:IssueDate>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Test Seller</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street 1</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Test Buyer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street 2</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="EUR">100.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>19</cbc:Percent>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:TaxExclusiveAmount currencyID="EUR">100.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="EUR">119.00</cbc:TaxInclusiveAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Product</cbc:Name>
|
||||
<cac:ClassifiedTaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>19</cbc:Percent>
|
||||
</cac:ClassifiedTaxCategory>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
// Create temporary test directory
|
||||
const tempDir = path.join(process.cwd(), '.nogit', 'test-conformance');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write test file
|
||||
const testFile = path.join(tempDir, 'minimal-test.xml');
|
||||
fs.writeFileSync(testFile, minimalUBL);
|
||||
|
||||
// Create test sample metadata
|
||||
const testSamples = [{
|
||||
id: 'minimal-test',
|
||||
name: 'minimal-test.xml',
|
||||
path: testFile,
|
||||
format: 'UBL' as const,
|
||||
standard: 'EN16931',
|
||||
expectedValid: false, // We expect some validation errors
|
||||
description: 'Minimal test invoice'
|
||||
}];
|
||||
|
||||
// Load test samples manually
|
||||
(harness as any).testSamples = testSamples;
|
||||
|
||||
// Run conformance test
|
||||
await harness.runConformanceTests();
|
||||
|
||||
// Generate coverage matrix
|
||||
const coverage = harness.generateCoverageMatrix();
|
||||
console.log(`Coverage: ${coverage.coveragePercentage.toFixed(1)}%`);
|
||||
console.log(`Rules covered: ${coverage.coveredRules}/${coverage.totalRules}`);
|
||||
|
||||
// Clean up
|
||||
fs.unlinkSync(testFile);
|
||||
});
|
||||
|
||||
tap.test('Conformance Test Harness - coverage report generation', async () => {
|
||||
const harness = new ConformanceTestHarness();
|
||||
|
||||
// Generate empty coverage report
|
||||
const coverage = harness.generateCoverageMatrix();
|
||||
|
||||
expect(coverage.totalRules).toBeGreaterThan(100);
|
||||
expect(coverage.coveredRules).toBeGreaterThanOrEqual(0);
|
||||
expect(coverage.coveragePercentage).toBeGreaterThanOrEqual(0);
|
||||
expect(coverage.byCategory.document.total).toBeGreaterThan(0);
|
||||
expect(coverage.byCategory.calculation.total).toBeGreaterThan(0);
|
||||
expect(coverage.byCategory.vat.total).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('Conformance Test Harness - full test suite', async (tools) => {
|
||||
tools.timeout(60000); // 60 seconds timeout for full test
|
||||
|
||||
const samplesDir = path.join(process.cwd(), 'test-samples');
|
||||
if (!fs.existsSync(samplesDir)) {
|
||||
console.log('Test samples not found - skipping full conformance test');
|
||||
console.log('Run: npm run download-test-samples');
|
||||
return;
|
||||
}
|
||||
|
||||
// Run full conformance test
|
||||
console.log('\n=== Running Full Conformance Test Suite ===\n');
|
||||
await runConformanceTests(samplesDir, true);
|
||||
|
||||
// Check if HTML report was generated
|
||||
const reportPath = path.join(process.cwd(), 'coverage-report.html');
|
||||
if (fs.existsSync(reportPath)) {
|
||||
console.log(`\n✅ HTML report generated: ${reportPath}`);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap;
|
Reference in New Issue
Block a user