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; } }