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:
2025-08-11 12:25:32 +00:00
parent 01c6e8daad
commit 10e14af85b
53 changed files with 11315 additions and 17 deletions

View File

@@ -0,0 +1,317 @@
import type { ValidationResult } from './validation.types.js';
import { CodeLists } from './validation.types.js';
import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
import type { EInvoice } from '../../einvoice.js';
import type { IExtendedAccountingDocItem } from '../../interfaces/en16931-metadata.js';
/**
* Code List Validator for EN16931 compliance
* Validates against standard code lists (ISO, UNCL, UNECE)
*/
export class CodeListValidator {
private results: ValidationResult[] = [];
/**
* Validate all code lists in an invoice
*/
public validate(invoice: EInvoice): ValidationResult[] {
this.results = [];
// Currency validation
this.validateCurrency(invoice);
// Country codes
this.validateCountryCodes(invoice);
// Document type
this.validateDocumentType(invoice);
// Tax categories
this.validateTaxCategories(invoice);
// Payment means
this.validatePaymentMeans(invoice);
// Unit codes
this.validateUnitCodes(invoice);
return this.results;
}
/**
* Validate currency codes (ISO 4217)
*/
private validateCurrency(invoice: EInvoice): void {
// Document currency (BT-5)
if (invoice.currency) {
if (!CodeLists.ISO4217.codes.has(invoice.currency.toUpperCase())) {
this.addError(
'BR-CL-03',
`Invalid currency code: ${invoice.currency}. Must be ISO 4217`,
'EN16931',
'currency',
'BT-5',
invoice.currency,
Array.from(CodeLists.ISO4217.codes).join(', ')
);
}
}
// VAT accounting currency (BT-6)
const vatCurrency = invoice.metadata?.vatAccountingCurrency;
if (vatCurrency && !CodeLists.ISO4217.codes.has(vatCurrency.toUpperCase())) {
this.addError(
'BR-CL-04',
`Invalid VAT accounting currency: ${vatCurrency}. Must be ISO 4217`,
'EN16931',
'metadata.vatAccountingCurrency',
'BT-6',
vatCurrency,
Array.from(CodeLists.ISO4217.codes).join(', ')
);
}
}
/**
* Validate country codes (ISO 3166-1 alpha-2)
*/
private validateCountryCodes(invoice: EInvoice): void {
// Seller country (BT-40)
const sellerCountry = invoice.from?.address?.countryCode;
if (sellerCountry && !CodeLists.ISO3166.codes.has(sellerCountry.toUpperCase())) {
this.addError(
'BR-CL-14',
`Invalid seller country code: ${sellerCountry}. Must be ISO 3166-1 alpha-2`,
'EN16931',
'from.address.countryCode',
'BT-40',
sellerCountry,
'Two-letter country code (e.g., DE, FR, IT)'
);
}
// Buyer country (BT-55)
const buyerCountry = invoice.to?.address?.countryCode;
if (buyerCountry && !CodeLists.ISO3166.codes.has(buyerCountry.toUpperCase())) {
this.addError(
'BR-CL-15',
`Invalid buyer country code: ${buyerCountry}. Must be ISO 3166-1 alpha-2`,
'EN16931',
'to.address.countryCode',
'BT-55',
buyerCountry,
'Two-letter country code (e.g., DE, FR, IT)'
);
}
// Delivery country (BT-80)
const deliveryCountry = invoice.metadata?.deliveryAddress?.countryCode;
if (deliveryCountry && !CodeLists.ISO3166.codes.has(deliveryCountry.toUpperCase())) {
this.addError(
'BR-CL-16',
`Invalid delivery country code: ${deliveryCountry}. Must be ISO 3166-1 alpha-2`,
'EN16931',
'metadata.deliveryAddress.countryCode',
'BT-80',
deliveryCountry,
'Two-letter country code (e.g., DE, FR, IT)'
);
}
}
/**
* Validate document type code (UNCL1001)
*/
private validateDocumentType(invoice: EInvoice): void {
const typeCode = invoice.metadata?.documentTypeCode ||
(invoice.accountingDocType === 'invoice' ? '380' :
invoice.accountingDocType === 'creditnote' ? '381' :
invoice.accountingDocType === 'debitnote' ? '383' : null);
if (typeCode && !CodeLists.UNCL1001.codes.has(typeCode)) {
this.addError(
'BR-CL-01',
`Invalid document type code: ${typeCode}. Must be UNCL1001`,
'EN16931',
'metadata.documentTypeCode',
'BT-3',
typeCode,
Array.from(CodeLists.UNCL1001.codes.entries()).map(([k, v]) => `${k} (${v})`).join(', ')
);
}
}
/**
* Validate tax category codes (UNCL5305)
*/
private validateTaxCategories(invoice: EInvoice): void {
// Document level tax breakdown
// Note: taxBreakdown is a computed property that doesn't have metadata
// We would need to access the raw tax breakdown data from metadata if it exists
invoice.taxBreakdown?.forEach((breakdown, index) => {
// Since the computed taxBreakdown doesn't have metadata,
// we'll skip the tax category code validation for now
// This would need to be implemented differently to access the raw data
// TODO: Access raw tax breakdown data with metadata from invoice.metadata.taxBreakdown
// when that structure is implemented
});
// Line level tax categories
invoice.items?.forEach((item, index) => {
// Cast to extended type to access metadata
const extendedItem = item as IExtendedAccountingDocItem;
const categoryCode = extendedItem.metadata?.vatCategoryCode;
if (categoryCode && !CodeLists.UNCL5305.codes.has(categoryCode)) {
this.addError(
'BR-CL-10',
`Invalid line tax category: ${categoryCode}. Must be UNCL5305`,
'EN16931',
`items[${index}].metadata.vatCategoryCode`,
'BT-151',
categoryCode,
Array.from(CodeLists.UNCL5305.codes.entries()).map(([k, v]) => `${k} (${v})`).join(', ')
);
}
});
}
/**
* Validate payment means codes (UNCL4461)
*/
private validatePaymentMeans(invoice: EInvoice): void {
const paymentMeans = invoice.metadata?.paymentMeansCode;
if (paymentMeans && !CodeLists.UNCL4461.codes.has(paymentMeans)) {
this.addError(
'BR-CL-16',
`Invalid payment means code: ${paymentMeans}. Must be UNCL4461`,
'EN16931',
'metadata.paymentMeansCode',
'BT-81',
paymentMeans,
Array.from(CodeLists.UNCL4461.codes.entries()).map(([k, v]) => `${k} (${v})`).join(', ')
);
}
// Validate payment requirements based on means code
if (paymentMeans === '30' || paymentMeans === '58') { // Credit transfer
if (!invoice.metadata?.paymentAccount?.iban) {
this.addWarning(
'BR-CL-16-1',
`Payment means ${paymentMeans} (${CodeLists.UNCL4461.codes.get(paymentMeans)}) typically requires IBAN`,
'EN16931',
'metadata.paymentAccount.iban',
'BT-84'
);
}
}
}
/**
* Validate unit codes (UNECE Rec 20)
*/
private validateUnitCodes(invoice: EInvoice): void {
invoice.items?.forEach((item, index) => {
const unitCode = item.unitType;
if (unitCode && !CodeLists.UNECERec20.codes.has(unitCode)) {
this.addError(
'BR-CL-23',
`Invalid unit code: ${unitCode}. Must be UNECE Rec 20`,
'EN16931',
`items[${index}].unitCode`,
'BT-130',
unitCode,
'Common codes: C62 (one), KGM (kilogram), HUR (hour), DAY (day), MTR (metre)'
);
}
// Validate quantity is positive for standard units
if (unitCode && item.unitQuantity <= 0 && unitCode !== 'LS') { // LS = Lump sum can be 1
this.addError(
'BR-25',
`Quantity must be positive for unit ${unitCode}`,
'EN16931',
`items[${index}].quantity`,
'BT-129',
item.unitQuantity,
'> 0'
);
}
});
}
/**
* Add validation error
*/
private addError(
ruleId: string,
message: string,
source: string,
field: string,
btReference?: string,
value?: any,
expected?: any
): void {
this.results.push({
ruleId,
source,
severity: 'error',
message,
field,
btReference,
value,
expected,
codeList: this.getCodeListForRule(ruleId)
});
}
/**
* Add validation warning
*/
private addWarning(
ruleId: string,
message: string,
source: string,
field: string,
btReference?: string,
value?: any,
expected?: any
): void {
this.results.push({
ruleId,
source,
severity: 'warning',
message,
field,
btReference,
value,
expected,
codeList: this.getCodeListForRule(ruleId)
});
}
/**
* Get code list metadata for a rule
*/
private getCodeListForRule(ruleId: string): { name: string; version: string } | undefined {
if (ruleId.includes('CL-03') || ruleId.includes('CL-04')) {
return { name: 'ISO4217', version: CodeLists.ISO4217.version };
}
if (ruleId.includes('CL-14') || ruleId.includes('CL-15') || ruleId.includes('CL-16')) {
return { name: 'ISO3166', version: CodeLists.ISO3166.version };
}
if (ruleId.includes('CL-01')) {
return { name: 'UNCL1001', version: CodeLists.UNCL1001.version };
}
if (ruleId.includes('CL-10')) {
return { name: 'UNCL5305', version: CodeLists.UNCL5305.version };
}
if (ruleId.includes('CL-16')) {
return { name: 'UNCL4461', version: CodeLists.UNCL4461.version };
}
if (ruleId.includes('CL-23')) {
return { name: 'UNECERec20', version: CodeLists.UNECERec20.version };
}
return undefined;
}
}