2025-05-30 18:18:42 +00:00
import { UBLBaseValidator } from './ubl.validator.js' ;
import { ValidationLevel } from '../../interfaces/common.js' ;
import { xpath } from '../../plugins.js' ;
/**
* EN16931-compliant UBL validator that implements all business rules
*/
export class EN16931UBLValidator extends UBLBaseValidator {
/**
* Validates the structure of the UBL document
*/
protected validateStructure ( ) : boolean {
let valid = true ;
// Check for required elements
const requiredElements = [
{ path : '//cbc:ID' , error : 'Required element cbc:ID is missing' } ,
{ path : '//cbc:IssueDate' , error : 'Required element cbc:IssueDate is missing' } ,
{ path : '//cbc:CustomizationID' , error : 'Required element cbc:CustomizationID is missing' }
] ;
for ( const element of requiredElements ) {
if ( ! this . exists ( element . path ) ) {
this . addError ( 'STRUCT-REQUIRED' , element . error , element . path ) ;
valid = false ;
}
}
// Check for at least one invoice line or credit note line
const invoiceLines = this . select ( '//cac:InvoiceLine' , this . doc ) as Node [ ] ;
const creditNoteLines = this . select ( '//cac:CreditNoteLine' , this . doc ) as Node [ ] ;
if ( invoiceLines . length === 0 && creditNoteLines . length === 0 ) {
this . addError ( 'STRUCT-LINE' , 'At least one invoice line or credit note line is required' , '/' ) ;
valid = false ;
}
return valid ;
}
/**
* Validates EN16931 business rules
*/
protected validateBusinessRules ( ) : boolean {
let valid = true ;
// BR-01: An Invoice shall have a Specification identifier (BT-24).
if ( ! this . exists ( '//cbc:CustomizationID' ) ) {
this . addError ( 'BR-01' , 'An Invoice shall have a Specification identifier' , '//cbc:CustomizationID' ) ;
valid = false ;
}
// BR-02: An Invoice shall have an Invoice number (BT-1).
if ( ! this . exists ( '//cbc:ID' ) ) {
this . addError ( 'BR-02' , 'An Invoice shall have an Invoice number' , '//cbc:ID' ) ;
valid = false ;
}
// BR-03: An Invoice shall have an Invoice issue date (BT-2).
if ( ! this . exists ( '//cbc:IssueDate' ) ) {
this . addError ( 'BR-03' , 'An Invoice shall have an Invoice issue date' , '//cbc:IssueDate' ) ;
valid = false ;
}
// BR-04: An Invoice shall have an Invoice type code (BT-3).
const isInvoice = this . doc . documentElement . localName === 'Invoice' ;
if ( isInvoice && ! this . exists ( '//cbc:InvoiceTypeCode' ) ) {
this . addError ( 'BR-04' , 'An Invoice shall have an Invoice type code' , '//cbc:InvoiceTypeCode' ) ;
valid = false ;
}
// BR-05: An Invoice shall have an Invoice currency code (BT-5).
if ( ! this . exists ( '//cbc:DocumentCurrencyCode' ) ) {
this . addError ( 'BR-05' , 'An Invoice shall have an Invoice currency code' , '//cbc:DocumentCurrencyCode' ) ;
valid = false ;
}
// BR-06: An Invoice shall contain the Seller name (BT-27).
if ( ! this . exists ( '//cac:AccountingSupplierParty//cbc:RegistrationName' ) &&
! this . exists ( '//cac:AccountingSupplierParty//cbc:Name' ) ) {
this . addError ( 'BR-06' , 'An Invoice shall contain the Seller name' , '//cac:AccountingSupplierParty' ) ;
valid = false ;
}
// BR-07: An Invoice shall contain the Buyer name (BT-44).
if ( ! this . exists ( '//cac:AccountingCustomerParty//cbc:RegistrationName' ) &&
! this . exists ( '//cac:AccountingCustomerParty//cbc:Name' ) ) {
this . addError ( 'BR-07' , 'An Invoice shall contain the Buyer name' , '//cac:AccountingCustomerParty' ) ;
valid = false ;
}
// BR-08: An Invoice shall contain the Seller postal address (BG-5).
2026-04-16 20:30:56 +00:00
const sellerAddressResult = this . select ( '//cac:AccountingSupplierParty//cac:PostalAddress' , this . doc ) ;
const sellerAddress = Array . isArray ( sellerAddressResult ) ? sellerAddressResult [ 0 ] : null ;
2025-05-30 18:18:42 +00:00
if ( ! sellerAddress || ! this . exists ( './/cbc:IdentificationCode' , sellerAddress ) ) {
this . addError ( 'BR-08' , 'An Invoice shall contain the Seller postal address' , '//cac:AccountingSupplierParty//cac:PostalAddress' ) ;
valid = false ;
}
// BR-09: The Seller postal address (BG-5) shall contain a Seller country code (BT-40).
if ( sellerAddress && ! this . exists ( './/cac:Country/cbc:IdentificationCode' , sellerAddress ) ) {
this . addError ( 'BR-09' , 'The Seller postal address shall contain a Seller country code' , '//cac:AccountingSupplierParty//cac:PostalAddress//cac:Country' ) ;
valid = false ;
}
// BR-10: An Invoice shall contain the Buyer postal address (BG-8).
2026-04-16 20:30:56 +00:00
const buyerAddressResult = this . select ( '//cac:AccountingCustomerParty//cac:PostalAddress' , this . doc ) ;
const buyerAddress = Array . isArray ( buyerAddressResult ) ? buyerAddressResult [ 0 ] : null ;
2025-05-30 18:18:42 +00:00
if ( ! buyerAddress || ! this . exists ( './/cbc:IdentificationCode' , buyerAddress ) ) {
this . addError ( 'BR-10' , 'An Invoice shall contain the Buyer postal address' , '//cac:AccountingCustomerParty//cac:PostalAddress' ) ;
valid = false ;
}
// BR-11: The Buyer postal address (BG-8) shall contain a Buyer country code (BT-55).
if ( buyerAddress && ! this . exists ( './/cac:Country/cbc:IdentificationCode' , buyerAddress ) ) {
this . addError ( 'BR-11' , 'The Buyer postal address shall contain a Buyer country code' , '//cac:AccountingCustomerParty//cac:PostalAddress//cac:Country' ) ;
valid = false ;
}
// BR-12: An Invoice shall have the Sum of Invoice line net amount (BT-106).
if ( ! this . exists ( '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount' ) ) {
this . addError ( 'BR-12' , 'An Invoice shall have the Sum of Invoice line net amount' , '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount' ) ;
valid = false ;
}
// BR-13: An Invoice shall have the Invoice total amount without VAT (BT-109).
if ( ! this . exists ( '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount' ) ) {
this . addError ( 'BR-13' , 'An Invoice shall have the Invoice total amount without VAT' , '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount' ) ;
valid = false ;
}
// BR-14: An Invoice shall have the Invoice total amount with VAT (BT-112).
if ( ! this . exists ( '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount' ) ) {
this . addError ( 'BR-14' , 'An Invoice shall have the Invoice total amount with VAT' , '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount' ) ;
valid = false ;
}
// BR-15: An Invoice shall have the Amount due for payment (BT-115).
if ( ! this . exists ( '//cac:LegalMonetaryTotal/cbc:PayableAmount' ) ) {
this . addError ( 'BR-15' , 'An Invoice shall have the Amount due for payment' , '//cac:LegalMonetaryTotal/cbc:PayableAmount' ) ;
valid = false ;
}
// BR-16: An Invoice shall have at least one Invoice line (BG-25).
const lines = this . select ( '//cac:InvoiceLine | //cac:CreditNoteLine' , this . doc ) as Node [ ] ;
if ( lines . length === 0 ) {
this . addError ( 'BR-16' , 'An Invoice shall have at least one Invoice line' , '//cac:InvoiceLine' ) ;
valid = false ;
}
// Validate calculation rules if we have the necessary data
if ( this . exists ( '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount' ) ) {
valid = this . validateCalculationRules ( ) && valid ;
}
return valid ;
}
/**
* Validates calculation rules (BR-CO-*)
*/
private validateCalculationRules ( ) : boolean {
let valid = true ;
// BR-CO-10: Sum of Invoice line net amount = Σ Invoice line net amount.
const lineExtensionAmount = this . getNumber ( '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount' ) ;
const lines = this . select ( '//cac:InvoiceLine | //cac:CreditNoteLine' , this . doc ) as Node [ ] ;
let calculatedSum = 0 ;
for ( const line of lines ) {
const lineAmount = this . getNumber ( './/cbc:LineExtensionAmount' , line ) ;
calculatedSum += lineAmount ;
}
// Allow for small rounding differences (0.01)
if ( Math . abs ( lineExtensionAmount - calculatedSum ) > 0.01 ) {
this . addError (
'BR-CO-10' ,
` Sum of Invoice line net amount ( ${ lineExtensionAmount } ) must equal sum of line amounts ( ${ calculatedSum } ) ` ,
'//cac:LegalMonetaryTotal/cbc:LineExtensionAmount'
) ;
valid = false ;
}
// BR-CO-13: Invoice total amount without VAT = Σ Invoice line net amount - Sum of allowances on document level + Sum of charges on document level.
const taxExclusiveAmount = this . getNumber ( '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount' ) ;
const allowanceTotal = this . getNumber ( '//cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount' ) || 0 ;
const chargeTotal = this . getNumber ( '//cac:LegalMonetaryTotal/cbc:ChargeTotalAmount' ) || 0 ;
const calculatedTaxExclusive = lineExtensionAmount - allowanceTotal + chargeTotal ;
if ( Math . abs ( taxExclusiveAmount - calculatedTaxExclusive ) > 0.01 ) {
this . addError (
'BR-CO-13' ,
` Invoice total amount without VAT ( ${ taxExclusiveAmount } ) must equal calculated amount ( ${ calculatedTaxExclusive } ) ` ,
'//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'
) ;
valid = false ;
}
// BR-CO-15: Invoice total amount with VAT = Invoice total amount without VAT + Invoice total VAT amount.
const taxInclusiveAmount = this . getNumber ( '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount' ) ;
const totalTaxAmount = this . getNumber ( '//cac:TaxTotal/cbc:TaxAmount' ) || 0 ;
const calculatedTaxInclusive = taxExclusiveAmount + totalTaxAmount ;
if ( Math . abs ( taxInclusiveAmount - calculatedTaxInclusive ) > 0.01 ) {
this . addError (
'BR-CO-15' ,
` Invoice total amount with VAT ( ${ taxInclusiveAmount } ) must equal calculated amount ( ${ calculatedTaxInclusive } ) ` ,
'//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount'
) ;
valid = false ;
}
return valid ;
}
2026-04-16 20:30:56 +00:00
}