xinvoice/ts/formats/cii/facturx/facturx.validator.ts

181 lines
6.3 KiB
TypeScript
Raw Normal View History

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