import { CIIBaseValidator } from '../cii.validator.js'; import { ValidationLevel } from '../../../interfaces/common.js'; import type { ValidationResult } from '../../../interfaces/common.js'; /** * Validator for Factur-X invoice format * Implements validation rules according to EN16931 and Factur-X specification */ export class FacturXValidator extends CIIBaseValidator { /** * Validates structure of the Factur-X XML document * @returns True if structure validation passed */ protected validateStructure(): boolean { if (!this.doc) return false; let valid = true; // Check for required main sections const sections = [ 'rsm:ExchangedDocumentContext', 'rsm:ExchangedDocument', 'rsm:SupplyChainTradeTransaction' ]; for (const section of sections) { if (!this.exists(section)) { this.addError('FX-STRUCT-1', `Required section ${section} is missing`, '/rsm:CrossIndustryInvoice'); valid = false; } } // Check for SupplyChainTradeTransaction sections if (this.exists('rsm:SupplyChainTradeTransaction')) { const tradeSubsections = [ 'ram:ApplicableHeaderTradeAgreement', 'ram:ApplicableHeaderTradeDelivery', 'ram:ApplicableHeaderTradeSettlement' ]; for (const subsection of tradeSubsections) { if (!this.exists(`rsm:SupplyChainTradeTransaction/${subsection}`)) { this.addError('FX-STRUCT-2', `Required subsection ${subsection} is missing`, '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction'); valid = false; } } } return valid; } /** * Validates business rules * @returns True if business rule validation passed */ protected validateBusinessRules(): boolean { if (!this.doc) return false; let valid = true; // BR-16: Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) - Paid amount (BT-113) valid = this.validateAmounts() && valid; // BR-CO-3: Value added tax point date (BT-7) and Value added tax point date code (BT-8) are mutually exclusive valid = this.validateMutuallyExclusiveFields() && valid; // BR-S-1: An Invoice that contains a line (BG-25) where the Invoiced item VAT category code (BT-151) is "Standard rated" // shall contain the Seller VAT Identifier (BT-31), the Seller tax registration identifier (BT-32) // and/or the Seller tax representative VAT identifier (BT-63). valid = this.validateSellerVatIdentifier() && valid; return valid; } /** * Validates amount calculations in the invoice * @returns True if amount validation passed */ private validateAmounts(): boolean { if (!this.doc) return false; try { // Extract amounts const totalAmount = this.getNumber( '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount' ); const paidAmount = this.getNumber( '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount' ) || 0; const dueAmount = this.getNumber( '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount' ); // Calculate expected due amount const expectedDueAmount = totalAmount - paidAmount; // Compare with a small tolerance for rounding errors if (Math.abs(dueAmount - expectedDueAmount) > 0.01) { this.addError( 'BR-16', `Amount due for payment (${dueAmount}) must equal Invoice total amount with VAT (${totalAmount}) - Paid amount (${paidAmount})`, '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation' ); return false; } return true; } catch (error) { this.addError('FX-AMOUNT', `Error validating amounts: ${error}`, '/'); return false; } } /** * Validates mutually exclusive fields * @returns True if validation passed */ private validateMutuallyExclusiveFields(): boolean { if (!this.doc) return false; try { // Check for VAT point date and code (BR-CO-3) const vatPointDate = this.exists('//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:TaxPointDate'); const vatPointDateCode = this.exists('//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:DueDateTypeCode'); if (vatPointDate && vatPointDateCode) { this.addError( 'BR-CO-3', 'Value added tax point date and Value added tax point date code are mutually exclusive', '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax' ); return false; } return true; } catch (error) { this.addError('FX-MUTUAL', `Error validating mutually exclusive fields: ${error}`, '/'); return false; } } /** * Validates seller VAT identifier requirements * @returns True if validation passed */ private validateSellerVatIdentifier(): boolean { if (!this.doc) return false; try { // Check if there are any standard rated line items const standardRatedItems = this.exists( '//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode[text()="S"]' ); if (standardRatedItems) { // Check for seller VAT identifier const sellerVatId = this.exists('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]'); const sellerTaxId = this.exists('//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]'); const sellerTaxRepId = this.exists('//ram:ApplicableHeaderTradeAgreement/ram:SellerTaxRepresentativeTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]'); if (!sellerVatId && !sellerTaxId && !sellerTaxRepId) { this.addError( 'BR-S-1', 'An Invoice with standard rated items must contain the Seller VAT Identifier, Tax registration identifier or Tax representative VAT identifier', '//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty' ); return false; } } return true; } catch (error) { this.addError('FX-VAT', `Error validating seller VAT identifier: ${error}`, '/'); return false; } } }