- 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.
238 lines
6.6 KiB
TypeScript
238 lines
6.6 KiB
TypeScript
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(); |