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:
238
test/test.en16931-validators.ts
Normal file
238
test/test.en16931-validators.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../ts/index.js';
|
||||
import { ValidationLevel } from '../ts/interfaces/common.js';
|
||||
|
||||
// Test EN16931 business rules and code list validators
|
||||
tap.test('EN16931 Validators - should validate business rules with feature flags', async () => {
|
||||
// Create a minimal invoice that violates several EN16931 rules
|
||||
const invoice = new EInvoice();
|
||||
|
||||
// Set some basic fields but leave mandatory ones missing
|
||||
invoice.currency = 'EUR';
|
||||
invoice.date = Date.now();
|
||||
invoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Seller',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10115',
|
||||
countryCode: 'DE'
|
||||
}
|
||||
} as any;
|
||||
|
||||
// Missing buyer details and invoice ID (violates BR-02, BR-07)
|
||||
|
||||
// Add an item with calculation issues
|
||||
invoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Item',
|
||||
unitType: 'C62', // Valid UNECE code
|
||||
unitQuantity: 10,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Test without feature flags (should pass basic validation)
|
||||
const basicResult = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
console.log('Basic validation errors:', basicResult.errors.length);
|
||||
|
||||
// Test with EN16931 business rules feature flag
|
||||
const en16931Result = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES'],
|
||||
checkCalculations: true,
|
||||
checkVAT: true
|
||||
});
|
||||
|
||||
console.log('EN16931 validation errors:', en16931Result.errors.length);
|
||||
|
||||
// Should find missing mandatory fields
|
||||
const mandatoryErrors = en16931Result.errors.filter(e =>
|
||||
e.code && ['BR-01', 'BR-02', 'BR-07'].includes(e.code)
|
||||
);
|
||||
expect(mandatoryErrors.length).toBeGreaterThan(0);
|
||||
|
||||
// Test code list validation
|
||||
const codeListResult = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['CODE_LIST_VALIDATION'],
|
||||
checkCodeLists: true
|
||||
});
|
||||
|
||||
console.log('Code list validation errors:', codeListResult.errors.length);
|
||||
|
||||
// Test invalid currency code
|
||||
invoice.currency = 'XXX' as any; // Invalid currency
|
||||
const currencyResult = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['CODE_LIST_VALIDATION']
|
||||
});
|
||||
|
||||
const currencyErrors = currencyResult.errors.filter(e =>
|
||||
e.code && e.code.includes('BR-CL-03')
|
||||
);
|
||||
expect(currencyErrors.length).toEqual(1);
|
||||
|
||||
// Test with both validators enabled
|
||||
const fullResult = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
|
||||
checkCalculations: true,
|
||||
checkVAT: true,
|
||||
checkCodeLists: true,
|
||||
reportOnly: true // Don't fail validation, just report
|
||||
});
|
||||
|
||||
console.log('Full validation with both validators:');
|
||||
console.log('- Total errors:', fullResult.errors.length);
|
||||
console.log('- Valid (report-only mode):', fullResult.valid);
|
||||
|
||||
expect(fullResult.valid).toEqual(true); // Should be true in report-only mode
|
||||
expect(fullResult.errors.length).toBeGreaterThan(0); // Should find issues
|
||||
console.log('Error codes found:', fullResult.errors.map(e => e.code));
|
||||
});
|
||||
|
||||
tap.test('EN16931 Validators - should validate calculations correctly', async () => {
|
||||
const invoice = new EInvoice();
|
||||
|
||||
// Set up a complete invoice with correct mandatory fields
|
||||
invoice.accountingDocId = 'INV-2024-001';
|
||||
invoice.currency = 'EUR';
|
||||
invoice.date = Date.now();
|
||||
invoice.metadata = {
|
||||
customizationId: 'urn:cen.eu:en16931:2017'
|
||||
};
|
||||
|
||||
invoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Seller GmbH',
|
||||
address: {
|
||||
streetName: 'Hauptstraße',
|
||||
houseNumber: '1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10115',
|
||||
countryCode: 'DE'
|
||||
}
|
||||
} as any;
|
||||
|
||||
invoice.to = {
|
||||
type: 'company',
|
||||
name: 'Test Buyer Ltd',
|
||||
address: {
|
||||
streetName: 'Main Street',
|
||||
houseNumber: '10',
|
||||
city: 'London',
|
||||
postalCode: 'SW1A 1AA',
|
||||
countryCode: 'GB'
|
||||
}
|
||||
} as any;
|
||||
|
||||
// Add items with specific amounts
|
||||
invoice.items = [
|
||||
{
|
||||
position: 1,
|
||||
name: 'Product A',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 5,
|
||||
unitNetPrice: 100.00,
|
||||
vatPercentage: 19
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
name: 'Product B',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 3,
|
||||
unitNetPrice: 50.00,
|
||||
vatPercentage: 19
|
||||
}
|
||||
];
|
||||
|
||||
// Expected calculations:
|
||||
// Line 1: 5 * 100 = 500
|
||||
// Line 2: 3 * 50 = 150
|
||||
// Total net: 650
|
||||
// VAT (19%): 123.50
|
||||
// Total gross: 773.50
|
||||
|
||||
const result = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES'],
|
||||
checkCalculations: true,
|
||||
tolerance: 0.01
|
||||
});
|
||||
|
||||
// Should not have calculation errors
|
||||
const calcErrors = result.errors.filter(e =>
|
||||
e.code && e.code.startsWith('BR-CO-')
|
||||
);
|
||||
|
||||
console.log('Calculation validation errors:', calcErrors);
|
||||
expect(calcErrors.length).toEqual(0);
|
||||
|
||||
// Verify computed totals
|
||||
expect(invoice.totalNet).toEqual(650);
|
||||
expect(invoice.totalVat).toEqual(123.50);
|
||||
expect(invoice.totalGross).toEqual(773.50);
|
||||
});
|
||||
|
||||
tap.test('EN16931 Validators - should validate VAT rules correctly', async () => {
|
||||
const invoice = new EInvoice();
|
||||
|
||||
// Set up mandatory fields
|
||||
invoice.accountingDocId = 'INV-2024-002';
|
||||
invoice.currency = 'EUR';
|
||||
invoice.date = Date.now();
|
||||
invoice.metadata = {
|
||||
customizationId: 'urn:cen.eu:en16931:2017'
|
||||
};
|
||||
|
||||
invoice.from = {
|
||||
type: 'company',
|
||||
name: 'Seller',
|
||||
address: { countryCode: 'DE' }
|
||||
} as any;
|
||||
|
||||
invoice.to = {
|
||||
type: 'company',
|
||||
name: 'Buyer',
|
||||
address: { countryCode: 'FR' }
|
||||
} as any;
|
||||
|
||||
// Add mixed VAT rate items
|
||||
invoice.items = [
|
||||
{
|
||||
position: 1,
|
||||
name: 'Standard rated item',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19 // Standard rate
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
name: 'Zero rated item',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 0 // Zero rate
|
||||
}
|
||||
];
|
||||
|
||||
const result = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES'],
|
||||
checkVAT: true
|
||||
});
|
||||
|
||||
// Check for VAT breakdown requirements
|
||||
const vatErrors = result.errors.filter(e =>
|
||||
e.code && (e.code.startsWith('BR-S-') || e.code.startsWith('BR-Z-'))
|
||||
);
|
||||
|
||||
console.log('VAT validation results:');
|
||||
console.log('- VAT errors found:', vatErrors.length);
|
||||
console.log('- Tax breakdown:', invoice.taxBreakdown);
|
||||
|
||||
// Should have proper tax breakdown
|
||||
expect(invoice.taxBreakdown.length).toEqual(2);
|
||||
expect(invoice.taxBreakdown.find(t => t.taxPercent === 19)).toBeTruthy();
|
||||
expect(invoice.taxBreakdown.find(t => t.taxPercent === 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user