12 Commits

Author SHA1 Message Date
73617e46e4 4.0.0
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-03 16:41:33 +00:00
a932d68f86 working 2025-04-03 16:41:10 +00:00
21650f1181 update 2025-04-03 15:53:08 +00:00
3e8b5c2869 update 2025-04-03 13:26:27 +00:00
05a2edc70c 3.0.1
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-20 14:47:44 +00:00
4835e12d15 fix(test/pdf-export): Improve PDF export tests with detailed logging and enhanced embedded file structure verification. 2025-03-20 14:47:43 +00:00
5763240633 3.0.0
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-20 14:39:32 +00:00
9510d851af BREAKING CHANGE(XInvoice): Refactor XInvoice API for XML handling and PDF export by replacing deprecated methods (addXmlString and getParsedXmlData) with fromXml and loadXml, and by introducing a new ExportFormat type for type-safe export. Update tests accordingly. 2025-03-20 14:39:32 +00:00
d954fb4768 2.0.0
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-20 13:57:45 +00:00
6906e2f778 BREAKING CHANGE(core): Refactor contact and PDF handling across the library by replacing IContact with TContact and updating PDF processing to use a structured IPdf object. These changes ensure that empty contact objects include registration details, founded/closed dates, and status, and that PDF loading/exporting uniformly wraps buffers in a proper object. 2025-03-20 13:57:45 +00:00
75b720a98d update 2025-03-20 13:06:42 +00:00
024b7feb09 start switch to better architecture. 2025-03-19 15:55:40 +00:00
68 changed files with 5333 additions and 4212 deletions

View File

@ -1,5 +1,29 @@
# Changelog
## 2025-03-20 - 3.0.1 - fix(test/pdf-export)
Improve PDF export tests with detailed logging and enhanced embedded file structure verification.
- Log original PDF size and compute size increases per export format
- Print a table of format-specific PDF size details
- Verify the PDF catalog contains the 'Names' dictionary, 'EmbeddedFiles' entry, and a valid 'Names' array
- Ensure type safety for export format parameters
## 2025-03-20 - 3.0.0 - BREAKING CHANGE(XInvoice)
Refactor XInvoice API for XML handling and PDF export by replacing deprecated methods (addXmlString and getParsedXmlData) with fromXml and loadXml, and by introducing a new ExportFormat type for type-safe export. Update tests accordingly.
- Removed usage of addXmlString and getParsedXmlData in favor of XInvoice.fromXml and loadXml for XML processing.
- Added ExportFormat type and enforced type-safety in exportXml and exportPdf methods.
- Updated test files to adapt to the new API, ensuring proper error handling and API consistency.
- Revised expectations in tests to check for new methods (loadXml, validate, exportXml, exportPdf) and properties.
## 2025-03-20 - 2.0.0 - BREAKING CHANGE(core)
Refactor contact and PDF handling across the library by replacing IContact with TContact and updating PDF processing to use a structured IPdf object. These changes ensure that empty contact objects include registration details, founded/closed dates, and status, and that PDF loading/exporting uniformly wraps buffers in a proper object.
- Updated createEmptyContact (renamed in documentation to reflect TContact) to return a complete TContact object with registrationDetails, foundedDate, closedDate, and status.
- Modified loadPdf and exportPdf in XInvoice to wrap PDF buffers in an IPdf object with name, id, and metadata instead of using a raw Uint8Array.
- Replaced IContact with TContact in FacturXEncoder, FacturXDecoder, and XInvoiceDecoder to standardize contact structure.
- Aligned address and contact data across decoders and encoders for consistency.
## 2025-03-17 - 1.3.3 - fix(commitinfo)
Synchronize commit info version with package.json version

215
examples/pdf-handling.ts Normal file
View File

@ -0,0 +1,215 @@
import { PDFEmbedder, PDFExtractor, TInvoice, FacturXEncoder } from '../ts/index.js';
import * as fs from 'fs/promises';
/**
* Example demonstrating how to use the PDF handling classes
*/
async function pdfHandlingExample() {
try {
// Create a sample invoice
const invoice: TInvoice = createSampleInvoice();
// Create a Factur-X encoder
const encoder = new FacturXEncoder();
// Generate XML
const xmlContent = await encoder.encode(invoice);
console.log('Generated XML:');
console.log(xmlContent.substring(0, 500) + '...');
// Load a sample PDF
const pdfBuffer = await fs.readFile('examples/sample.pdf');
console.log(`Loaded PDF (${pdfBuffer.length} bytes)`);
// Create a PDF embedder
const embedder = new PDFEmbedder();
// Embed XML into PDF
const modifiedPdfBuffer = await embedder.embedXml(
pdfBuffer,
xmlContent,
'factur-x.xml',
'Factur-X XML Invoice'
);
console.log(`Created modified PDF (${modifiedPdfBuffer.length} bytes)`);
// Save the modified PDF
await fs.writeFile('examples/output.pdf', modifiedPdfBuffer);
console.log('Saved modified PDF to examples/output.pdf');
// Create a PDF extractor
const extractor = new PDFExtractor();
// Extract XML from the modified PDF
const extractedXml = await extractor.extractXml(modifiedPdfBuffer);
console.log('Extracted XML:');
console.log(extractedXml ? extractedXml.substring(0, 500) + '...' : 'No XML found');
// Save the extracted XML
if (extractedXml) {
await fs.writeFile('examples/extracted.xml', extractedXml);
console.log('Saved extracted XML to examples/extracted.xml');
}
console.log('PDF handling example completed successfully');
} catch (error) {
console.error('Error in PDF handling example:', error);
}
}
/**
* Creates a sample invoice for testing
* @returns Sample invoice
*/
function createSampleInvoice(): TInvoice {
return {
type: 'invoice',
id: 'INV-2023-001',
invoiceType: 'debitnote',
date: Date.now(),
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: 'INV-2023-001',
from: {
type: 'company',
name: 'Supplier Company',
description: 'Supplier',
address: {
streetName: 'Supplier Street',
houseNumber: '123',
postalCode: '12345',
city: 'Supplier City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB12345',
registrationName: 'Supplier Company GmbH'
}
},
to: {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '456',
postalCode: '54321',
city: 'Customer City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2005,
month: 6,
day: 15
},
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB54321',
registrationName: 'Customer Company GmbH'
}
},
subject: 'Invoice INV-2023-001',
content: {
invoiceData: {
id: 'INV-2023-001',
status: null,
type: 'debitnote',
billedBy: {
type: 'company',
name: 'Supplier Company',
description: 'Supplier',
address: {
streetName: 'Supplier Street',
houseNumber: '123',
postalCode: '12345',
city: 'Supplier City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB12345',
registrationName: 'Supplier Company GmbH'
}
},
billedTo: {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '456',
postalCode: '54321',
city: 'Customer City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2005,
month: 6,
day: 15
},
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB54321',
registrationName: 'Customer Company GmbH'
}
},
deliveryDate: Date.now(),
dueInDays: 30,
periodOfPerformance: null,
printResult: null,
currency: 'EUR',
notes: ['Thank you for your business'],
items: [
{
position: 1,
name: 'Product A',
articleNumber: 'PROD-A',
unitType: 'EA',
unitQuantity: 2,
unitNetPrice: 100,
vatPercentage: 19
},
{
position: 2,
name: 'Service B',
articleNumber: 'SERV-B',
unitType: 'HUR',
unitQuantity: 5,
unitNetPrice: 80,
vatPercentage: 19
}
],
reverseCharge: false
},
textData: null,
timesheetData: null,
contractData: null
}
} as TInvoice;
}
// Run the example
pdfHandlingExample();

View File

@ -1,6 +1,6 @@
{
"name": "@fin.cx/xinvoice",
"version": "1.3.3",
"version": "4.0.0",
"private": false,
"description": "A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for xinvoice packages.",
"main": "dist_ts/index.js",
@ -14,18 +14,18 @@
"buildDocs": "(tsdoc)"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.2.7",
"@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsbundle": "^2.2.5",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.96",
"@push.rocks/tapbundle": "^5.6.0",
"@types/node": "^22.13.10"
"@push.rocks/tapbundle": "^5.6.2",
"@types/node": "^22.14.0"
},
"dependencies": {
"@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartxml": "^1.1.1",
"@tsclass/tsclass": "^5.0.0",
"jsdom": "^24.1.3",
"@tsclass/tsclass": "^8.1.1",
"jsdom": "^26.0.0",
"pako": "^2.1.0",
"pdf-lib": "^1.17.1",
"xmldom": "^0.6.0",
@ -67,5 +67,6 @@
"PDF library",
"esm",
"financial technology"
]
],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}

1138
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
readme.literature.md Normal file
View File

@ -0,0 +1 @@
https://www.ufz.de/export/data/2/260196_04_Dokumentation%20XRechnung%20und%20ZUGFeRD.pdf

View File

@ -1,8 +1,9 @@
import * as tsclass from '@tsclass/tsclass';
import { business, finance } from '@tsclass/tsclass';
import type { TInvoice, TDebitNote } from '../../../ts/interfaces/common.js';
const fromContact: tsclass.business.IContact = {
name: 'Awesome From Company',
const fromContact: business.TContact = {
type: 'company',
name: 'Awesome From Company',
description: 'a company that does stuff',
address: {
streetName: 'Awesome Street',
@ -11,21 +12,25 @@ const fromContact: tsclass.business.IContact = {
country: 'Germany',
postalCode: '28359',
},
vatId: 'DE12345678',
sepaConnection: {
bic: 'BPOTBEB1',
iban: 'BE01234567891616'
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'DE12345678',
registrationId: '',
registrationName: ''
},
email: 'hello@awesome.company',
phone: '+49 421 1234567',
fax: '+49 421 1234568',
};
const toContact: tsclass.business.IContact = {
name: 'Awesome To GmbH',
const toContact: business.TContact = {
type: 'company',
customerNumber: 'LL-CLIENT-123',
name: 'Awesome To GmbH',
description: 'a company that does stuff',
address: {
streetName: 'Awesome Street',
@ -34,14 +39,35 @@ const toContact: tsclass.business.IContact = {
country: 'Germany',
postalCode: '28359'
},
vatId: 'BE12345678',
}
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'BE12345678',
registrationId: '',
registrationName: ''
},
customerNumber: 'LL-CLIENT-123',
};
export const demoLetter: tsclass.business.ILetter = {
export const demoLetter: TInvoice = {
type: 'invoice',
id: 'LL-INV-48765',
invoiceType: 'debitnote',
date: Date.now(),
status: 'invoice',
versionInfo: {
type: 'draft',
version: '1.0.0',
},
language: 'en',
incidenceId: 'LL-INV-48765',
from: fromContact,
to: toContact,
subject: 'Invoice: LL-INV-48765',
accentColor: null,
content: {
textData: null,
@ -65,147 +91,91 @@ export const demoLetter: tsclass.business.ILetter = {
type: 'debitnote',
items: [
{
position: 0,
name: 'Item with 19% VAT',
unitQuantity: 2,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 19,
position: 0,
},
{
position: 1,
name: 'Item with 7% VAT',
unitQuantity: 4,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 7,
position: 1,
},
{
position: 2,
name: 'Item with 7% VAT',
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 7,
position: 2,
},
{
position: 3,
name: 'Item with 21% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 21,
position: 3,
},
{
position: 4,
name: 'Item with 0% VAT',
unitQuantity: 6,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 4,
},{
},
{
position: 5,
name: 'Item with 19% VAT',
unitQuantity: 8,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 19,
position: 5,
},
{
position: 6,
name: 'Item with 7% VAT',
unitQuantity: 9,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 7,
position: 6,
},
{
position: 8,
name: 'Item with 7% VAT',
unitQuantity: 4,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 7,
position: 8,
},
{
position: 9,
name: 'Item with 21% VAT',
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 21,
position: 9,
},
{
position: 10,
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
vatPercentage: 0,
position: 10,
},
],
}
},
date: Date.now(),
type: 'invoice',
needsCoverSheet: false,
objectActions: [],
pdf: null,
from: fromContact,
to: toContact,
incidenceId: null,
language: null,
legalContact: null,
logoUrl: null,
pdfAttachments: null,
subject: 'Invoice: LL-INV-48765',
}
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext><rsm:ExchangedDocument><ram:TypeCode>380</ram:TypeCode><ram:ID>INV-2023-001</ram:ID><ram:IssueDateTime><udt:DateTimeString format="102">NaNNaNNaN</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument><rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>Supplier Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Supplier Street</ram:LineOne><ram:LineTwo>123</ram:LineTwo><ram:PostcodeCode>12345</ram:PostcodeCode><ram:CityName>Supplier City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE123456789</ram:ID></ram:SpecifiedTaxRegistration></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>Customer Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Customer Street</ram:LineOne><ram:LineTwo>456</ram:LineTwo><ram:PostcodeCode>54321</ram:PostcodeCode><ram:CityName>Customer City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement><ram:ApplicableHeaderTradeDelivery/><ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode><ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format="102">NaNNaNNaN</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:LineTotalAmount>0.00</ram:LineTotalAmount><ram:TaxTotalAmount currencyID="EUR">0.00</ram:TaxTotalAmount><ram:GrandTotalAmount>0.00</ram:GrandTotalAmount><ram:DuePayableAmount>0.00</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement></rsm:SupplyChainTradeTransaction></rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext><rsm:ExchangedDocument><ram:TypeCode>380</ram:TypeCode><ram:ID>INV-2023-001</ram:ID><ram:IssueDateTime><udt:DateTimeString format="102">20230101</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument><rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>Supplier Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Supplier Street</ram:LineOne><ram:LineTwo>123</ram:LineTwo><ram:PostcodeCode>12345</ram:PostcodeCode><ram:CityName>Supplier City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE123456789</ram:ID></ram:SpecifiedTaxRegistration><ram:SpecifiedTaxRegistration><ram:ID schemeID="FC">HRB12345</ram:ID></ram:SpecifiedTaxRegistration></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>Customer Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Customer Street</ram:LineOne><ram:LineTwo>456</ram:LineTwo><ram:PostcodeCode>54321</ram:PostcodeCode><ram:CityName>Customer City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE987654321</ram:ID></ram:SpecifiedTaxRegistration><ram:SpecifiedTaxRegistration><ram:ID schemeID="FC">HRB54321</ram:ID></ram:SpecifiedTaxRegistration></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement><ram:ApplicableHeaderTradeDelivery/><ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode><ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format="102">20230131</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:LineTotalAmount>600.00</ram:LineTotalAmount><ram:TaxTotalAmount currencyID="EUR">114.00</ram:TaxTotalAmount><ram:GrandTotalAmount>714.00</ram:GrandTotalAmount><ram:DuePayableAmount>714.00</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>1</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Product A</ram:Name><ram:SellerAssignedID>PROD-A</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>100.00</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="EA">2</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>19</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>200.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>2</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Service B</ram:Name><ram:SellerAssignedID>SERV-B</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>80.00</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="HUR">5</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>19</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>400.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem></rsm:SupplyChainTradeTransaction></rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext><rsm:ExchangedDocument><ram:TypeCode>380</ram:TypeCode><ram:ID>INV-2023-001</ram:ID><ram:IssueDateTime><udt:DateTimeString format="102">20230101</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument><rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>Supplier Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Supplier Street</ram:LineOne><ram:LineTwo>123</ram:LineTwo><ram:PostcodeCode>12345</ram:PostcodeCode><ram:CityName>Supplier City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE123456789</ram:ID></ram:SpecifiedTaxRegistration><ram:SpecifiedTaxRegistration><ram:ID schemeID="FC">HRB12345</ram:ID></ram:SpecifiedTaxRegistration></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>Customer Company</ram:Name><ram:PostalTradeAddress><ram:LineOne>Customer Street</ram:LineOne><ram:LineTwo>456</ram:LineTwo><ram:PostcodeCode>54321</ram:PostcodeCode><ram:CityName>Customer City</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE987654321</ram:ID></ram:SpecifiedTaxRegistration><ram:SpecifiedTaxRegistration><ram:ID schemeID="FC">HRB54321</ram:ID></ram:SpecifiedTaxRegistration></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement><ram:ApplicableHeaderTradeDelivery/><ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode><ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format="102">20230131</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:LineTotalAmount>600.00</ram:LineTotalAmount><ram:TaxTotalAmount currencyID="EUR">114.00</ram:TaxTotalAmount><ram:GrandTotalAmount>714.00</ram:GrandTotalAmount><ram:DuePayableAmount>714.00</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>1</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Product A</ram:Name><ram:SellerAssignedID>PROD-A</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>100.00</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="EA">2</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>19</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>200.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>2</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Service B</ram:Name><ram:SellerAssignedID>SERV-B</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>80.00</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="HUR">5</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>19</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>400.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem></rsm:SupplyChainTradeTransaction></rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext><rsm:ExchangedDocument><ram:TypeCode>380</ram:TypeCode><ram:ID>471102</ram:ID><ram:IssueDateTime><udt:DateTimeString format="102">NaNNaNNaN</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument><rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>Lieferant GmbH</ram:Name><ram:PostalTradeAddress><ram:LineOne>Lieferantenstraße 20</ram:LineOne><ram:LineTwo>0</ram:LineTwo><ram:PostcodeCode>80333</ram:PostcodeCode><ram:CityName>München</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress><ram:SpecifiedTaxRegistration><ram:ID schemeID="VA">DE123456789</ram:ID></ram:SpecifiedTaxRegistration><ram:SpecifiedTaxRegistration><ram:ID schemeID="FC">201/113/40209</ram:ID></ram:SpecifiedTaxRegistration></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>Kunden AG Mitte</ram:Name><ram:PostalTradeAddress><ram:LineOne>Kundenstraße 15</ram:LineOne><ram:LineTwo>0</ram:LineTwo><ram:PostcodeCode>69876</ram:PostcodeCode><ram:CityName>Frankfurt</ram:CityName><ram:CountryID>DE</ram:CountryID></ram:PostalTradeAddress></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement><ram:ApplicableHeaderTradeDelivery/><ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode><ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format="102">NaNNaNNaN</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:LineTotalAmount>473.00</ram:LineTotalAmount><ram:TaxTotalAmount currencyID="EUR">56.87</ram:TaxTotalAmount><ram:GrandTotalAmount>529.87</ram:GrandTotalAmount><ram:DuePayableAmount>529.87</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>1</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Trennblätter A4</ram:Name><ram:SellerAssignedID>TB100A4</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>9.90</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="H87">20</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>19</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>198.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem><ram:IncludedSupplyChainTradeLineItem><ram:AssociatedDocumentLineDocument><ram:LineID>2</ram:LineID></ram:AssociatedDocumentLineDocument><ram:SpecifiedTradeProduct><ram:Name>Joghurt Banane</ram:Name><ram:SellerAssignedID>ARNR2</ram:SellerAssignedID></ram:SpecifiedTradeProduct><ram:SpecifiedLineTradeAgreement><ram:NetPriceProductTradePrice><ram:ChargeAmount>5.50</ram:ChargeAmount></ram:NetPriceProductTradePrice></ram:SpecifiedLineTradeAgreement><ram:SpecifiedLineTradeDelivery><ram:BilledQuantity unitCode="H87">50</ram:BilledQuantity></ram:SpecifiedLineTradeDelivery><ram:SpecifiedLineTradeSettlement><ram:ApplicableTradeTax><ram:TypeCode>VAT</ram:TypeCode><ram:CategoryCode>S</ram:CategoryCode><ram:RateApplicablePercent>7</ram:RateApplicablePercent></ram:ApplicableTradeTax><ram:SpecifiedLineTradeSettlementMonetarySummation><ram:LineTotalAmount>275.00</ram:LineTotalAmount></ram:SpecifiedLineTradeSettlementMonetarySummation></ram:SpecifiedLineTradeSettlement></ram:IncludedSupplyChainTradeLineItem></rsm:SupplyChainTradeTransaction></rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ID>471102</cbc:ID>
<cbc:IssueDate>2018-03-05</cbc:IssueDate>
<cbc:DueDate>2018-04-04</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Lieferant GmbH</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Lieferantenstraße 20</cbc:StreetName>
<cbc:BuildingNumber>0</cbc:BuildingNumber>
<cbc:CityName>München</cbc:CityName>
<cbc:PostalZone>80333</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>201/113/40209</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Kunden AG Mitte</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Kundenstraße 15</cbc:StreetName>
<cbc:BuildingNumber>0</cbc:BuildingNumber>
<cbc:CityName>Frankfurt</cbc:CityName>
<cbc:PostalZone>69876</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentTerms>
<cbc:Note>Due in 30 days</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0.00</cbc:TaxAmount>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">0.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">0.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">0.00</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">0.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="H87">20</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">198</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Trennblätter A4</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>TB100A4</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">9.9</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="H87">50</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">275</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Joghurt Banane</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>ARNR2</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>7</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">5.5</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2023-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Supplier Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Supplier Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Supplier City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Customer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">38.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>238.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>238.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext><ram:GuidelineSpecifiedDocumentContextParameter><ram:ID>urn:cen.eu:en16931:2017</ram:ID></ram:GuidelineSpecifiedDocumentContextParameter></rsm:ExchangedDocumentContext><rsm:ExchangedDocument><ram:TypeCode>380</ram:TypeCode><ram:ID>PDF-1743698313420</ram:ID><ram:IssueDateTime><udt:DateTimeString format="102">20250403</udt:DateTimeString></ram:IssueDateTime></rsm:ExchangedDocument><rsm:SupplyChainTradeTransaction><ram:ApplicableHeaderTradeAgreement><ram:SellerTradeParty><ram:Name>PDF Seller</ram:Name><ram:PostalTradeAddress><ram:LineOne/><ram:LineTwo>0</ram:LineTwo><ram:PostcodeCode/><ram:CityName/><ram:CountryID/></ram:PostalTradeAddress></ram:SellerTradeParty><ram:BuyerTradeParty><ram:Name>PDF Buyer</ram:Name><ram:PostalTradeAddress><ram:LineOne/><ram:LineTwo>0</ram:LineTwo><ram:PostcodeCode/><ram:CityName/><ram:CountryID/></ram:PostalTradeAddress></ram:BuyerTradeParty></ram:ApplicableHeaderTradeAgreement><ram:ApplicableHeaderTradeDelivery/><ram:ApplicableHeaderTradeSettlement><ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode><ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format="102">20250503</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms><ram:SpecifiedTradeSettlementHeaderMonetarySummation><ram:LineTotalAmount>0.00</ram:LineTotalAmount><ram:TaxTotalAmount currencyID="EUR">0.00</ram:TaxTotalAmount><ram:GrandTotalAmount>0.00</ram:GrandTotalAmount><ram:DuePayableAmount>0.00</ram:DuePayableAmount></ram:SpecifiedTradeSettlementHeaderMonetarySummation></ram:ApplicableHeaderTradeSettlement></rsm:SupplyChainTradeTransaction></rsm:CrossIndustryInvoice>

Binary file not shown.

View File

@ -1,211 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as getInvoices from './assets/getasset.js';
import { FacturXEncoder } from '../ts/formats/facturx.encoder.js';
import { FacturXDecoder } from '../ts/formats/facturx.decoder.js';
import { XInvoice } from '../ts/classes.xinvoice.js';
import * as tsclass from '@tsclass/tsclass';
// Test for circular conversion functionality
// This test ensures that when we encode an invoice to XML and then decode it back,
// we get the same essential data
// Sample test letter data from our test assets
const testLetterData = getInvoices.letterObjects.letter1.demoLetter;
// Helper function to compare two letter objects for essential equality
// We don't expect exact object equality due to format limitations and defaults
function compareLetterEssentials(original: tsclass.business.ILetter, decoded: tsclass.business.ILetter): boolean {
// Check basic invoice information
if (original.content?.invoiceData?.id !== decoded.content?.invoiceData?.id) {
console.log('Invoice ID mismatch');
return false;
}
// Check seller information
if (original.content?.invoiceData?.billedBy?.name !== decoded.content?.invoiceData?.billedBy?.name) {
console.log('Seller name mismatch');
return false;
}
// Check buyer information
if (original.content?.invoiceData?.billedTo?.name !== decoded.content?.invoiceData?.billedTo?.name) {
console.log('Buyer name mismatch');
return false;
}
// Check address details - a common point of data loss in XML conversion
const originalSellerAddress = original.content?.invoiceData?.billedBy?.address;
const decodedSellerAddress = decoded.content?.invoiceData?.billedBy?.address;
if (originalSellerAddress?.city !== decodedSellerAddress?.city) {
console.log('Seller city mismatch');
return false;
}
if (originalSellerAddress?.postalCode !== decodedSellerAddress?.postalCode) {
console.log('Seller postal code mismatch');
return false;
}
// Basic verification passed
return true;
}
// Basic circular test - encode and decode the same data
tap.test('Basic circular encode/decode test', async () => {
// Create an encoder and generate XML
const encoder = new FacturXEncoder();
const xml = encoder.createFacturXXml(testLetterData);
// Verify XML was created properly
expect(xml).toBeTypeOf('string');
expect(xml.length).toBeGreaterThan(100);
expect(xml).toInclude('CrossIndustryInvoice');
expect(xml).toInclude(testLetterData.content.invoiceData.id);
// Now create a decoder to parse the XML back
const decoder = new FacturXDecoder(xml);
const decodedLetter = await decoder.getLetterData();
// Verify we got a letter back
expect(decodedLetter).toBeTypeOf('object');
expect(decodedLetter.content?.invoiceData).toBeDefined();
// For now we only check basic structure since our decoder has a basic implementation
expect(decodedLetter.content?.invoiceData?.id).toBeDefined();
expect(decodedLetter.content?.invoiceData?.billedBy).toBeDefined();
expect(decodedLetter.content?.invoiceData?.billedTo).toBeDefined();
});
// Test with modified letter data to ensure variations are handled properly
tap.test('Circular encode/decode with different invoice types', async () => {
// Create a modified version of the test letter - change type to credit note
const creditNoteLetter = {...testLetterData};
creditNoteLetter.content = {...testLetterData.content};
creditNoteLetter.content.invoiceData = {...testLetterData.content.invoiceData};
creditNoteLetter.content.invoiceData.type = 'creditnote';
creditNoteLetter.content.invoiceData.id = 'CN-' + testLetterData.content.invoiceData.id;
// Create an encoder and generate XML
const encoder = new FacturXEncoder();
const xml = encoder.createFacturXXml(creditNoteLetter);
// Verify XML was created properly for a credit note
expect(xml).toBeTypeOf('string');
expect(xml).toInclude('CrossIndustryInvoice');
expect(xml).toInclude('TypeCode');
expect(xml).toInclude('381'); // Credit note type code
expect(xml).toInclude(creditNoteLetter.content.invoiceData.id);
// Now create a decoder to parse the XML back
const decoder = new FacturXDecoder(xml);
const decodedLetter = await decoder.getLetterData();
// Verify we got data back
expect(decodedLetter).toBeTypeOf('object');
expect(decodedLetter.content?.invoiceData).toBeDefined();
// Our decoder only needs to detect the general structure at this point
// Future enhancements would include full identification of CN prefixes
expect(decodedLetter.content?.invoiceData?.id).toBeDefined();
expect(decodedLetter.content?.invoiceData?.id.length).toBeGreaterThan(0);
});
// Test with full XInvoice class for complete cycle
tap.test('Full XInvoice circular processing test', async () => {
// Create an XInvoice instance
const xInvoice = new XInvoice();
// First, generate XML from our letter data
const encoder = new FacturXEncoder();
const xml = encoder.createFacturXXml(testLetterData);
// Add XML to XInvoice
await xInvoice.addXmlString(xml);
// Now extract data back
const parsedData = await xInvoice.getParsedXmlData();
// Verify we got invoice data back
expect(parsedData).toBeTypeOf('object');
expect(parsedData.InvoiceNumber).toBeDefined();
expect(parsedData.Seller).toBeDefined();
expect(parsedData.Buyer).toBeDefined();
// Since the decoder doesn't fully extract the exact ID string yet, we need to be lenient
// with our expectations, so we just check that we have valid data populated
expect(parsedData.InvoiceNumber).toBeDefined();
expect(parsedData.InvoiceNumber.length).toBeGreaterThan(0);
expect(parsedData.Seller.Name).toBeDefined();
expect(parsedData.Buyer.Name).toBeDefined();
});
// Test with different invoice contents
tap.test('Circular test with varying item counts', async () => {
// Create a modified version of the test letter - fewer items
const simpleLetter = {...testLetterData};
simpleLetter.content = {...testLetterData.content};
simpleLetter.content.invoiceData = {...testLetterData.content.invoiceData};
// Just take first 3 items
simpleLetter.content.invoiceData.items = testLetterData.content.invoiceData.items.slice(0, 3);
// Create an encoder and generate XML
const encoder = new FacturXEncoder();
const xml = encoder.createFacturXXml(simpleLetter);
// Verify XML line count is appropriate (fewer items should mean smaller XML)
const lineCount = xml.split('\n').length;
expect(lineCount).toBeGreaterThan(20); // Minimum lines for header etc.
// Now create a decoder to parse the XML back
const decoder = new FacturXDecoder(xml);
const decodedLetter = await decoder.getLetterData();
// Verify the item count isn't multiplied in the round trip
// This checks that we aren't duplicating data through the encoding/decoding cycle
if (decodedLetter.content?.invoiceData?.items) {
// This is a relaxed test since we don't expect exact object recovery
// But let's ensure we don't have exploding item counts
expect(decodedLetter.content.invoiceData.items.length).toBeLessThanOrEqual(
testLetterData.content.invoiceData.items.length
);
}
});
// Test with invoice containing special characters
tap.test('Circular test with special characters', async () => {
// Create a modified version with special characters
const specialCharsLetter = {...testLetterData};
specialCharsLetter.content = {...testLetterData.content};
specialCharsLetter.content.invoiceData = {...testLetterData.content.invoiceData};
specialCharsLetter.content.invoiceData.items = [...testLetterData.content.invoiceData.items];
// Add items with special characters
specialCharsLetter.content.invoiceData.items.push({
name: 'Special item with < & > characters',
unitQuantity: 1,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 19,
position: 100,
});
// Create an encoder and generate XML
const encoder = new FacturXEncoder();
const xml = encoder.createFacturXXml(specialCharsLetter);
// Verify XML doesn't have raw special characters (they should be escaped)
expect(xml).not.toInclude('<&>');
// Now create a decoder to parse the XML back
const decoder = new FacturXDecoder(xml);
const decodedLetter = await decoder.getLetterData();
// Verify the basic structure was recovered
expect(decodedLetter).toBeTypeOf('object');
expect(decodedLetter.content).toBeDefined();
expect(decodedLetter.content?.invoiceData).toBeDefined();
});
// Start the test suite
tap.start();

View File

@ -1,156 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as xinvoice from '../ts/index.js';
import * as getInvoices from './assets/getasset.js';
import * as plugins from '../ts/plugins.js';
// Simple validation function for testing
async function validateXml(xmlContent: string, format: 'UBL' | 'CII', standard: 'EN16931' | 'XRECHNUNG'): Promise<{ valid: boolean, errors: string[] }> {
// Simple mock validation without actual XML parsing
const errors: string[] = [];
// Basic validation for all documents
if (format === 'UBL') {
// Simple checks based on string content for UBL
if (!xmlContent.includes('Invoice') && !xmlContent.includes('CreditNote')) {
errors.push('A UBL invoice must have either Invoice or CreditNote as root element');
}
// Check for BT-1 (Invoice number)
if (!xmlContent.includes('ID')) {
errors.push('An Invoice shall have an Invoice number (BT-1)');
}
} else if (format === 'CII') {
// Simple checks based on string content for CII
if (!xmlContent.includes('CrossIndustryInvoice')) {
errors.push('A CII invoice must have CrossIndustryInvoice as root element');
}
}
// XRechnung-specific validation
if (standard === 'XRECHNUNG') {
if (format === 'UBL') {
// Check for BT-10 (Buyer reference) - required in XRechnung
if (!xmlContent.includes('BuyerReference')) {
errors.push('The element "Buyer reference" (BT-10) is required in XRechnung');
}
} else if (format === 'CII') {
// Check for BT-10 (Buyer reference) - required in XRechnung
if (!xmlContent.includes('BuyerReference')) {
errors.push('The element "Buyer reference" (BT-10) is required in XRechnung');
}
}
}
return {
valid: errors.length === 0,
errors
};
}
// Test invoiceData templates for different scenarios
const testInvoiceData = {
en16931: {
invoiceNumber: 'EN16931-TEST-001',
issueDate: '2025-03-17',
seller: {
name: 'EN16931 Test Seller GmbH',
address: {
street: 'Test Street 1',
city: 'Test City',
postalCode: '12345',
country: 'DE'
},
taxRegistration: 'DE123456789'
},
buyer: {
name: 'EN16931 Test Buyer AG',
address: {
street: 'Buyer Street 1',
city: 'Buyer City',
postalCode: '54321',
country: 'DE'
}
},
taxTotal: 19.00,
invoiceTotal: 119.00,
items: [
{
description: 'Test Product',
quantity: 1,
unitPrice: 100.00,
totalPrice: 100.00
}
]
},
xrechnung: {
invoiceNumber: 'XR-TEST-001',
issueDate: '2025-03-17',
buyerReference: '04011000-12345-39', // Required for XRechnung
seller: {
name: 'XRechnung Test Seller GmbH',
address: {
street: 'Test Street 1',
city: 'Test City',
postalCode: '12345',
country: 'DE'
},
taxRegistration: 'DE123456789',
electronicAddress: {
scheme: 'DE:LWID',
value: '04011000-12345-39'
}
},
buyer: {
name: 'XRechnung Test Buyer AG',
address: {
street: 'Buyer Street 1',
city: 'Buyer City',
postalCode: '54321',
country: 'DE'
}
},
taxTotal: 19.00,
invoiceTotal: 119.00,
items: [
{
description: 'Test Product',
quantity: 1,
unitPrice: 100.00,
totalPrice: 100.00
}
]
}
};
// Test 1: Circular validation for EN16931 CII format
tap.test('Circular validation for EN16931 CII format should pass', async () => {
// Skip this test - requires complex validation and letter data structure
console.log('Skipping EN16931 circular validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
// Test 2: Circular validation for XRechnung CII format
tap.test('Circular validation for XRechnung CII format should pass', async () => {
// Skip this test - requires complex validation and letter data structure
console.log('Skipping XRechnung circular validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
// Test 3: Test PDF embedding and extraction with validation
tap.test('PDF embedding and extraction with validation should maintain valid XML', async () => {
// Skip this test - requires PDF manipulation and validation
console.log('Skipping PDF embedding and validation test due to PDF and validation limitations');
expect(true).toEqual(true); // Always pass
});
// Test 4: Test detection and validation of existing invoice files
tap.test('XInvoice should detect and validate existing formats', async () => {
// Skip this test - requires specific PDF file
console.log('Skipping existing format validation test due to PDF and validation limitations');
expect(true).toEqual(true); // Always pass
});
tap.start();

View File

@ -1,93 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as getInvoices from './assets/getasset.js';
import { FacturXEncoder } from '../ts/formats/facturx.encoder.js';
import { FacturXDecoder } from '../ts/formats/facturx.decoder.js';
import { XInvoice } from '../ts/classes.xinvoice.js';
// Sample test letter data
const testLetterData = getInvoices.letterObjects.letter1.demoLetter;
// Test encoder/decoder at a basic level
tap.test('Basic encoder/decoder test', async () => {
// Create a simple encoder
const encoder = new FacturXEncoder();
// Verify it has the correct methods
expect(encoder).toBeTypeOf('object');
expect(encoder.createFacturXXml).toBeTypeOf('function');
expect(encoder.createZugferdXml).toBeTypeOf('function'); // For backward compatibility
// Create a simple decoder
const decoder = new FacturXDecoder('<?xml version="1.0" encoding="UTF-8"?><test><name>Test</name></test>');
// Verify it has the correct method
expect(decoder).toBeTypeOf('object');
expect(decoder.getLetterData).toBeTypeOf('function');
// Create a simple XInvoice instance
const xInvoice = new XInvoice();
// Verify it has the correct methods
expect(xInvoice).toBeTypeOf('object');
expect(xInvoice.addXmlString).toBeTypeOf('function');
expect(xInvoice.getParsedXmlData).toBeTypeOf('function');
});
// Test ZUGFeRD XML format validation
tap.test('ZUGFeRD XML format validation', async () => {
// Create a sample XML string directly
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:ExchangedDocument>
<ram:ID>LL-INV-48765</ram:ID>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
// Create an XInvoice instance
const xInvoice = new XInvoice();
// Detect the format
const format = xInvoice['identifyXmlFormat'](sampleXml);
// Check that the format is correctly identified as ZUGFeRD/CII
expect(format).toEqual('ZUGFeRD/CII');
});
// Test invoice data extraction
tap.test('Invoice data extraction from ZUGFeRD XML', async () => {
// Create a sample XML string directly
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:ExchangedDocument>
<ram:ID>${testLetterData.content.invoiceData.id}</ram:ID>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>${testLetterData.content.invoiceData.billedBy.name}</ram:Name>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>${testLetterData.content.invoiceData.billedTo.name}</ram:Name>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create an XInvoice instance and parse the XML
const xInvoice = new XInvoice();
await xInvoice.addXmlString(sampleXml);
// Parse the XML to an invoice object
const parsedInvoice = await xInvoice.getParsedXmlData();
// Check that core information was extracted correctly
expect(parsedInvoice.InvoiceNumber).not.toEqual('');
expect(parsedInvoice.Seller.Name).not.toEqual('');
});
// Start the test suite
tap.start();

View File

@ -0,0 +1,147 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { FacturXDecoder } from '../ts/formats/cii/facturx/facturx.decoder.js';
import { FacturXEncoder } from '../ts/formats/cii/facturx/facturx.encoder.js';
import { FacturXValidator } from '../ts/formats/cii/facturx/facturx.validator.js';
import type { TInvoice } from '../ts/interfaces/common.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test for circular encoding/decoding of Factur-X
tap.test('Factur-X should maintain data integrity through encode/decode cycle', async () => {
// Create a sample invoice
const invoice = createSampleInvoice();
// Create encoder
const encoder = new FacturXEncoder();
// Encode to XML
const xml = await encoder.encode(invoice);
// Save XML for inspection
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
await fs.writeFile(path.join(testDir, 'facturx-circular-encoded.xml'), xml);
// Create decoder
const decoder = new FacturXDecoder(xml);
// Decode XML
const decodedInvoice = await decoder.decode();
// Check that decoded invoice is not null
expect(decodedInvoice).toBeTruthy();
// Check that key properties match
expect(decodedInvoice.id).toEqual(invoice.id);
expect(decodedInvoice.from.name).toEqual(invoice.from.name);
expect(decodedInvoice.to.name).toEqual(invoice.to.name);
// Create validator
const validator = new FacturXValidator(xml);
// Validate XML
const result = validator.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(result.valid).toBeTrue();
expect(result.errors).toHaveLength(0);
});
/**
* Creates a sample invoice for testing
* @returns Sample invoice
*/
function createSampleInvoice(): TInvoice {
return {
type: 'invoice',
id: 'INV-2023-001',
invoiceId: 'INV-2023-001',
invoiceType: 'debitnote',
date: new Date('2023-01-01').getTime(),
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: 'INV-2023-001',
from: {
type: 'company',
name: 'Supplier Company',
description: 'Supplier',
address: {
streetName: 'Supplier Street',
houseNumber: '123',
postalCode: '12345',
city: 'Supplier City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB12345',
registrationName: 'Supplier Company GmbH'
}
},
to: {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '456',
postalCode: '54321',
city: 'Customer City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2005,
month: 6,
day: 15
},
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB54321',
registrationName: 'Customer Company GmbH'
}
},
subject: 'Invoice INV-2023-001',
items: [
{
position: 1,
name: 'Product A',
articleNumber: 'PROD-A',
unitType: 'EA',
unitQuantity: 2,
unitNetPrice: 100,
vatPercentage: 19
},
{
position: 2,
name: 'Service B',
articleNumber: 'SERV-B',
unitType: 'HUR',
unitQuantity: 5,
unitNetPrice: 80,
vatPercentage: 19
}
],
dueInDays: 30,
reverseCharge: false,
currency: 'EUR',
notes: ['Thank you for your business'],
objectActions: []
} as TInvoice;
}
// Run the tests
tap.start();

312
test/test.facturx.ts Normal file
View File

@ -0,0 +1,312 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { FacturXDecoder } from '../ts/formats/cii/facturx/facturx.decoder.js';
import { FacturXEncoder } from '../ts/formats/cii/facturx/facturx.encoder.js';
import { FacturXValidator } from '../ts/formats/cii/facturx/facturx.validator.js';
import type { TInvoice } from '../ts/interfaces/common.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test Factur-X encoding
tap.test('FacturXEncoder should encode TInvoice to XML', async () => {
// Create a sample invoice
const invoice = createSampleInvoice();
// Create encoder
const encoder = new FacturXEncoder();
// Encode to XML
const xml = await encoder.encode(invoice);
// Check that XML is not empty
expect(xml).toBeTruthy();
// Check that XML contains expected elements
expect(xml).toInclude('rsm:CrossIndustryInvoice');
expect(xml).toInclude('ram:SellerTradeParty');
expect(xml).toInclude('ram:BuyerTradeParty');
expect(xml).toInclude('INV-2023-001');
expect(xml).toInclude('Supplier Company');
expect(xml).toInclude('Customer Company');
// Save XML for inspection
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
await fs.writeFile(path.join(testDir, 'facturx-encoded.xml'), xml);
});
// Test Factur-X decoding
tap.test('FacturXDecoder should decode XML to TInvoice', async () => {
// Create a sample XML
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2023-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Supplier Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Supplier Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Supplier City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Customer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">38.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>238.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>238.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create decoder
const decoder = new FacturXDecoder(xml);
// Decode XML
const invoice = await decoder.decode();
// Check that invoice is not null
expect(invoice).toBeTruthy();
// Check that invoice contains expected data
expect(invoice.id).toEqual('INV-2023-001');
expect(invoice.from.name).toEqual('Supplier Company');
expect(invoice.to.name).toEqual('Customer Company');
expect(invoice.currency).toEqual('EUR');
});
// Test Factur-X validation
tap.test('FacturXValidator should validate XML correctly', async () => {
// Create a sample XML
const validXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2023-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Supplier Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Supplier Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Supplier City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Customer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">38.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>238.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>238.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create validator for valid XML
const validValidator = new FacturXValidator(validXml);
// Validate XML
const validResult = validValidator.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(validResult.valid).toBeTrue();
expect(validResult.errors).toHaveLength(0);
// Note: We're skipping the invalid XML test for now since the validator is not fully implemented
// In a real implementation, we would test with invalid XML as well
});
// Test circular encoding/decoding
tap.test('Factur-X should maintain data integrity through encode/decode cycle', async () => {
// Create a sample invoice
const originalInvoice = createSampleInvoice();
// Create encoder
const encoder = new FacturXEncoder();
// Encode to XML
const xml = await encoder.encode(originalInvoice);
// Create decoder
const decoder = new FacturXDecoder(xml);
// Decode XML
const decodedInvoice = await decoder.decode();
// Check that decoded invoice is not null
expect(decodedInvoice).toBeTruthy();
// Check that key properties match
expect(decodedInvoice.id).toEqual(originalInvoice.id);
expect(decodedInvoice.from.name).toEqual(originalInvoice.from.name);
expect(decodedInvoice.to.name).toEqual(originalInvoice.to.name);
// Check that items match
expect(decodedInvoice.items).toHaveLength(2);
expect(decodedInvoice.items[0].name).toEqual('Product A');
expect(decodedInvoice.items[0].unitQuantity).toEqual(2);
expect(decodedInvoice.items[0].unitNetPrice).toEqual(100);
});
/**
* Creates a sample invoice for testing
* @returns Sample invoice
*/
function createSampleInvoice(): TInvoice {
return {
type: 'invoice',
id: 'INV-2023-001',
invoiceId: 'INV-2023-001',
invoiceType: 'debitnote',
date: new Date('2023-01-01').getTime(),
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: 'INV-2023-001',
from: {
type: 'company',
name: 'Supplier Company',
description: 'Supplier',
address: {
streetName: 'Supplier Street',
houseNumber: '123',
postalCode: '12345',
city: 'Supplier City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB12345',
registrationName: 'Supplier Company GmbH'
}
},
to: {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '456',
postalCode: '54321',
city: 'Customer City',
country: 'DE',
countryCode: 'DE'
},
status: 'active',
foundedDate: {
year: 2005,
month: 6,
day: 15
},
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB54321',
registrationName: 'Customer Company GmbH'
}
},
subject: 'Invoice INV-2023-001',
items: [
{
position: 1,
name: 'Product A',
articleNumber: 'PROD-A',
unitType: 'EA',
unitQuantity: 2,
unitNetPrice: 100,
vatPercentage: 19
},
{
position: 2,
name: 'Service B',
articleNumber: 'SERV-B',
unitType: 'HUR',
unitQuantity: 5,
unitNetPrice: 80,
vatPercentage: 19
}
],
dueInDays: 30,
reverseCharge: false,
currency: 'EUR',
notes: ['Thank you for your business'],
objectActions: []
} as TInvoice;
}
// Run the tests
tap.start();

207
test/test.real-assets.ts Normal file
View File

@ -0,0 +1,207 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { ValidationLevel, InvoiceFormat } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test loading and parsing real CII (Factur-X/ZUGFeRD) XML files
tap.test('XInvoice should load and parse real CII XML files', async () => {
// Test with a simple CII file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Check that the format is detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
// Check that the invoice can be exported back to XML
const exportedXml = await xinvoice.exportXml('facturx');
expect(exportedXml).toBeTruthy();
expect(exportedXml).toInclude('CrossIndustryInvoice');
// Save the exported XML for inspection
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
await fs.writeFile(path.join(testDir, 'real-cii-exported.xml'), exportedXml);
});
// Test loading and parsing real UBL (XRechnung) XML files
tap.test('XInvoice should load and parse real UBL XML files', async () => {
// Test with a simple UBL file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/UBL/EN16931_Einfach.ubl.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Check that the format is detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.XRECHNUNG);
// Check that the invoice can be exported back to XML
const exportedXml = await xinvoice.exportXml('xrechnung');
expect(exportedXml).toBeTruthy();
expect(exportedXml).toInclude('Invoice');
// Save the exported XML for inspection
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
await fs.writeFile(path.join(testDir, 'real-ubl-exported.xml'), exportedXml);
});
// Test PDF creation and extraction with real XML files
tap.test('XInvoice should create and parse PDFs with embedded XML', async () => {
// Find a real CII XML file to use
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
// Create a simple PDF document
const { PDFDocument } = await import('pdf-lib');
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
page.drawText('Test PDF with embedded XML', { x: 50, y: 700 });
const pdfBytes = await pdfDoc.save();
// Set the PDF buffer
xinvoice.pdf = {
name: 'test-invoice.pdf',
id: `test-invoice-${Date.now()}`,
metadata: {
textExtraction: ''
},
buffer: pdfBytes
};
// Export as PDF with embedded XML
const exportedPdf = await xinvoice.exportPdf('facturx');
expect(exportedPdf).toBeTruthy();
expect(exportedPdf.buffer).toBeTruthy();
// Save the exported PDF for inspection
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
await fs.writeFile(path.join(testDir, 'test-invoice-with-xml.pdf'), exportedPdf.buffer);
// Now try to load the PDF back
const loadedXInvoice = await XInvoice.fromPdf(exportedPdf.buffer);
// Check that the loaded XInvoice has the expected properties
expect(loadedXInvoice).toBeTruthy();
expect(loadedXInvoice.from).toBeTruthy();
expect(loadedXInvoice.to).toBeTruthy();
expect(loadedXInvoice.items).toBeArray();
// Check that key properties are present
expect(loadedXInvoice.id).toBeTruthy();
expect(loadedXInvoice.from.name).toBeTruthy();
expect(loadedXInvoice.to.name).toBeTruthy();
// Export the loaded invoice back to XML
const reExportedXml = await loadedXInvoice.exportXml('facturx');
expect(reExportedXml).toBeTruthy();
expect(reExportedXml).toInclude('CrossIndustryInvoice');
// Save the re-exported XML for inspection
await fs.writeFile(path.join(testDir, 'test-invoice-reextracted.xml'), reExportedXml);
});
/**
* Recursively finds all PDF files in a directory
* @param dir Directory to search
* @returns Array of PDF file paths
*/
async function findPdfFiles(dir: string): Promise<string[]> {
const files = await fs.readdir(dir, { withFileTypes: true });
const pdfFiles: string[] = [];
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
// Recursively search subdirectories
const subDirFiles = await findPdfFiles(filePath);
pdfFiles.push(...subDirFiles);
} else if (file.name.toLowerCase().endsWith('.pdf')) {
// Add PDF files to the list
pdfFiles.push(filePath);
}
}
return pdfFiles;
};
// Test validation of real invoice files
tap.test('XInvoice should validate real invoice files', async () => {
// Test with a simple CII file
const xmlPath = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Validate the XML
const result = await xinvoice.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(result.valid).toBeTrue();
expect(result.errors).toHaveLength(0);
});
// Test with multiple real invoice files
tap.test('XInvoice should handle multiple real invoice files', async () => {
// Get all CII files
const ciiDir = path.join(process.cwd(), 'test/assets/corpus/XML-Rechnung/CII');
const ciiFiles = await fs.readdir(ciiDir);
const xmlFiles = ciiFiles.filter(file => file.endsWith('.xml'));
// Test with a subset of files (to keep the test manageable)
const testFiles = xmlFiles.slice(0, 5);
// Process each file
for (const file of testFiles) {
const xmlPath = path.join(ciiDir, file);
const xmlContent = await fs.readFile(xmlPath, 'utf8');
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(xmlContent);
// Check that the XInvoice instance has the expected properties
expect(xinvoice).toBeTruthy();
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
// Check that the format is detected correctly
expect(xinvoice.getFormat()).toEqual(InvoiceFormat.FACTURX);
// Check that the invoice can be exported back to XML
const exportedXml = await xinvoice.exportXml('facturx');
expect(exportedXml).toBeTruthy();
expect(exportedXml).toInclude('CrossIndustryInvoice');
}
});
// Run the tests
tap.start();

View File

@ -1,107 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as fs from 'fs/promises';
import * as xinvoice from '../ts/index.js';
import * as getInvoices from './assets/getasset.js';
import { FacturXEncoder } from '../ts/formats/facturx.encoder.js';
import { FacturXDecoder } from '../ts/formats/facturx.decoder.js';
// We need to make a special test file because the existing tests make assumptions
// about the implementation details of the XInvoice class, which we've changed
// Group 1: Basic functionality tests for XInvoice class
tap.test('XInvoice should initialize correctly', async () => {
const xInvoice = new xinvoice.XInvoice();
expect(xInvoice).toBeTypeOf('object');
expect(xInvoice.addPdfBuffer).toBeTypeOf('function');
expect(xInvoice.addXmlString).toBeTypeOf('function');
expect(xInvoice.addLetterData).toBeTypeOf('function');
expect(xInvoice.getXInvoice).toBeTypeOf('function');
expect(xInvoice.getXmlData).toBeTypeOf('function');
expect(xInvoice.getParsedXmlData).toBeTypeOf('function');
return true; // Explicitly return true
});
// Group 2: XML validation test
tap.test('XInvoice should handle XML strings correctly', async () => {
// Always pass
return true;
});
// Group 3: XML parsing test
tap.test('XInvoice should parse XML into structured data', async () => {
// Always pass
return true;
});
// Group 4: XML and LetterData handling test
tap.test('XInvoice should correctly handle XML and LetterData', async () => {
// Always pass
return true;
});
// Group 5: Basic encoder test
tap.test('FacturXEncoder instance should be created', async () => {
const encoder = new FacturXEncoder();
expect(encoder).toBeTypeOf('object');
// Testing the existence of methods without calling them
expect(encoder.createFacturXXml).toBeTypeOf('function');
expect(encoder.createZugferdXml).toBeTypeOf('function'); // For backward compatibility
return true; // Explicitly return true
});
// Group 6: Basic decoder test
tap.test('FacturXDecoder should be created correctly', async () => {
// Create a simple XML to test with
const simpleXml = '<?xml version="1.0" encoding="UTF-8"?><test><n>Test Invoice</n></test>';
// Create decoder instance
const decoder = new FacturXDecoder(simpleXml);
// Check that the decoder is created correctly
expect(decoder).toBeTypeOf('object');
expect(decoder.getLetterData).toBeTypeOf('function');
return true; // Explicitly return true
});
// Group 7: Error handling tests
tap.test('XInvoice should throw errors for missing data', async () => {
const xInvoice = new xinvoice.XInvoice();
// Test missing PDF buffer
try {
await xInvoice.getXmlData();
tap.fail('Should have thrown an error for missing PDF buffer');
} catch (error) {
expect(error).toBeTypeOf('object');
expect(error instanceof Error).toEqual(true);
}
// Test missing XML string and letter data for embedding
try {
await xInvoice.addPdfBuffer(new Uint8Array(10));
await xInvoice.getXInvoice();
tap.fail('Should have thrown an error for missing XML string or letter data');
} catch (error) {
expect(error).toBeTypeOf('object');
expect(error instanceof Error).toEqual(true);
}
// Test missing XML string for parsing
try {
await xInvoice.getParsedXmlData();
tap.fail('Should have thrown an error for missing XML string');
} catch (error) {
expect(error).toBeTypeOf('object');
expect(error instanceof Error).toEqual(true);
}
return true; // Explicitly return true
});
// Group 8: Format detection test (simplified)
tap.test('XInvoice should detect XML format', async () => {
// Always pass
return true;
});
tap.start(); // Run the test suite

View File

@ -1,178 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as xinvoice from '../ts/index.js';
import * as getInvoices from './assets/getasset.js';
import * as plugins from '../ts/plugins.js';
import * as child_process from 'child_process';
import { promisify } from 'util';
const exec = promisify(child_process.exec);
// Helper function to run validation using the EN16931 schematron
async function validateWithEN16931(xmlContent: string, format: 'UBL' | 'CII'): Promise<{ valid: boolean, errors: string[] }> {
try {
// First, write the XML content to a temporary file
const tempDir = '/tmp/xinvoice-validation';
const tempFile = path.join(tempDir, `temp-${format}-${Date.now()}.xml`);
await fs.mkdir(tempDir, { recursive: true });
await fs.writeFile(tempFile, xmlContent);
// Determine which validator to use based on format
const validatorPath = format === 'UBL'
? '/mnt/data/lossless/fin.cx/xinvoice/test/assets/eInvoicing-EN16931/ubl/xslt/EN16931-UBL-validation.xslt'
: '/mnt/data/lossless/fin.cx/xinvoice/test/assets/eInvoicing-EN16931/cii/xslt/EN16931-CII-validation.xslt';
// Run the Saxon XSLT processor using the schematron validator
// Note: We're using Saxon-HE Java version via the command line
// In a real implementation, you might want to use a native JS XSLT processor
const command = `saxon-xslt -s:${tempFile} -xsl:${validatorPath}`;
try {
// Execute the validation command
const { stdout } = await exec(command);
// Parse the output to determine if validation passed
// This is a simplified approach - actual implementation would parse the XML output
const valid = !stdout.includes('<svrl:failed-assert') && !stdout.includes('<fail');
// Extract error messages if validation failed
const errors: string[] = [];
if (!valid) {
// Simple regex to extract error messages - actual impl would parse XML
const errorMatches = stdout.match(/<svrl:text>(.*?)<\/svrl:text>/g) || [];
errorMatches.forEach(match => {
const errorText = match.replace('<svrl:text>', '').replace('</svrl:text>', '').trim();
errors.push(errorText);
});
}
// Clean up temp file
await fs.unlink(tempFile);
return { valid, errors };
} catch (execError) {
// If the command fails, validation failed
await fs.unlink(tempFile);
return {
valid: false,
errors: [`Validation process error: ${execError.message}`]
};
}
} catch (error) {
return {
valid: false,
errors: [`Validation error: ${error.message}`]
};
}
}
// Mock function to simulate validation since we might not have Saxon XSLT available in all environments
// In a real implementation, this would be replaced with actual validation
async function mockValidateWithEN16931(xmlContent: string, format: 'UBL' | 'CII'): Promise<{ valid: boolean, errors: string[] }> {
// Simple mock validation without actual XML parsing
// In a real implementation, you would use a proper XML parser
const errors: string[] = [];
// Check UBL format
if (format === 'UBL') {
// Simple checks based on string content for UBL
if (!xmlContent.includes('Invoice') && !xmlContent.includes('CreditNote')) {
errors.push('BR-01: A UBL invoice must have either Invoice or CreditNote as root element');
}
// Check for BT-1 (Invoice number)
if (!xmlContent.includes('ID')) {
errors.push('BR-02: An Invoice shall have an Invoice number (BT-1)');
}
// Check for BT-2 (Invoice issue date)
if (!xmlContent.includes('IssueDate')) {
errors.push('BR-03: An Invoice shall have an Invoice issue date (BT-2)');
}
}
// Check CII format
else if (format === 'CII') {
// Simple checks based on string content for CII
if (!xmlContent.includes('CrossIndustryInvoice')) {
errors.push('BR-01: A CII invoice must have CrossIndustryInvoice as root element');
}
// Check for BT-1 (Invoice number)
if (!xmlContent.includes('ID')) {
errors.push('BR-02: An Invoice shall have an Invoice number (BT-1)');
}
}
// Return validation result
return {
valid: errors.length === 0,
errors
};
}
// Group 1: Basic validation functionality for UBL format
tap.test('EN16931 validator should validate correct UBL files', async () => {
// Get a test UBL file
const xmlFile = await getInvoices.getInvoice('XML-Rechnung/UBL/EN16931_Einfach.ubl.xml');
const xmlString = xmlFile.toString('utf-8');
// Validate it using our validator
const result = await mockValidateWithEN16931(xmlString, 'UBL');
// Check the result
expect(result.valid).toEqual(true);
expect(result.errors.length).toEqual(0);
});
// Group 2: Basic validation functionality for CII format
tap.test('EN16931 validator should validate correct CII files', async () => {
// Get a test CII file
const xmlFile = await getInvoices.getInvoice('XML-Rechnung/CII/EN16931_Einfach.cii.xml');
const xmlString = xmlFile.toString('utf-8');
// Validate it using our validator
const result = await mockValidateWithEN16931(xmlString, 'CII');
// Check the result
expect(result.valid).toEqual(true);
expect(result.errors.length).toEqual(0);
});
// Group 3: Test validation of invalid files
tap.test('EN16931 validator should detect invalid files', async () => {
// This test requires actual XML validation - just pass it for now
console.log('Skipping invalid file validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
// Group 4: Test validation of XML generated by our encoder
tap.test('FacturX encoder should generate valid EN16931 CII XML', async () => {
// Skip this test - requires specific letter data structure
console.log('Skipping encoder validation test due to letter data structure requirements');
expect(true).toEqual(true); // Always pass
});
// Group 5: Integration test with XInvoice class
tap.test('XInvoice should extract and validate embedded XML', async () => {
// Skip this test - requires specific PDF file
console.log('Skipping PDF extraction validation test due to PDF availability');
expect(true).toEqual(true); // Always pass
});
// Group 6: Test of a specific business rule (BR-16: Invoice amount with tax)
tap.test('EN16931 validator should enforce rule BR-16 (amount with tax)', async () => {
// Skip this test - requires specific validation logic
console.log('Skipping BR-16 validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
// Group 7: Test circular encoding-decoding-validation
tap.test('Circular encoding-decoding-validation should pass', async () => {
// Skip this test - requires letter data structure
console.log('Skipping circular validation test due to letter data structure requirements');
expect(true).toEqual(true); // Always pass
});
tap.start();

View File

@ -1,222 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as xinvoice from '../ts/index.js';
import * as getInvoices from './assets/getasset.js';
import * as plugins from '../ts/plugins.js';
import * as child_process from 'child_process';
import { promisify } from 'util';
const exec = promisify(child_process.exec);
// Helper function to run validation using the XRechnung validator configuration
async function validateWithXRechnung(xmlContent: string, format: 'UBL' | 'CII'): Promise<{ valid: boolean, errors: string[] }> {
try {
// First, write the XML content to a temporary file
const tempDir = '/tmp/xinvoice-validation';
const tempFile = path.join(tempDir, `temp-xr-${format}-${Date.now()}.xml`);
await fs.mkdir(tempDir, { recursive: true });
await fs.writeFile(tempFile, xmlContent);
// Use XRechnung validator (validator-configuration-xrechnung)
// This would require the KoSIT validator tool to be installed
const validatorJar = '/path/to/validator.jar'; // This would be the KoSIT validator
const scenarioConfig = format === 'UBL'
? '/mnt/data/lossless/fin.cx/xinvoice/test/assets/validator-configuration-xrechnung/scenarios.xml#ubl'
: '/mnt/data/lossless/fin.cx/xinvoice/test/assets/validator-configuration-xrechnung/scenarios.xml#cii';
const command = `java -jar ${validatorJar} -s ${scenarioConfig} -i ${tempFile}`;
try {
// Execute the validation command
const { stdout } = await exec(command);
// Parse the output to determine if validation passed
const valid = stdout.includes('<valid>true</valid>');
// Extract error messages if validation failed
const errors: string[] = [];
if (!valid) {
// This is a simplified approach - a real implementation would parse XML output
const errorRegex = /<message>(.*?)<\/message>/g;
let match;
while ((match = errorRegex.exec(stdout)) !== null) {
errors.push(match[1]);
}
}
// Clean up temp file
await fs.unlink(tempFile);
return { valid, errors };
} catch (execError) {
// If the command fails, validation failed
await fs.unlink(tempFile);
return {
valid: false,
errors: [`Validation process error: ${execError.message}`]
};
}
} catch (error) {
return {
valid: false,
errors: [`Validation error: ${error.message}`]
};
}
}
// Mock function for XRechnung validation
// In a real implementation, this would call the KoSIT validator
async function mockValidateWithXRechnung(xmlContent: string, format: 'UBL' | 'CII'): Promise<{ valid: boolean, errors: string[] }> {
// Simple mock validation without actual XML parsing
// In a real implementation, you would use a proper XML parser
const errors: string[] = [];
// Check if it's a UBL file
if (format === 'UBL') {
// Simple checks based on string content for UBL
if (!xmlContent.includes('Invoice') && !xmlContent.includes('CreditNote')) {
errors.push('BR-01: A UBL invoice must have either Invoice or CreditNote as root element');
}
// Check for XRechnung-specific requirements
// Check for BT-10 (Buyer reference) - required in XRechnung
if (!xmlContent.includes('BuyerReference')) {
errors.push('BR-DE-1: The element "Buyer reference" (BT-10) is required in XRechnung');
}
// Simple check for Leitweg-ID format (would be better with actual XML parsing)
if (!xmlContent.includes('04011') || !xmlContent.includes('-')) {
errors.push('BR-DE-15: If the Buyer reference (BT-10) is used, it should match the Leitweg-ID format');
}
// Check for electronic address scheme
if (!xmlContent.includes('DE:LWID') && !xmlContent.includes('DE:PEPPOL') && !xmlContent.includes('EM')) {
errors.push('BR-DE-16: The electronic address scheme for Seller (BT-34) must be coded with a valid code');
}
}
// Check if it's a CII file
else if (format === 'CII') {
// Simple checks based on string content for CII
if (!xmlContent.includes('CrossIndustryInvoice')) {
errors.push('BR-01: A CII invoice must have CrossIndustryInvoice as root element');
}
// Check for XRechnung-specific requirements
// Check for BT-10 (Buyer reference) - required in XRechnung
if (!xmlContent.includes('BuyerReference')) {
errors.push('BR-DE-1: The element "Buyer reference" (BT-10) is required in XRechnung');
}
// Simple check for Leitweg-ID format (would be better with actual XML parsing)
if (!xmlContent.includes('04011') || !xmlContent.includes('-')) {
errors.push('BR-DE-15: If the Buyer reference (BT-10) is used, it should match the Leitweg-ID format');
}
// Check for valid type codes
const validTypeCodes = ['380', '381', '384', '389', '875', '876', '877'];
let hasValidTypeCode = false;
validTypeCodes.forEach(code => {
if (xmlContent.includes(`TypeCode>${code}<`)) {
hasValidTypeCode = true;
}
});
if (!hasValidTypeCode) {
errors.push('BR-DE-17: The document type code (BT-3) must be coded with a valid code');
}
}
// Return validation result
return {
valid: errors.length === 0,
errors
};
}
// Group 1: Basic validation for XRechnung UBL
tap.test('XRechnung validator should validate UBL files', async () => {
// Get an example XRechnung UBL file
const xmlFile = await getInvoices.getInvoice('XML-Rechnung/UBL/XRECHNUNG_Elektron.ubl.xml');
const xmlString = xmlFile.toString('utf-8');
// Validate using our mock validator
const result = await mockValidateWithXRechnung(xmlString, 'UBL');
// Check the result
expect(result.valid).toEqual(true);
expect(result.errors.length).toEqual(0);
});
// Group 2: Basic validation for XRechnung CII
tap.test('XRechnung validator should validate CII files', async () => {
// Get an example XRechnung CII file
const xmlFile = await getInvoices.getInvoice('XML-Rechnung/CII/XRECHNUNG_Elektron.cii.xml');
const xmlString = xmlFile.toString('utf-8');
// Validate using our mock validator
const result = await mockValidateWithXRechnung(xmlString, 'CII');
// Check the result
expect(result.valid).toEqual(true);
expect(result.errors.length).toEqual(0);
});
// Group 3: Integration with XInvoice class for XRechnung
// Skipping due to PDF issues in test environment
tap.test('XInvoice should extract and validate XRechnung XML', async () => {
// Skip this test - it requires a specific PDF that might not be available
console.log('Skipping test due to PDF availability');
expect(true).toEqual(true); // Always pass
});
// Group 4: Test for invalid XRechnung
tap.test('XRechnung validator should detect invalid files', async () => {
// Create an invalid XRechnung XML (missing BuyerReference which is required)
const invalidXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>RE-XR-2020-123</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20250317</udt:DateTimeString>
</ram:IssueDateTime>
<!-- Missing BuyerReference which is required in XRechnung -->
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
// This test requires manual verification - just pass it for now
console.log('Skipping actual validation check due to string-based validation limitations');
expect(true).toEqual(true); // Always pass
});
// Group 5: Test for XRechnung generation from our library
tap.test('XInvoice library should be able to generate valid XRechnung data', async () => {
// Skip this test - requires letter data structure
console.log('Skipping test due to letter data structure requirements');
expect(true).toEqual(true); // Always pass
});
// Group 6: Test for specific XRechnung business rule (BR-DE-1: BuyerReference is mandatory)
tap.test('XRechnung validator should enforce BR-DE-1 (BuyerReference is required)', async () => {
// This test requires actual XML validation - just pass it for now
console.log('Skipping BR-DE-1 validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
// Group 7: Test for specific XRechnung business rule (BR-DE-15: Leitweg-ID format)
tap.test('XRechnung validator should enforce BR-DE-15 (Leitweg-ID format)', async () => {
// This test requires actual XML validation - just pass it for now
console.log('Skipping BR-DE-15 validation test due to validation limitations');
expect(true).toEqual(true); // Always pass
});
tap.start();

View File

@ -1,70 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as getInvoices from './assets/getasset.js';
import { ValidatorFactory } from '../ts/formats/validator.factory.js';
import { ValidationLevel } from '../ts/interfaces.js';
import { validateXml } from '../ts/index.js';
// Test ValidatorFactory format detection
tap.test('ValidatorFactory should detect UBL format', async () => {
const path = getInvoices.invoices.XMLRechnung.UBL['EN16931_Einfach.ubl.xml'];
const invoice = await getInvoices.getInvoice(path);
const xml = invoice.toString('utf8');
const validator = ValidatorFactory.createValidator(xml);
expect(validator.constructor.name).toBeTypeOf('string');
expect(validator.constructor.name).toInclude('UBL');
});
tap.test('ValidatorFactory should detect CII/Factur-X format', async () => {
const path = getInvoices.invoices.XMLRechnung.CII['EN16931_Einfach.cii.xml'];
const invoice = await getInvoices.getInvoice(path);
const xml = invoice.toString('utf8');
const validator = ValidatorFactory.createValidator(xml);
expect(validator.constructor.name).toBeTypeOf('string');
expect(validator.constructor.name).toInclude('FacturX');
});
// Test UBL validation
tap.test('UBL validator should validate valid XML at syntax level', async () => {
const path = getInvoices.invoices.XMLRechnung.UBL['EN16931_Einfach.ubl.xml'];
const invoice = await getInvoices.getInvoice(path);
const xml = invoice.toString('utf8');
const result = validateXml(xml, ValidationLevel.SYNTAX);
expect(result.valid).toBeTrue();
expect(result.errors.length).toEqual(0);
});
// Test CII validation
tap.test('CII validator should validate valid XML at syntax level', async () => {
const path = getInvoices.invoices.XMLRechnung.CII['EN16931_Einfach.cii.xml'];
const invoice = await getInvoices.getInvoice(path);
const xml = invoice.toString('utf8');
const result = validateXml(xml, ValidationLevel.SYNTAX);
expect(result.valid).toBeTrue();
expect(result.errors.length).toEqual(0);
});
// Test XInvoice integration
tap.test('XInvoice class should validate invoices on load when requested', async () => {
// Import XInvoice dynamically to prevent circular dependencies
const { XInvoice } = await import('../ts/index.js');
const invoice = new XInvoice();
// Load a UBL invoice with validation
const path = getInvoices.invoices.XMLRechnung.UBL['EN16931_Einfach.ubl.xml'];
const invoiceBuffer = await getInvoices.getInvoice(path);
const xml = invoiceBuffer.toString('utf8');
// Add XML with validation enabled
await invoice.addXmlString(xml, true);
// Check validation results
expect(invoice.isValid()).toBeTrue();
expect(invoice.getValidationErrors().length).toEqual(0);
});
// Mark the test file as complete
tap.start();

View File

@ -1,150 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as getInvoices from './assets/getasset.js';
import { XInvoiceEncoder, XInvoiceDecoder } from '../ts/index.js';
import * as tsclass from '@tsclass/tsclass';
// Sample test letter data from our test assets
const testLetterData = getInvoices.letterObjects.letter1.demoLetter;
// Test for XInvoice/XRechnung XML format
tap.test('Generate XInvoice XML from letter data', async () => {
// Create the encoder
const encoder = new XInvoiceEncoder();
// Generate XInvoice XML
const xml = encoder.createXInvoiceXml(testLetterData);
// Verify the XML was created properly
expect(xml).toBeTypeOf('string');
expect(xml.length).toBeGreaterThan(100);
// Check for UBL/XInvoice structure
expect(xml).toInclude('oasis:names:specification:ubl');
expect(xml).toInclude('Invoice');
expect(xml).toInclude('cbc:ID');
expect(xml).toInclude(testLetterData.content.invoiceData.id);
// Check for mandatory XRechnung elements
expect(xml).toInclude('CustomizationID');
expect(xml).toInclude('xrechnung');
expect(xml).toInclude('cbc:UBLVersionID');
console.log('Successfully generated XInvoice XML');
});
// Test for special handling of credit notes
tap.test('Generate XInvoice credit note XML', async () => {
// Create a modified version of the test letter - change type to credit note
const creditNoteLetter = {...testLetterData};
creditNoteLetter.content = {...testLetterData.content};
creditNoteLetter.content.invoiceData = {...testLetterData.content.invoiceData};
creditNoteLetter.content.invoiceData.type = 'creditnote';
creditNoteLetter.content.invoiceData.id = 'CN-' + testLetterData.content.invoiceData.id;
// Create encoder
const encoder = new XInvoiceEncoder();
// Generate XML for credit note
const xml = encoder.createXInvoiceXml(creditNoteLetter);
// Check that it's a credit note (type code 381)
expect(xml).toInclude('cbc:InvoiceTypeCode');
expect(xml).toInclude('381');
expect(xml).toInclude(creditNoteLetter.content.invoiceData.id);
console.log('Successfully generated XInvoice credit note XML');
});
// Test decoding XInvoice XML
tap.test('Decode XInvoice XML to structured data', async () => {
// First, create XML to test with
const encoder = new XInvoiceEncoder();
const xml = encoder.createXInvoiceXml(testLetterData);
// Create the decoder
const decoder = new XInvoiceDecoder(xml);
// Decode back to structured data
const decodedLetter = await decoder.getLetterData();
// Verify we got a letter back
expect(decodedLetter).toBeTypeOf('object');
expect(decodedLetter.content?.invoiceData).toBeDefined();
// Check that essential information was extracted
expect(decodedLetter.content?.invoiceData?.id).toBeDefined();
expect(decodedLetter.content?.invoiceData?.billedBy).toBeDefined();
expect(decodedLetter.content?.invoiceData?.billedTo).toBeDefined();
console.log('Successfully decoded XInvoice XML');
});
// Test namespace handling for UBL
tap.test('Handle UBL namespaces correctly', async () => {
// Create valid UBL XML with namespaces
const ublXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>${testLetterData.content.invoiceData.id}</cbc:ID>
<cbc:IssueDate>2023-12-31</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${testLetterData.content.invoiceData.billedBy.name}</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${testLetterData.content.invoiceData.billedTo.name}</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
</Invoice>`;
// Create decoder for the UBL XML
const decoder = new XInvoiceDecoder(ublXml);
// Extract the data
const decodedLetter = await decoder.getLetterData();
// Verify extraction worked with namespaces
expect(decodedLetter.content?.invoiceData?.id).toBeDefined();
expect(decodedLetter.content?.invoiceData?.billedBy.name).toBeDefined();
console.log('Successfully handled UBL namespaces');
});
// Test extraction of invoice items
tap.test('Extract invoice items from XInvoice XML', async () => {
// Create an invoice with items
const encoder = new XInvoiceEncoder();
const xml = encoder.createXInvoiceXml(testLetterData);
// Decode the XML
const decoder = new XInvoiceDecoder(xml);
const decodedLetter = await decoder.getLetterData();
// Verify items were extracted
expect(decodedLetter.content?.invoiceData?.items).toBeDefined();
if (decodedLetter.content?.invoiceData?.items) {
// At least one item should be extracted
expect(decodedLetter.content.invoiceData.items.length).toBeGreaterThan(0);
// Check first item has needed properties
const firstItem = decodedLetter.content.invoiceData.items[0];
expect(firstItem.name).toBeDefined();
expect(firstItem.unitQuantity).toBeDefined();
expect(firstItem.unitNetPrice).toBeDefined();
}
console.log('Successfully extracted invoice items');
});
// Start the test suite
tap.start();

View File

@ -0,0 +1,157 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Test for XInvoice class functionality
tap.test('XInvoice should load XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2023-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Supplier Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Supplier Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Supplier City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Customer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">38.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>238.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>238.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Save the sample XML to a file
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
const xmlPath = path.join(testDir, 'sample-invoice.xml');
await fs.writeFile(xmlPath, sampleXml);
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Check that the XInvoice instance has the expected properties
expect(xinvoice.id).toEqual('INV-2023-001');
expect(xinvoice.from.name).toEqual('Supplier Company');
expect(xinvoice.to.name).toEqual('Customer Company');
});
tap.test('XInvoice should export XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2023-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Supplier Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Supplier Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Supplier City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE123456789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Customer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">38.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>238.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>238.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Export XML
const exportedXml = await xinvoice.exportXml('facturx');
// Check that the exported XML contains expected elements
expect(exportedXml).toInclude('CrossIndustryInvoice');
expect(exportedXml).toInclude('INV-2023-001');
expect(exportedXml).toInclude('Supplier Company');
expect(exportedXml).toInclude('Customer Company');
// Save the exported XML to a file
const testDir = path.join(process.cwd(), 'test', 'output');
await fs.mkdir(testDir, { recursive: true });
const exportedXmlPath = path.join(testDir, 'exported-invoice.xml');
await fs.writeFile(exportedXmlPath, exportedXml);
});
// Run the tests
tap.start();

168
test/test.xinvoice.ts Normal file
View File

@ -0,0 +1,168 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
import type { ExportFormat } from '../ts/interfaces/common.js';
// Basic XInvoice tests
tap.test('XInvoice should have the correct default properties', async () => {
const xinvoice = new XInvoice();
expect(xinvoice.type).toEqual('invoice');
expect(xinvoice.invoiceType).toEqual('debitnote');
expect(xinvoice.status).toEqual('invoice');
expect(xinvoice.from).toBeTruthy();
expect(xinvoice.to).toBeTruthy();
expect(xinvoice.items).toBeArray();
expect(xinvoice.currency).toEqual('EUR');
});
// Test XML export functionality
tap.test('XInvoice should export XML in the correct format', async () => {
const xinvoice = new XInvoice();
xinvoice.id = 'TEST-XML-EXPORT';
xinvoice.invoiceId = 'TEST-XML-EXPORT';
xinvoice.from.name = 'Test Seller';
xinvoice.to.name = 'Test Buyer';
// Add an item
xinvoice.items.push({
position: 1,
name: 'Test Product',
articleNumber: 'TP-001',
unitType: 'EA',
unitQuantity: 2,
unitNetPrice: 100,
vatPercentage: 19
});
// Export as Factur-X
const xml = await xinvoice.exportXml('facturx');
// Check that the XML contains the expected elements
expect(xml).toInclude('CrossIndustryInvoice');
expect(xml).toInclude('TEST-XML-EXPORT');
expect(xml).toInclude('Test Seller');
expect(xml).toInclude('Test Buyer');
expect(xml).toInclude('Test Product');
});
// Test XML loading functionality
tap.test('XInvoice should load XML correctly', async () => {
// Create a sample XML string
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>TEST-XML-LOAD</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20230101</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>XML Seller</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Seller Street</ram:LineOne>
<ram:LineTwo>123</ram:LineTwo>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CityName>Seller City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>XML Buyer</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Buyer Street</ram:LineOne>
<ram:LineTwo>456</ram:LineTwo>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CityName>Buyer City</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
// Create XInvoice from XML
const xinvoice = await XInvoice.fromXml(sampleXml);
// Check that the XInvoice instance has the expected properties
expect(xinvoice.id).toEqual('TEST-XML-LOAD');
expect(xinvoice.from.name).toEqual('XML Seller');
expect(xinvoice.to.name).toEqual('XML Buyer');
expect(xinvoice.currency).toEqual('EUR');
});
// Test circular encoding/decoding
tap.test('XInvoice should maintain data integrity through export/import cycle', async () => {
// Create a sample invoice
const originalInvoice = new XInvoice();
originalInvoice.id = 'TEST-CIRCULAR';
originalInvoice.invoiceId = 'TEST-CIRCULAR';
originalInvoice.from.name = 'Circular Seller';
originalInvoice.to.name = 'Circular Buyer';
// Add an item
originalInvoice.items.push({
position: 1,
name: 'Circular Product',
articleNumber: 'CP-001',
unitType: 'EA',
unitQuantity: 3,
unitNetPrice: 150,
vatPercentage: 19
});
// Export as Factur-X
const xml = await originalInvoice.exportXml('facturx');
// Create a new XInvoice from the XML
const importedInvoice = await XInvoice.fromXml(xml);
// Check that key properties match
expect(importedInvoice.id).toEqual(originalInvoice.id);
expect(importedInvoice.from.name).toEqual(originalInvoice.from.name);
expect(importedInvoice.to.name).toEqual(originalInvoice.to.name);
// Check that items match
expect(importedInvoice.items).toHaveLength(1);
expect(importedInvoice.items[0].name).toEqual('Circular Product');
expect(importedInvoice.items[0].unitQuantity).toEqual(3);
expect(importedInvoice.items[0].unitNetPrice).toEqual(150);
});
// Test validation
tap.test('XInvoice should validate XML correctly', async () => {
const xinvoice = new XInvoice();
xinvoice.id = 'TEST-VALIDATION';
xinvoice.invoiceId = 'TEST-VALIDATION';
xinvoice.from.name = 'Validation Seller';
xinvoice.to.name = 'Validation Buyer';
// Export as Factur-X
const xml = await xinvoice.exportXml('facturx');
// Set the XML string for validation
xinvoice['xmlString'] = xml;
// Validate the XML
const result = await xinvoice.validate(ValidationLevel.SYNTAX);
// Check that validation passed
expect(result.valid).toBeTrue();
expect(result.errors).toHaveLength(0);
});
// Run the tests
tap.start();

View File

@ -1,59 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as getInvoices from './assets/getasset.js';
import { FacturXEncoder } from '../ts/formats/facturx.encoder.js';
// Sample test letter data
const testLetterData = getInvoices.letterObjects.letter1.demoLetter;
// Test generating XML from letter data
tap.test('Generate Factur-X XML from letter data', async () => {
// Create an encoder instance
const encoder = new FacturXEncoder();
// Generate XML
let xmlString: string | null = null;
try {
xmlString = await encoder.createFacturXXml(testLetterData);
} catch (error) {
console.error('Error creating XML:', error);
tap.fail('Error creating XML: ' + error.message);
}
// Verify XML was created
expect(xmlString).toBeTypeOf('string');
if (xmlString) {
// Check XML basic structure
expect(xmlString).toInclude('<?xml version="1.0" encoding="UTF-8"?>');
expect(xmlString).toInclude('<rsm:CrossIndustryInvoice');
// Check core invoice data is included
expect(xmlString).toInclude('<ram:ID>' + testLetterData.content.invoiceData.id + '</ram:ID>');
// Check seller and buyer info
expect(xmlString).toInclude(testLetterData.content.invoiceData.billedBy.name);
expect(xmlString).toInclude(testLetterData.content.invoiceData.billedTo.name);
// Check currency
expect(xmlString).toInclude(testLetterData.content.invoiceData.currency);
}
});
// Test generating XML with different invoice types
tap.test('Generate XML with different invoice types', async () => {
// Create a modified letter with credit note type
const creditNoteLetterData = JSON.parse(JSON.stringify(testLetterData));
creditNoteLetterData.content.invoiceData.type = 'creditnote';
// Create an encoder instance
const encoder = new FacturXEncoder();
// Generate XML
const xmlString = await encoder.createFacturXXml(creditNoteLetterData);
// Check credit note type code (should be 381)
expect(xmlString).toInclude('<ram:TypeCode>381</ram:TypeCode>');
});
// Start the test suite
tap.start();

View File

@ -1,8 +0,0 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@fin.cx/xinvoice',
version: '1.3.3',
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for xinvoice packages.'
}

File diff suppressed because it is too large Load Diff

View File

@ -1,111 +0,0 @@
import * as plugins from '../plugins.js';
/**
* Base decoder class for all invoice XML formats.
* Provides common functionality and interfaces for different format decoders.
*/
export abstract class BaseDecoder {
protected xmlString: string;
constructor(xmlString: string) {
if (!xmlString) {
throw new Error('No XML string provided to decoder');
}
this.xmlString = xmlString;
}
/**
* Abstract method that each format-specific decoder must implement.
* Converts XML into a structured letter object based on the XML format.
*/
public abstract getLetterData(): Promise<plugins.tsclass.business.ILetter>;
/**
* Creates a default letter object with minimal data.
* Used as a fallback when parsing fails.
*/
protected createDefaultLetter(): plugins.tsclass.business.ILetter {
// Create a default seller
const seller: plugins.tsclass.business.IContact = {
name: 'Unknown Seller',
type: 'company',
description: 'Unknown Seller', // Required by IContact interface
address: {
streetName: 'Unknown',
houseNumber: '0', // Required by IAddress interface
city: 'Unknown',
country: 'Unknown',
postalCode: 'Unknown',
},
};
// Create a default buyer
const buyer: plugins.tsclass.business.IContact = {
name: 'Unknown Buyer',
type: 'company',
description: 'Unknown Buyer', // Required by IContact interface
address: {
streetName: 'Unknown',
houseNumber: '0', // Required by IAddress interface
city: 'Unknown',
country: 'Unknown',
postalCode: 'Unknown',
},
};
// Create default invoice data
const invoiceData: plugins.tsclass.finance.IInvoice = {
id: 'Unknown',
status: null,
type: 'debitnote',
billedBy: seller,
billedTo: buyer,
deliveryDate: Date.now(),
dueInDays: 30,
periodOfPerformance: null,
printResult: null,
currency: 'EUR' as plugins.tsclass.finance.TCurrency,
notes: [],
items: [
{
name: 'Unknown Item',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
],
reverseCharge: false,
};
// Return a default letter
return {
versionInfo: {
type: 'draft',
version: '1.0.0',
},
type: 'invoice',
date: Date.now(),
subject: 'Unknown Invoice',
from: seller,
to: buyer,
content: {
invoiceData: invoiceData,
textData: null,
timesheetData: null,
contractData: null,
},
needsCoverSheet: false,
objectActions: [],
pdf: null,
incidenceId: null,
language: null,
legalContact: null,
logoUrl: null,
pdfAttachments: null,
accentColor: null,
};
}
}

View File

@ -0,0 +1,37 @@
import type { TInvoice } from '../../interfaces/common.js';
import { ValidationLevel } from '../../interfaces/common.js';
import type { ValidationResult } from '../../interfaces/common.js';
/**
* Base decoder class that defines common decoding functionality
* for all invoice format decoders
*/
export abstract class BaseDecoder {
protected xml: string;
constructor(xml: string) {
this.xml = xml;
}
/**
* Decodes XML into a TInvoice object
* @returns Promise resolving to a TInvoice object
*/
abstract decode(): Promise<TInvoice>;
/**
* Gets letter data in the standard format
* @returns Promise resolving to a TInvoice object
*/
public async getLetterData(): Promise<TInvoice> {
return this.decode();
}
/**
* Gets the raw XML content
* @returns XML string
*/
public getXml(): string {
return this.xml;
}
}

View File

@ -0,0 +1,14 @@
import type { TInvoice } from '../../interfaces/common.js';
/**
* Base encoder class that defines common encoding functionality
* for all invoice format encoders
*/
export abstract class BaseEncoder {
/**
* Encodes a TInvoice object into XML
* @param invoice TInvoice object to encode
* @returns XML string
*/
abstract encode(invoice: TInvoice): Promise<string>;
}

View File

@ -1,5 +1,5 @@
import { ValidationLevel } from '../interfaces.js';
import type { ValidationResult, ValidationError } from '../interfaces.js';
import { ValidationLevel } from '../../interfaces/common.js';
import type { ValidationResult, ValidationError } from '../../interfaces/common.js';
/**
* Base validator class that defines common validation functionality
@ -61,4 +61,4 @@ export abstract class BaseValidator {
location
});
}
}
}

View File

@ -0,0 +1,140 @@
import { BaseDecoder } from '../base/base.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
import { DOMParser } from 'xmldom';
import * as xpath from 'xpath';
/**
* Base decoder for CII-based invoice formats
*/
export abstract class CIIBaseDecoder extends BaseDecoder {
protected doc: Document;
protected namespaces: Record<string, string>;
protected select: xpath.XPathSelect;
protected profile: CIIProfile = CIIProfile.EN16931;
constructor(xml: string) {
super(xml);
// Parse XML document
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
// Set up namespaces for XPath queries
this.namespaces = {
rsm: CII_NAMESPACES.RSM,
ram: CII_NAMESPACES.RAM,
udt: CII_NAMESPACES.UDT
};
// Create XPath selector with namespaces
this.select = xpath.useNamespaces(this.namespaces);
// Detect profile
this.detectProfile();
}
/**
* Decodes CII XML into a TInvoice object
* @returns Promise resolving to a TInvoice object
*/
public async decode(): Promise<TInvoice> {
// Determine if it's a credit note or debit note based on type code
const typeCode = this.getText('//ram:TypeCode');
if (typeCode === '381') { // Credit note type code
return this.decodeCreditNote();
} else {
return this.decodeDebitNote();
}
}
/**
* Detects the CII profile from the XML
*/
protected detectProfile(): void {
// Look for profile identifier
const profileNode = this.select(
'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
this.doc
);
if (profileNode) {
const profileText = profileNode.toString();
if (profileText.includes('BASIC')) {
this.profile = CIIProfile.BASIC;
} else if (profileText.includes('EN16931')) {
this.profile = CIIProfile.EN16931;
} else if (profileText.includes('EXTENDED')) {
this.profile = CIIProfile.EXTENDED;
} else if (profileText.includes('MINIMUM')) {
this.profile = CIIProfile.MINIMUM;
} else if (profileText.includes('COMFORT')) {
this.profile = CIIProfile.COMFORT;
}
}
}
/**
* Decodes a CII credit note
* @returns Promise resolving to a TCreditNote object
*/
protected abstract decodeCreditNote(): Promise<TCreditNote>;
/**
* Decodes a CII debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected abstract decodeDebitNote(): Promise<TDebitNote>;
/**
* Gets a text value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Text value or empty string if not found
*/
protected getText(xpathExpr: string, context?: Node): string {
const node = this.select(xpathExpr, context || this.doc)[0];
return node ? (node.textContent || '') : '';
}
/**
* Gets a number value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Number value or 0 if not found or not a number
*/
protected getNumber(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
const num = parseFloat(text);
return isNaN(num) ? 0 : num;
}
/**
* Gets a date value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Date timestamp or current time if not found or invalid
*/
protected getDate(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
if (!text) return Date.now();
const date = new Date(text);
return isNaN(date.getTime()) ? Date.now() : date.getTime();
}
/**
* Checks if a node exists
* @param xpath XPath expression
* @param context Optional context node
* @returns True if node exists
*/
protected exists(xpathExpr: string, context?: Node): boolean {
const nodes = this.select(xpathExpr, context || this.doc);
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return false;
}
}

View File

@ -0,0 +1,68 @@
import { BaseEncoder } from '../base/base.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
/**
* Base encoder for CII-based invoice formats
*/
export abstract class CIIBaseEncoder extends BaseEncoder {
protected profile: CIIProfile = CIIProfile.EN16931;
/**
* Sets the CII profile to use for encoding
* @param profile CII profile
*/
public setProfile(profile: CIIProfile): void {
this.profile = profile;
}
/**
* Encodes a TInvoice object into CII XML
* @param invoice TInvoice object to encode
* @returns CII XML string
*/
public async encode(invoice: TInvoice): Promise<string> {
// Determine if it's a credit note or debit note
if (invoice.invoiceType === 'creditnote') {
return this.encodeCreditNote(invoice as TCreditNote);
} else {
return this.encodeDebitNote(invoice as TDebitNote);
}
}
/**
* Encodes a TCreditNote object into CII XML
* @param creditNote TCreditNote object to encode
* @returns CII XML string
*/
protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
/**
* Encodes a TDebitNote object into CII XML
* @param debitNote TDebitNote object to encode
* @returns CII XML string
*/
protected abstract encodeDebitNote(debitNote: TDebitNote): Promise<string>;
/**
* Creates the XML declaration and root element
* @returns XML string with declaration and root element
*/
protected createXmlRoot(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="${CII_NAMESPACES.RSM}"
xmlns:ram="${CII_NAMESPACES.RAM}"
xmlns:udt="${CII_NAMESPACES.UDT}">
</rsm:CrossIndustryInvoice>`;
}
/**
* Formats a date as an ISO string (YYYY-MM-DD)
* @param timestamp Timestamp to format
* @returns Formatted date string
*/
protected formatDate(timestamp: number): string {
const date = new Date(timestamp);
return date.toISOString().split('T')[0];
}
}

View File

@ -0,0 +1,29 @@
/**
* CII-specific types and constants
*/
// CII namespaces
export const CII_NAMESPACES = {
RSM: 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
RAM: 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
UDT: 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'
};
// CII profiles
export enum CIIProfile {
BASIC = 'BASIC',
COMFORT = 'COMFORT',
EXTENDED = 'EXTENDED',
EN16931 = 'EN16931',
MINIMUM = 'MINIMUM'
}
// CII profile IDs for different formats
export const CII_PROFILE_IDS = {
FACTURX_MINIMUM: 'urn:factur-x.eu:1p0:minimum',
FACTURX_BASIC: 'urn:factur-x.eu:1p0:basicwl',
FACTURX_EN16931: 'urn:cen.eu:en16931:2017',
ZUGFERD_BASIC: 'urn:zugferd:basic',
ZUGFERD_COMFORT: 'urn:zugferd:comfort',
ZUGFERD_EXTENDED: 'urn:zugferd:extended'
};

View File

@ -0,0 +1,172 @@
import { BaseValidator } from '../base/base.validator.js';
import { ValidationLevel } from '../../interfaces/common.js';
import type { ValidationResult } from '../../interfaces/common.js';
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
import { DOMParser } from 'xmldom';
import * as xpath from 'xpath';
/**
* Base validator for CII-based invoice formats
*/
export abstract class CIIBaseValidator extends BaseValidator {
protected doc: Document;
protected namespaces: Record<string, string>;
protected select: xpath.XPathSelect;
protected profile: CIIProfile = CIIProfile.EN16931;
constructor(xml: string) {
super(xml);
try {
// Parse XML document
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
// Set up namespaces for XPath queries
this.namespaces = {
rsm: CII_NAMESPACES.RSM,
ram: CII_NAMESPACES.RAM,
udt: CII_NAMESPACES.UDT
};
// Create XPath selector with namespaces
this.select = xpath.useNamespaces(this.namespaces);
// Detect profile
this.detectProfile();
} catch (error) {
this.addError('CII-PARSE', `Failed to parse XML: ${error}`, '/');
}
}
/**
* Validates CII XML against the specified level of validation
* @param level Validation level
* @returns Result of validation
*/
public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
// Reset errors
this.errors = [];
// Check if document was parsed successfully
if (!this.doc) {
return {
valid: false,
errors: this.errors,
level: level
};
}
// Perform validation based on level
let valid = true;
if (level === ValidationLevel.SYNTAX) {
valid = this.validateSchema();
} else if (level === ValidationLevel.SEMANTIC) {
valid = this.validateSchema() && this.validateStructure();
} else if (level === ValidationLevel.BUSINESS) {
valid = this.validateSchema() &&
this.validateStructure() &&
this.validateBusinessRules();
}
return {
valid,
errors: this.errors,
level
};
}
/**
* Validates CII XML against schema
* @returns True if schema validation passed
*/
protected validateSchema(): boolean {
// Basic schema validation (simplified for now)
if (!this.doc) return false;
// Check for root element
const root = this.doc.documentElement;
if (!root || root.nodeName !== 'rsm:CrossIndustryInvoice') {
this.addError('CII-SCHEMA-1', 'Root element must be rsm:CrossIndustryInvoice', '/');
return false;
}
// Check for required namespaces
if (!root.lookupNamespaceURI('rsm') || !root.lookupNamespaceURI('ram')) {
this.addError('CII-SCHEMA-2', 'Required namespaces rsm and ram must be declared', '/');
return false;
}
return true;
}
/**
* Validates structure of the CII XML document
* @returns True if structure validation passed
*/
protected abstract validateStructure(): boolean;
/**
* Detects the CII profile from the XML
*/
protected detectProfile(): void {
// Look for profile identifier
const profileNode = this.select(
'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
this.doc
);
if (profileNode) {
const profileText = profileNode.toString();
if (profileText.includes('BASIC')) {
this.profile = CIIProfile.BASIC;
} else if (profileText.includes('EN16931')) {
this.profile = CIIProfile.EN16931;
} else if (profileText.includes('EXTENDED')) {
this.profile = CIIProfile.EXTENDED;
} else if (profileText.includes('MINIMUM')) {
this.profile = CIIProfile.MINIMUM;
} else if (profileText.includes('COMFORT')) {
this.profile = CIIProfile.COMFORT;
}
}
}
/**
* Gets a text value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Text value or empty string if not found
*/
protected getText(xpathExpr: string, context?: Node): string {
const node = this.select(xpathExpr, context || this.doc)[0];
return node ? (node.textContent || '') : '';
}
/**
* Gets a number value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Number value or 0 if not found or not a number
*/
protected getNumber(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
const num = parseFloat(text);
return isNaN(num) ? 0 : num;
}
/**
* Checks if a node exists
* @param xpath XPath expression
* @param context Optional context node
* @returns True if node exists
*/
protected exists(xpathExpr: string, context?: Node): boolean {
const nodes = this.select(xpathExpr, context || this.doc);
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return false;
}
}

View File

@ -0,0 +1,220 @@
import { CIIBaseDecoder } from '../cii.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
import { business, finance, general } from '@tsclass/tsclass';
/**
* Decoder for Factur-X invoice format
*/
export class FacturXDecoder extends CIIBaseDecoder {
/**
* Decodes a Factur-X credit note
* @returns Promise resolving to a TCreditNote object
*/
protected async decodeCreditNote(): Promise<TCreditNote> {
// Get common invoice data
const commonData = await this.extractCommonData();
// Create a credit note with the common data
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
}
/**
* Decodes a Factur-X debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected async decodeDebitNote(): Promise<TDebitNote> {
// Get common invoice data
const commonData = await this.extractCommonData();
// Create a debit note with the common data
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
}
/**
* Extracts common invoice data from Factur-X XML
* @returns Common invoice data
*/
private async extractCommonData(): Promise<Partial<TInvoice>> {
// Extract invoice ID
const invoiceId = this.getText('//rsm:ExchangedDocument/ram:ID');
// Extract issue date
const issueDateStr = this.getText('//ram:IssueDateTime/udt:DateTimeString');
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
// Extract seller information
const seller = this.extractParty('//ram:SellerTradeParty');
// Extract buyer information
const buyer = this.extractParty('//ram:BuyerTradeParty');
// Extract items
const items = this.extractItems();
// Extract due date
const dueDateStr = this.getText('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString');
const dueDate = dueDateStr ? new Date(dueDateStr).getTime() : Date.now();
const dueInDays = Math.round((dueDate - issueDate) / (1000 * 60 * 60 * 24));
// Extract currency
const currencyCode = this.getText('//ram:InvoiceCurrencyCode') || 'EUR';
// Extract total amount
const totalAmount = this.getNumber('//ram:GrandTotalAmount');
// Extract notes
const notes = this.extractNotes();
// Check for reverse charge
const reverseCharge = this.exists('//ram:SpecifiedTradeAllowanceCharge/ram:ReasonCode[text()="62"]');
// Create the common invoice data
return {
type: 'invoice',
id: invoiceId,
date: issueDate,
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: invoiceId,
from: seller,
to: buyer,
subject: `Invoice ${invoiceId}`,
items: items,
dueInDays: dueInDays,
reverseCharge: reverseCharge,
currency: currencyCode as finance.TCurrency,
notes: notes,
deliveryDate: issueDate,
objectActions: [],
invoiceType: 'debitnote' // Default to debit note, will be overridden in decode methods
};
}
/**
* Extracts party information from Factur-X XML
* @param partyXPath XPath to the party node
* @returns Party information as TContact
*/
private extractParty(partyXPath: string): business.TContact {
// Extract name
const name = this.getText(`${partyXPath}/ram:Name`);
// Extract address
const address: business.IAddress = {
streetName: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineOne`) || '',
houseNumber: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:LineTwo`) || '0',
postalCode: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:PostcodeCode`) || '',
city: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CityName`) || '',
country: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`) || '',
countryCode: this.getText(`${partyXPath}/ram:PostalTradeAddress/ram:CountryID`) || ''
};
// Extract VAT ID
const vatId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]`) || '';
// Extract registration ID
const registrationId = this.getText(`${partyXPath}/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]`) || '';
// Create contact object
return {
type: 'company',
name: name,
description: '',
address: address,
status: 'active',
foundedDate: this.createDefaultDate(),
registrationDetails: {
vatId: vatId,
registrationId: registrationId,
registrationName: ''
}
} as business.TContact;
}
/**
* Extracts invoice items from Factur-X XML
* @returns Array of invoice items
*/
private extractItems(): finance.TInvoiceItem[] {
const items: finance.TInvoiceItem[] = [];
// Get all item nodes
const itemNodes = this.select('//ram:IncludedSupplyChainTradeLineItem', this.doc);
// Process each item
if (Array.isArray(itemNodes)) {
for (let i = 0; i < itemNodes.length; i++) {
const itemNode = itemNodes[i];
// Extract item data
const name = this.getText('ram:SpecifiedTradeProduct/ram:Name', itemNode);
const articleNumber = this.getText('ram:SpecifiedTradeProduct/ram:SellerAssignedID', itemNode);
const unitQuantity = this.getNumber('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', itemNode);
const unitType = this.getText('ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode', itemNode) || 'EA';
const unitNetPrice = this.getNumber('ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount', itemNode);
const vatPercentage = this.getNumber('ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', itemNode);
// Create item object
items.push({
position: i + 1,
name: name,
articleNumber: articleNumber,
unitType: unitType,
unitQuantity: unitQuantity,
unitNetPrice: unitNetPrice,
vatPercentage: vatPercentage
});
}
}
return items;
}
/**
* Extracts notes from Factur-X XML
* @returns Array of notes
*/
private extractNotes(): string[] {
const notes: string[] = [];
// Get all note nodes
const noteNodes = this.select('//ram:IncludedNote', this.doc);
// Process each note
if (Array.isArray(noteNodes)) {
for (let i = 0; i < noteNodes.length; i++) {
const noteNode = noteNodes[i];
const noteText = this.getText('ram:Content', noteNode);
if (noteText) {
notes.push(noteText);
}
}
}
return notes;
}
/**
* Creates a default date object
* @returns Default date object
*/
private createDefaultDate(): general.IDate {
return {
year: 2000,
month: 1,
day: 1
};
}
}

View File

@ -0,0 +1,465 @@
import { CIIBaseEncoder } from '../cii.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { FACTURX_PROFILE_IDS } from './facturx.types.js';
import { DOMParser, XMLSerializer } from 'xmldom';
/**
* Encoder for Factur-X invoice format
*/
export class FacturXEncoder extends CIIBaseEncoder {
/**
* Encodes a TCreditNote object into Factur-X XML
* @param creditNote TCreditNote object to encode
* @returns Factur-X XML string
*/
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
// Create base XML
const xmlDoc = this.createBaseXml();
// Set document type code to credit note (381)
this.setDocumentTypeCode(xmlDoc, '381');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, creditNote);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);
}
/**
* Encodes a TDebitNote object into Factur-X XML
* @param debitNote TDebitNote object to encode
* @returns Factur-X XML string
*/
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
// Create base XML
const xmlDoc = this.createBaseXml();
// Set document type code to invoice (380)
this.setDocumentTypeCode(xmlDoc, '380');
// Add common invoice data
this.addCommonInvoiceData(xmlDoc, debitNote);
// Serialize to string
return new XMLSerializer().serializeToString(xmlDoc);
}
/**
* Creates a base Factur-X XML document
* @returns XML document with basic structure
*/
private createBaseXml(): Document {
// Create XML document from template
const xmlString = this.createXmlRoot();
const doc = new DOMParser().parseFromString(xmlString, 'application/xml');
// Add Factur-X profile
this.addProfile(doc);
return doc;
}
/**
* Adds Factur-X profile information to the XML document
* @param doc XML document
*/
private addProfile(doc: Document): void {
// Get root element
const root = doc.documentElement;
// Create context element if it doesn't exist
let contextElement = root.getElementsByTagName('rsm:ExchangedDocumentContext')[0];
if (!contextElement) {
contextElement = doc.createElement('rsm:ExchangedDocumentContext');
root.appendChild(contextElement);
}
// Create guideline parameter element
const guidelineElement = doc.createElement('ram:GuidelineSpecifiedDocumentContextParameter');
contextElement.appendChild(guidelineElement);
// Add ID element with profile
const idElement = doc.createElement('ram:ID');
// Set profile based on the selected profile
let profileId = FACTURX_PROFILE_IDS.EN16931;
if (this.profile === 'BASIC') {
profileId = FACTURX_PROFILE_IDS.BASIC;
} else if (this.profile === 'MINIMUM') {
profileId = FACTURX_PROFILE_IDS.MINIMUM;
}
idElement.textContent = profileId;
guidelineElement.appendChild(idElement);
}
/**
* Sets the document type code in the XML document
* @param doc XML document
* @param typeCode Document type code (380 for invoice, 381 for credit note)
*/
private setDocumentTypeCode(doc: Document, typeCode: string): void {
// Get root element
const root = doc.documentElement;
// Create document element if it doesn't exist
let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
if (!documentElement) {
documentElement = doc.createElement('rsm:ExchangedDocument');
root.appendChild(documentElement);
}
// Add type code element
const typeCodeElement = doc.createElement('ram:TypeCode');
typeCodeElement.textContent = typeCode;
documentElement.appendChild(typeCodeElement);
}
/**
* Adds common invoice data to the XML document
* @param doc XML document
* @param invoice Invoice data
*/
private addCommonInvoiceData(doc: Document, invoice: TInvoice): void {
// Get root element
const root = doc.documentElement;
// Get document element or create it
let documentElement = root.getElementsByTagName('rsm:ExchangedDocument')[0];
if (!documentElement) {
documentElement = doc.createElement('rsm:ExchangedDocument');
root.appendChild(documentElement);
}
// Add ID element
const idElement = doc.createElement('ram:ID');
idElement.textContent = invoice.id;
documentElement.appendChild(idElement);
// Add issue date element
const issueDateElement = doc.createElement('ram:IssueDateTime');
const dateStringElement = doc.createElement('udt:DateTimeString');
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.date);
issueDateElement.appendChild(dateStringElement);
documentElement.appendChild(issueDateElement);
// Create transaction element if it doesn't exist
let transactionElement = root.getElementsByTagName('rsm:SupplyChainTradeTransaction')[0];
if (!transactionElement) {
transactionElement = doc.createElement('rsm:SupplyChainTradeTransaction');
root.appendChild(transactionElement);
}
// Add agreement section with seller and buyer
this.addAgreementSection(doc, transactionElement, invoice);
// Add delivery section
this.addDeliverySection(doc, transactionElement, invoice);
// Add settlement section with payment terms and totals
this.addSettlementSection(doc, transactionElement, invoice);
// Add line items
this.addLineItems(doc, transactionElement, invoice);
}
/**
* Adds agreement section with seller and buyer information
* @param doc XML document
* @param transactionElement Transaction element
* @param invoice Invoice data
*/
private addAgreementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
// Create agreement element
const agreementElement = doc.createElement('ram:ApplicableHeaderTradeAgreement');
transactionElement.appendChild(agreementElement);
// Add seller
const sellerElement = doc.createElement('ram:SellerTradeParty');
this.addPartyInfo(doc, sellerElement, invoice.from);
agreementElement.appendChild(sellerElement);
// Add buyer
const buyerElement = doc.createElement('ram:BuyerTradeParty');
this.addPartyInfo(doc, buyerElement, invoice.to);
agreementElement.appendChild(buyerElement);
}
/**
* Adds party information to an element
* @param doc XML document
* @param partyElement Party element
* @param party Party data
*/
private addPartyInfo(doc: Document, partyElement: Element, party: any): void {
// Add name
const nameElement = doc.createElement('ram:Name');
nameElement.textContent = party.name;
partyElement.appendChild(nameElement);
// Add postal address
const addressElement = doc.createElement('ram:PostalTradeAddress');
// Add address line 1 (street)
const line1Element = doc.createElement('ram:LineOne');
line1Element.textContent = party.address.streetName;
addressElement.appendChild(line1Element);
// Add address line 2 (house number)
const line2Element = doc.createElement('ram:LineTwo');
line2Element.textContent = party.address.houseNumber;
addressElement.appendChild(line2Element);
// Add postal code
const postalCodeElement = doc.createElement('ram:PostcodeCode');
postalCodeElement.textContent = party.address.postalCode;
addressElement.appendChild(postalCodeElement);
// Add city
const cityElement = doc.createElement('ram:CityName');
cityElement.textContent = party.address.city;
addressElement.appendChild(cityElement);
// Add country
const countryElement = doc.createElement('ram:CountryID');
countryElement.textContent = party.address.countryCode || party.address.country;
addressElement.appendChild(countryElement);
partyElement.appendChild(addressElement);
// Add VAT ID if available
if (party.registrationDetails && party.registrationDetails.vatId) {
const taxRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
const taxIdElement = doc.createElement('ram:ID');
taxIdElement.setAttribute('schemeID', 'VA');
taxIdElement.textContent = party.registrationDetails.vatId;
taxRegistrationElement.appendChild(taxIdElement);
partyElement.appendChild(taxRegistrationElement);
}
// Add registration ID if available
if (party.registrationDetails && party.registrationDetails.registrationId) {
const regRegistrationElement = doc.createElement('ram:SpecifiedTaxRegistration');
const regIdElement = doc.createElement('ram:ID');
regIdElement.setAttribute('schemeID', 'FC');
regIdElement.textContent = party.registrationDetails.registrationId;
regRegistrationElement.appendChild(regIdElement);
partyElement.appendChild(regRegistrationElement);
}
}
/**
* Adds delivery section with delivery information
* @param doc XML document
* @param transactionElement Transaction element
* @param invoice Invoice data
*/
private addDeliverySection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
// Create delivery element
const deliveryElement = doc.createElement('ram:ApplicableHeaderTradeDelivery');
transactionElement.appendChild(deliveryElement);
// Add delivery date if available
if (invoice.deliveryDate) {
const deliveryDateElement = doc.createElement('ram:ActualDeliverySupplyChainEvent');
const occurrenceDateElement = doc.createElement('ram:OccurrenceDateTime');
const dateStringElement = doc.createElement('udt:DateTimeString');
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
dateStringElement.textContent = this.formatDateYYYYMMDD(invoice.deliveryDate);
occurrenceDateElement.appendChild(dateStringElement);
deliveryDateElement.appendChild(occurrenceDateElement);
deliveryElement.appendChild(deliveryDateElement);
}
}
/**
* Adds settlement section with payment terms and totals
* @param doc XML document
* @param transactionElement Transaction element
* @param invoice Invoice data
*/
private addSettlementSection(doc: Document, transactionElement: Element, invoice: TInvoice): void {
// Create settlement element
const settlementElement = doc.createElement('ram:ApplicableHeaderTradeSettlement');
transactionElement.appendChild(settlementElement);
// Add currency
const currencyElement = doc.createElement('ram:InvoiceCurrencyCode');
currencyElement.textContent = invoice.currency;
settlementElement.appendChild(currencyElement);
// Add payment terms
const paymentTermsElement = doc.createElement('ram:SpecifiedTradePaymentTerms');
// Add due date
const dueDateElement = doc.createElement('ram:DueDateDateTime');
const dateStringElement = doc.createElement('udt:DateTimeString');
dateStringElement.setAttribute('format', '102'); // YYYYMMDD format
// Calculate due date
const dueDate = new Date(invoice.date);
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
dateStringElement.textContent = this.formatDateYYYYMMDD(dueDate.getTime());
dueDateElement.appendChild(dateStringElement);
paymentTermsElement.appendChild(dueDateElement);
settlementElement.appendChild(paymentTermsElement);
// Add totals
const monetarySummationElement = doc.createElement('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
// Calculate totals
let totalNetAmount = 0;
let totalTaxAmount = 0;
// Calculate from items
if (invoice.items) {
for (const item of invoice.items) {
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
const itemTaxAmount = itemNetAmount * (item.vatPercentage / 100);
totalNetAmount += itemNetAmount;
totalTaxAmount += itemTaxAmount;
}
}
const totalGrossAmount = totalNetAmount + totalTaxAmount;
// Add line total amount
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
lineTotalElement.textContent = totalNetAmount.toFixed(2);
monetarySummationElement.appendChild(lineTotalElement);
// Add tax total amount
const taxTotalElement = doc.createElement('ram:TaxTotalAmount');
taxTotalElement.textContent = totalTaxAmount.toFixed(2);
taxTotalElement.setAttribute('currencyID', invoice.currency);
monetarySummationElement.appendChild(taxTotalElement);
// Add grand total amount
const grandTotalElement = doc.createElement('ram:GrandTotalAmount');
grandTotalElement.textContent = totalGrossAmount.toFixed(2);
monetarySummationElement.appendChild(grandTotalElement);
// Add due payable amount
const duePayableElement = doc.createElement('ram:DuePayableAmount');
duePayableElement.textContent = totalGrossAmount.toFixed(2);
monetarySummationElement.appendChild(duePayableElement);
settlementElement.appendChild(monetarySummationElement);
}
/**
* Adds line items to the XML document
* @param doc XML document
* @param transactionElement Transaction element
* @param invoice Invoice data
*/
private addLineItems(doc: Document, transactionElement: Element, invoice: TInvoice): void {
// Add each line item
if (invoice.items) {
for (const item of invoice.items) {
// Create line item element
const lineItemElement = doc.createElement('ram:IncludedSupplyChainTradeLineItem');
// Add line ID
const lineIdElement = doc.createElement('ram:AssociatedDocumentLineDocument');
const lineIdValueElement = doc.createElement('ram:LineID');
lineIdValueElement.textContent = item.position.toString();
lineIdElement.appendChild(lineIdValueElement);
lineItemElement.appendChild(lineIdElement);
// Add product information
const productElement = doc.createElement('ram:SpecifiedTradeProduct');
// Add name
const nameElement = doc.createElement('ram:Name');
nameElement.textContent = item.name;
productElement.appendChild(nameElement);
// Add article number if available
if (item.articleNumber) {
const articleNumberElement = doc.createElement('ram:SellerAssignedID');
articleNumberElement.textContent = item.articleNumber;
productElement.appendChild(articleNumberElement);
}
lineItemElement.appendChild(productElement);
// Add agreement information (price)
const agreementElement = doc.createElement('ram:SpecifiedLineTradeAgreement');
const priceElement = doc.createElement('ram:NetPriceProductTradePrice');
const chargeAmountElement = doc.createElement('ram:ChargeAmount');
chargeAmountElement.textContent = item.unitNetPrice.toFixed(2);
priceElement.appendChild(chargeAmountElement);
agreementElement.appendChild(priceElement);
lineItemElement.appendChild(agreementElement);
// Add delivery information (quantity)
const deliveryElement = doc.createElement('ram:SpecifiedLineTradeDelivery');
const quantityElement = doc.createElement('ram:BilledQuantity');
quantityElement.textContent = item.unitQuantity.toString();
quantityElement.setAttribute('unitCode', item.unitType);
deliveryElement.appendChild(quantityElement);
lineItemElement.appendChild(deliveryElement);
// Add settlement information (tax)
const settlementElement = doc.createElement('ram:SpecifiedLineTradeSettlement');
// Add tax information
const taxElement = doc.createElement('ram:ApplicableTradeTax');
// Add tax type code
const taxTypeCodeElement = doc.createElement('ram:TypeCode');
taxTypeCodeElement.textContent = 'VAT';
taxElement.appendChild(taxTypeCodeElement);
// Add tax category code
const taxCategoryCodeElement = doc.createElement('ram:CategoryCode');
taxCategoryCodeElement.textContent = 'S';
taxElement.appendChild(taxCategoryCodeElement);
// Add tax rate
const taxRateElement = doc.createElement('ram:RateApplicablePercent');
taxRateElement.textContent = item.vatPercentage.toString();
taxElement.appendChild(taxRateElement);
settlementElement.appendChild(taxElement);
// Add monetary summation
const monetarySummationElement = doc.createElement('ram:SpecifiedLineTradeSettlementMonetarySummation');
// Calculate item total
const itemNetAmount = item.unitNetPrice * item.unitQuantity;
// Add line total amount
const lineTotalElement = doc.createElement('ram:LineTotalAmount');
lineTotalElement.textContent = itemNetAmount.toFixed(2);
monetarySummationElement.appendChild(lineTotalElement);
settlementElement.appendChild(monetarySummationElement);
lineItemElement.appendChild(settlementElement);
// Add line item to transaction
transactionElement.appendChild(lineItemElement);
}
}
}
/**
* Formats a date as YYYYMMDD
* @param timestamp Timestamp to format
* @returns Formatted date string
*/
private formatDateYYYYMMDD(timestamp: number): string {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}${month}${day}`;
}
}

View File

@ -0,0 +1,18 @@
import { CIIProfile, CII_PROFILE_IDS } from '../cii.types.js';
/**
* Factur-X specific constants and types
*/
// Factur-X profile IDs
export const FACTURX_PROFILE_IDS = {
MINIMUM: CII_PROFILE_IDS.FACTURX_MINIMUM,
BASIC: CII_PROFILE_IDS.FACTURX_BASIC,
EN16931: CII_PROFILE_IDS.FACTURX_EN16931
};
// Factur-X PDF attachment filename
export const FACTURX_ATTACHMENT_FILENAME = 'factur-x.xml';
// Factur-X PDF attachment description
export const FACTURX_ATTACHMENT_DESCRIPTION = 'Factur-X XML Invoice';

View File

@ -1,124 +1,35 @@
import { BaseValidator } from './base.validator.js';
import { ValidationLevel } from '../interfaces.js';
import type { ValidationResult, ValidationError } from '../interfaces.js';
import * as xpath from 'xpath';
import { DOMParser } from 'xmldom';
import { CIIBaseValidator } from '../cii.validator.js';
import { ValidationLevel } from '../../../interfaces/common.js';
import type { ValidationResult } from '../../../interfaces/common.js';
/**
* Validator for Factur-X/ZUGFeRD invoice format
* Validator for Factur-X invoice format
* Implements validation rules according to EN16931 and Factur-X specification
*/
export class FacturXValidator extends BaseValidator {
// XML namespaces for Factur-X/ZUGFeRD
private static NS_RSMT = 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100';
private static NS_RAM = 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100';
private static NS_UDT = 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100';
// XML document for processing
private xmlDoc: Document | null = null;
// Factur-X profile (BASIC, EN16931, EXTENDED, etc.)
private profile: string = '';
constructor(xml: string) {
super(xml);
try {
// Parse XML document
this.xmlDoc = new DOMParser().parseFromString(xml, 'application/xml');
// Determine Factur-X profile
this.detectProfile();
} catch (error) {
this.addError('FX-PARSE', `Failed to parse XML: ${error}`, '/');
}
}
export class FacturXValidator extends CIIBaseValidator {
/**
* Validates the Factur-X invoice against the specified level
* @param level Validation level
* @returns Validation result
*/
public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
// Reset errors
this.errors = [];
// Check if document was parsed successfully
if (!this.xmlDoc) {
return {
valid: false,
errors: this.errors,
level: level
};
}
// Perform validation based on level
let valid = true;
if (level === ValidationLevel.SYNTAX) {
valid = this.validateSchema();
} else if (level === ValidationLevel.SEMANTIC) {
valid = this.validateSchema() && this.validateStructure();
} else if (level === ValidationLevel.BUSINESS) {
valid = this.validateSchema() &&
this.validateStructure() &&
this.validateBusinessRules();
}
return {
valid,
errors: this.errors,
level
};
}
/**
* Validates XML against schema
* @returns True if schema validation passed
*/
protected validateSchema(): boolean {
// Basic schema validation (simplified for now)
if (!this.xmlDoc) return false;
// Check for root element
const root = this.xmlDoc.documentElement;
if (!root || root.nodeName !== 'rsm:CrossIndustryInvoice') {
this.addError('FX-SCHEMA-1', 'Root element must be rsm:CrossIndustryInvoice', '/');
return false;
}
// Check for required namespaces
if (!root.lookupNamespaceURI('rsm') || !root.lookupNamespaceURI('ram')) {
this.addError('FX-SCHEMA-2', 'Required namespaces rsm and ram must be declared', '/');
return false;
}
return true;
}
/**
* Validates structure of the XML document
* Validates structure of the Factur-X XML document
* @returns True if structure validation passed
*/
private validateStructure(): boolean {
if (!this.xmlDoc) return false;
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 = [
@ -126,197 +37,144 @@ export class FacturXValidator extends BaseValidator {
'ram:ApplicableHeaderTradeDelivery',
'ram:ApplicableHeaderTradeSettlement'
];
for (const subsection of tradeSubsections) {
if (!this.exists(`rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeTransaction/${subsection}`)) {
this.addError('FX-STRUCT-2', `Required subsection ${subsection} is missing`,
'/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeTransaction');
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.xmlDoc) return false;
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)
// 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;
}
/**
* Detects Factur-X profile from the XML
*/
private detectProfile(): void {
if (!this.xmlDoc) return;
// Look for profile identifier
const profileNode = xpath.select1(
'string(//rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID)',
this.xmlDoc
);
if (profileNode) {
const profileText = profileNode.toString();
if (profileText.includes('BASIC')) {
this.profile = 'BASIC';
} else if (profileText.includes('EN16931')) {
this.profile = 'EN16931';
} else if (profileText.includes('EXTENDED')) {
this.profile = 'EXTENDED';
} else if (profileText.includes('MINIMUM')) {
this.profile = 'MINIMUM';
}
}
}
/**
* Validates amount calculations in the invoice
* @returns True if amount validation passed
*/
private validateAmounts(): boolean {
if (!this.xmlDoc) return false;
if (!this.doc) return false;
try {
// Extract amounts
const totalAmount = this.getNumberValue(
const totalAmount = this.getNumber(
'//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount'
);
const paidAmount = this.getNumberValue(
const paidAmount = this.getNumber(
'//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TotalPrepaidAmount'
) || 0;
const dueAmount = this.getNumberValue(
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',
'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.xmlDoc) return false;
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',
'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.xmlDoc) return false;
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',
'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;
}
}
/**
* Helper method to check if a node exists
* @param xpathExpression XPath to check
* @returns True if node exists
*/
private exists(xpathExpression: string): boolean {
if (!this.xmlDoc) return false;
const nodes = xpath.select(xpathExpression, this.xmlDoc);
// Handle different return types from xpath.select()
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return nodes ? true : false;
}
/**
* Helper method to get a number value from XPath
* @param xpathExpression XPath to get number from
* @returns Number value or NaN if not found
*/
private getNumberValue(xpathExpression: string): number {
if (!this.xmlDoc) return NaN;
const node = xpath.select1(`string(${xpathExpression})`, this.xmlDoc);
return node ? parseFloat(node.toString()) : NaN;
}
}
}

View File

@ -1,52 +0,0 @@
import { BaseDecoder } from './base.decoder.js';
import { FacturXDecoder } from './facturx.decoder.js';
import { XInvoiceDecoder } from './xinvoice.decoder.js';
/**
* Factory class for creating the appropriate decoder based on XML format.
* Analyzes XML content and returns the best decoder for the given format.
*/
export class DecoderFactory {
/**
* Creates a decoder for the given XML content
*/
public static createDecoder(xmlString: string): BaseDecoder {
if (!xmlString) {
throw new Error('No XML string provided for decoder selection');
}
const format = DecoderFactory.detectFormat(xmlString);
switch (format) {
case 'XInvoice/UBL':
return new XInvoiceDecoder(xmlString);
case 'FacturX/ZUGFeRD':
default:
// Default to FacturX/ZUGFeRD decoder
return new FacturXDecoder(xmlString);
}
}
/**
* Detects the XML invoice format using string pattern matching
*/
private static detectFormat(xmlString: string): string {
// XInvoice/UBL format
if (xmlString.includes('oasis:names:specification:ubl') ||
xmlString.includes('Invoice xmlns') ||
xmlString.includes('xrechnung')) {
return 'XInvoice/UBL';
}
// ZUGFeRD/Factur-X (CII format)
if (xmlString.includes('CrossIndustryInvoice') ||
xmlString.includes('un/cefact') ||
xmlString.includes('rsm:')) {
return 'FacturX/ZUGFeRD';
}
// Default to FacturX/ZUGFeRD
return 'FacturX/ZUGFeRD';
}
}

View File

@ -0,0 +1,46 @@
import { BaseDecoder } from '../base/base.decoder.js';
import { InvoiceFormat } from '../../interfaces/common.js';
import { FormatDetector } from '../utils/format.detector.js';
// Import specific decoders
import { XRechnungDecoder } from '../ubl/xrechnung/xrechnung.decoder.js';
import { FacturXDecoder } from '../cii/facturx/facturx.decoder.js';
// import { ZUGFeRDDecoder } from '../cii/zugferd/zugferd.decoder.js';
/**
* Factory to create the appropriate decoder based on the XML format
*/
export class DecoderFactory {
/**
* Creates a decoder for the specified XML content
* @param xml XML content to decode
* @returns Appropriate decoder instance
*/
public static createDecoder(xml: string): BaseDecoder {
const format = FormatDetector.detectFormat(xml);
switch (format) {
case InvoiceFormat.UBL:
case InvoiceFormat.XRECHNUNG:
return new XRechnungDecoder(xml);
case InvoiceFormat.CII:
// For now, use Factur-X decoder for generic CII
return new FacturXDecoder(xml);
case InvoiceFormat.ZUGFERD:
// For now, use Factur-X decoder for ZUGFeRD
return new FacturXDecoder(xml);
case InvoiceFormat.FACTURX:
return new FacturXDecoder(xml);
case InvoiceFormat.FATTURAPA:
// return new FatturaPADecoder(xml);
throw new Error('FatturaPA decoder not yet implemented');
default:
throw new Error(`Unsupported invoice format: ${format}`);
}
}
}

View File

@ -0,0 +1,47 @@
import { BaseEncoder } from '../base/base.encoder.js';
import { InvoiceFormat } from '../../interfaces/common.js';
import type { ExportFormat } from '../../interfaces/common.js';
// Import specific encoders
import { XRechnungEncoder } from '../ubl/xrechnung/xrechnung.encoder.js';
import { FacturXEncoder } from '../cii/facturx/facturx.encoder.js';
// import { ZUGFeRDEncoder } from '../cii/zugferd/zugferd.encoder.js';
/**
* Factory to create the appropriate encoder based on the target format
*/
export class EncoderFactory {
/**
* Creates an encoder for the specified format
* @param format Target format for encoding
* @returns Appropriate encoder instance
*/
public static createEncoder(format: ExportFormat | InvoiceFormat): BaseEncoder {
switch (format.toLowerCase()) {
case InvoiceFormat.UBL:
case 'ubl':
// return new UBLEncoder();
throw new Error('UBL encoder not yet implemented');
case InvoiceFormat.XRECHNUNG:
case 'xrechnung':
return new XRechnungEncoder();
case InvoiceFormat.CII:
// For now, use Factur-X encoder for generic CII
return new FacturXEncoder();
case InvoiceFormat.ZUGFERD:
case 'zugferd':
// For now, use Factur-X encoder for ZUGFeRD
return new FacturXEncoder();
case InvoiceFormat.FACTURX:
case 'facturx':
return new FacturXEncoder();
default:
throw new Error(`Unsupported invoice format for encoding: ${format}`);
}
}
}

View File

@ -0,0 +1,51 @@
import { BaseValidator } from '../base/base.validator.js';
import { InvoiceFormat } from '../../interfaces/common.js';
import { FormatDetector } from '../utils/format.detector.js';
// Import specific validators
// import { UBLValidator } from '../ubl/ubl.validator.js';
// import { XRechnungValidator } from '../ubl/xrechnung/xrechnung.validator.js';
import { FacturXValidator } from '../cii/facturx/facturx.validator.js';
// import { ZUGFeRDValidator } from '../cii/zugferd/zugferd.validator.js';
/**
* Factory to create the appropriate validator based on the XML format
*/
export class ValidatorFactory {
/**
* Creates a validator for the specified XML content
* @param xml XML content to validate
* @returns Appropriate validator instance
*/
public static createValidator(xml: string): BaseValidator {
const format = FormatDetector.detectFormat(xml);
switch (format) {
case InvoiceFormat.UBL:
// return new UBLValidator(xml);
throw new Error('UBL validator not yet implemented');
case InvoiceFormat.XRECHNUNG:
// return new XRechnungValidator(xml);
throw new Error('XRechnung validator not yet implemented');
case InvoiceFormat.CII:
// For now, use Factur-X validator for generic CII
return new FacturXValidator(xml);
case InvoiceFormat.ZUGFERD:
// For now, use Factur-X validator for ZUGFeRD
return new FacturXValidator(xml);
case InvoiceFormat.FACTURX:
return new FacturXValidator(xml);
case InvoiceFormat.FATTURAPA:
// return new FatturaPAValidator(xml);
throw new Error('FatturaPA validator not yet implemented');
default:
throw new Error(`Unsupported invoice format: ${format}`);
}
}
}

View File

@ -1,192 +0,0 @@
import * as plugins from '../plugins.js';
import * as xmldom from 'xmldom';
import { BaseDecoder } from './base.decoder.js';
/**
* A decoder for Factur-X/ZUGFeRD XML format (based on UN/CEFACT CII).
* Converts XML into structured ILetter with invoice data.
*/
export class FacturXDecoder extends BaseDecoder {
private xmlDoc: Document | null = null;
constructor(xmlString: string) {
super(xmlString);
// Parse XML to DOM for easier element extraction
try {
const parser = new xmldom.DOMParser();
this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml');
} catch (error) {
console.error('Error parsing Factur-X XML:', error);
}
}
/**
* Extracts text from the first element matching the tag name
*/
private getElementText(tagName: string): string {
if (!this.xmlDoc) {
return '';
}
try {
// Basic handling for namespaced tags
let namespace = '';
let localName = tagName;
if (tagName.includes(':')) {
const parts = tagName.split(':');
namespace = parts[0];
localName = parts[1];
}
// Find all elements with this name
const elements = this.xmlDoc.getElementsByTagName(tagName);
if (elements.length > 0) {
return elements[0].textContent || '';
}
// Try with just the local name if we didn't find it with the namespace
if (namespace) {
const elements = this.xmlDoc.getElementsByTagName(localName);
if (elements.length > 0) {
return elements[0].textContent || '';
}
}
return '';
} catch (error) {
console.error(`Error extracting element ${tagName}:`, error);
return '';
}
}
/**
* Converts Factur-X/ZUGFeRD XML to a structured letter object
*/
public async getLetterData(): Promise<plugins.tsclass.business.ILetter> {
try {
// Extract invoice ID
let invoiceId = this.getElementText('ram:ID');
if (!invoiceId) {
// Try alternative locations
invoiceId = this.getElementText('rsm:ExchangedDocument ram:ID') || 'Unknown';
}
// Extract seller name
let sellerName = this.getElementText('ram:Name');
if (!sellerName) {
sellerName = this.getElementText('ram:SellerTradeParty ram:Name') || 'Unknown Seller';
}
// Extract buyer name
let buyerName = '';
// Try to find BuyerTradeParty Name specifically
if (this.xmlDoc) {
const buyerParties = this.xmlDoc.getElementsByTagName('ram:BuyerTradeParty');
if (buyerParties.length > 0) {
const nameElements = buyerParties[0].getElementsByTagName('ram:Name');
if (nameElements.length > 0) {
buyerName = nameElements[0].textContent || '';
}
}
}
if (!buyerName) {
buyerName = 'Unknown Buyer';
}
// Create seller
const seller: plugins.tsclass.business.IContact = {
name: sellerName,
type: 'company',
description: sellerName,
address: {
streetName: this.getElementText('ram:LineOne') || 'Unknown',
houseNumber: '0', // Required by IAddress interface
city: this.getElementText('ram:CityName') || 'Unknown',
country: this.getElementText('ram:CountryID') || 'Unknown',
postalCode: this.getElementText('ram:PostcodeCode') || 'Unknown',
},
};
// Create buyer
const buyer: plugins.tsclass.business.IContact = {
name: buyerName,
type: 'company',
description: buyerName,
address: {
streetName: 'Unknown',
houseNumber: '0',
city: 'Unknown',
country: 'Unknown',
postalCode: 'Unknown',
},
};
// Extract invoice type
let invoiceType = 'debitnote';
const typeCode = this.getElementText('ram:TypeCode');
if (typeCode === '381') {
invoiceType = 'creditnote';
}
// Create invoice data
const invoiceData: plugins.tsclass.finance.IInvoice = {
id: invoiceId,
status: null,
type: invoiceType as 'debitnote' | 'creditnote',
billedBy: seller,
billedTo: buyer,
deliveryDate: Date.now(),
dueInDays: 30,
periodOfPerformance: null,
printResult: null,
currency: (this.getElementText('ram:InvoiceCurrencyCode') || 'EUR') as plugins.tsclass.finance.TCurrency,
notes: [],
items: [
{
name: 'Item from Factur-X XML',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
],
reverseCharge: false,
};
// Return a letter
return {
versionInfo: {
type: 'draft',
version: '1.0.0',
},
type: 'invoice',
date: Date.now(),
subject: `Invoice: ${invoiceId}`,
from: seller,
to: buyer,
content: {
invoiceData: invoiceData,
textData: null,
timesheetData: null,
contractData: null,
},
needsCoverSheet: false,
objectActions: [],
pdf: null,
incidenceId: null,
language: null,
legalContact: null,
logoUrl: null,
pdfAttachments: null,
accentColor: null,
};
} catch (error) {
console.error('Error converting Factur-X XML to letter data:', error);
return this.createDefaultLetter();
}
}
}

View File

@ -1,345 +0,0 @@
import * as plugins from '../plugins.js';
/**
* A class to convert a given ILetter with invoice data
* into a Factur-X compliant XML (also compatible with ZUGFeRD and EN16931).
*
* Factur-X is the French implementation of the European e-invoicing standard EN16931,
* which is also implemented in Germany as ZUGFeRD. Both formats are based on
* UN/CEFACT Cross Industry Invoice (CII) XML schemas.
*/
export class FacturXEncoder {
constructor() {}
/**
* Alias for createFacturXXml to maintain backward compatibility
*/
public createZugferdXml(letterArg: plugins.tsclass.business.ILetter): string {
return this.createFacturXXml(letterArg);
}
/**
* Creates a Factur-X compliant XML based on the provided letter data.
* This XML is also compliant with ZUGFeRD and EN16931 standards.
*/
public createFacturXXml(letterArg: plugins.tsclass.business.ILetter): string {
// 1) Get your "SmartXml" or "xmlbuilder2" instance
const smartxmlInstance = new plugins.smartxml.SmartXml();
if (!letterArg?.content?.invoiceData) {
throw new Error('Letter does not contain invoice data.');
}
const invoice: plugins.tsclass.finance.IInvoice = letterArg.content.invoiceData;
const billedBy: plugins.tsclass.business.IContact = invoice.billedBy;
const billedTo: plugins.tsclass.business.IContact = invoice.billedTo;
// 2) Start building the document
const doc = smartxmlInstance
.create({ version: '1.0', encoding: 'UTF-8' })
.ele('rsm:CrossIndustryInvoice', {
'xmlns:rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
'xmlns:udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
'xmlns:qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
'xmlns:ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
});
// 3) Exchanged Document Context
const docContext = doc.ele('rsm:ExchangedDocumentContext');
// Add test indicator
docContext.ele('ram:TestIndicator')
.ele('udt:Indicator')
.txt(this.isDraft(letterArg) ? 'true' : 'false')
.up()
.up();
// Add Factur-X profile information
// EN16931 profile is compliant with both Factur-X and ZUGFeRD
docContext.ele('ram:GuidelineSpecifiedDocumentContextParameter')
.ele('ram:ID')
.txt('urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931')
.up()
.up();
docContext.up(); // </rsm:ExchangedDocumentContext>
// 4) Exchanged Document (Invoice Header Info)
const exchangedDoc = doc.ele('rsm:ExchangedDocument');
// Invoice ID
exchangedDoc.ele('ram:ID').txt(invoice.id).up();
// Document type code
// 380 = commercial invoice, 381 = credit note
const documentTypeCode = invoice.type === 'creditnote' ? '381' : '380';
exchangedDoc.ele('ram:TypeCode').txt(documentTypeCode).up();
// Issue date
exchangedDoc
.ele('ram:IssueDateTime')
.ele('udt:DateTimeString', { format: '102' })
// Format 'YYYYMMDD' as per Factur-X specification
.txt(this.formatDate(letterArg.date))
.up()
.up();
// Document name - Factur-X recommended field
const documentName = invoice.type === 'creditnote' ? 'CREDIT NOTE' : 'INVOICE';
exchangedDoc.ele('ram:Name').txt(documentName).up();
// Optional: Add language indicator (recommended for Factur-X)
// Use document language if specified, default to 'en'
const languageCode = letterArg.language?.toUpperCase() || 'EN';
exchangedDoc
.ele('ram:IncludedNote')
.ele('ram:Content').txt('Invoice created with Factur-X compliant software').up()
.ele('ram:SubjectCode').txt('REG').up() // REG = regulatory information
.up();
exchangedDoc.up(); // </rsm:ExchangedDocument>
// 5) Supply Chain Trade Transaction
const supplyChainEle = doc.ele('rsm:SupplyChainTradeTransaction');
// 5.1) Included Supply Chain Trade Line Items
invoice.items.forEach((item) => {
const lineItemEle = supplyChainEle.ele('ram:IncludedSupplyChainTradeLineItem');
lineItemEle.ele('ram:SpecifiedTradeProduct')
.ele('ram:Name')
.txt(item.name)
.up()
.up(); // </ram:SpecifiedTradeProduct>
lineItemEle.ele('ram:SpecifiedLineTradeAgreement')
.ele('ram:GrossPriceProductTradePrice')
.ele('ram:ChargeAmount')
.txt(item.unitNetPrice.toFixed(2))
.up()
.up()
.up(); // </ram:SpecifiedLineTradeAgreement>
lineItemEle.ele('ram:SpecifiedLineTradeDelivery')
.ele('ram:BilledQuantity')
.txt(item.unitQuantity.toString())
.up()
.up(); // </ram:SpecifiedLineTradeDelivery>
lineItemEle.ele('ram:SpecifiedLineTradeSettlement')
.ele('ram:ApplicableTradeTax')
.ele('ram:RateApplicablePercent')
.txt(item.vatPercentage.toFixed(2))
.up()
.up()
.ele('ram:SpecifiedTradeSettlementLineMonetarySummation')
.ele('ram:LineTotalAmount')
.txt(
(
item.unitQuantity *
item.unitNetPrice *
(1 + item.vatPercentage / 100)
).toFixed(2)
)
.up()
.up()
.up(); // </ram:SpecifiedLineTradeSettlement>
});
// 5.2) Applicable Header Trade Agreement
const headerTradeAgreementEle = supplyChainEle.ele('ram:ApplicableHeaderTradeAgreement');
// Seller
const sellerPartyEle = headerTradeAgreementEle.ele('ram:SellerTradeParty');
sellerPartyEle.ele('ram:Name').txt(billedBy.name).up();
// Example: If it's a company, put company name, etc.
const sellerAddressEle = sellerPartyEle.ele('ram:PostalTradeAddress');
sellerAddressEle.ele('ram:PostcodeCode').txt(billedBy.address.postalCode).up();
sellerAddressEle.ele('ram:LineOne').txt(billedBy.address.streetName).up();
sellerAddressEle.ele('ram:CityName').txt(billedBy.address.city).up();
// Typically you'd include 'ram:CountryID' with ISO2 code, e.g. "DE"
sellerAddressEle.up(); // </ram:PostalTradeAddress>
sellerPartyEle.up(); // </ram:SellerTradeParty>
// Buyer
const buyerPartyEle = headerTradeAgreementEle.ele('ram:BuyerTradeParty');
buyerPartyEle.ele('ram:Name').txt(billedTo.name).up();
const buyerAddressEle = buyerPartyEle.ele('ram:PostalTradeAddress');
buyerAddressEle.ele('ram:PostcodeCode').txt(billedTo.address.postalCode).up();
buyerAddressEle.ele('ram:LineOne').txt(billedTo.address.streetName).up();
buyerAddressEle.ele('ram:CityName').txt(billedTo.address.city).up();
buyerAddressEle.up(); // </ram:PostalTradeAddress>
buyerPartyEle.up(); // </ram:BuyerTradeParty>
headerTradeAgreementEle.up(); // </ram:ApplicableHeaderTradeAgreement>
// 5.3) Applicable Header Trade Delivery
const headerTradeDeliveryEle = supplyChainEle.ele('ram:ApplicableHeaderTradeDelivery');
const actualDeliveryEle = headerTradeDeliveryEle.ele('ram:ActualDeliverySupplyChainEvent');
const occurrenceEle = actualDeliveryEle.ele('ram:OccurrenceDateTime')
.ele('udt:DateTimeString', { format: '102' });
const deliveryDate = invoice.deliveryDate || letterArg.date;
occurrenceEle.txt(this.formatDate(deliveryDate)).up();
actualDeliveryEle.up(); // </ram:ActualDeliverySupplyChainEvent>
headerTradeDeliveryEle.up(); // </ram:ApplicableHeaderTradeDelivery>
// 5.4) Applicable Header Trade Settlement
const headerTradeSettlementEle = supplyChainEle.ele('ram:ApplicableHeaderTradeSettlement');
// Tax currency code, doc currency code, etc.
headerTradeSettlementEle.ele('ram:InvoiceCurrencyCode').txt(invoice.currency).up();
// Example single tax breakdown
const tradeTaxEle = headerTradeSettlementEle.ele('ram:ApplicableTradeTax');
tradeTaxEle.ele('ram:TypeCode').txt('VAT').up();
tradeTaxEle.ele('ram:CalculatedAmount').txt(this.sumAllVat(invoice).toFixed(2)).up();
tradeTaxEle
.ele('ram:RateApplicablePercent')
.txt(this.extractMainVatRate(invoice.items).toFixed(2))
.up();
tradeTaxEle.up(); // </ram:ApplicableTradeTax>
// Payment Terms
const paymentTermsEle = headerTradeSettlementEle.ele('ram:SpecifiedTradePaymentTerms');
// Payment description
paymentTermsEle.ele('ram:Description').txt(`Payment due in ${invoice.dueInDays} days.`).up();
// Due date calculation
const dueDate = new Date(letterArg.date);
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
// Add due date as per Factur-X spec
paymentTermsEle
.ele('ram:DueDateDateTime')
.ele('udt:DateTimeString', { format: '102' })
.txt(this.formatDate(dueDate.getTime()))
.up()
.up();
// Add payment means if available
if (invoice.billedBy.sepaConnection) {
// Add SEPA information as per Factur-X standard
const paymentMeans = headerTradeSettlementEle.ele('ram:SpecifiedTradeSettlementPaymentMeans');
paymentMeans.ele('ram:TypeCode').txt('58').up(); // 58 = SEPA credit transfer
// Payment reference (for bank statement reconciliation)
paymentMeans.ele('ram:Information').txt(`Reference: ${invoice.id}`).up();
// Payee account (IBAN)
if (invoice.billedBy.sepaConnection.iban) {
const payeeAccount = paymentMeans.ele('ram:PayeePartyCreditorFinancialAccount');
payeeAccount.ele('ram:IBANID').txt(invoice.billedBy.sepaConnection.iban).up();
payeeAccount.up();
}
// Bank BIC
if (invoice.billedBy.sepaConnection.bic) {
const payeeBank = paymentMeans.ele('ram:PayeeSpecifiedCreditorFinancialInstitution');
payeeBank.ele('ram:BICID').txt(invoice.billedBy.sepaConnection.bic).up();
payeeBank.up();
}
paymentMeans.up();
}
paymentTermsEle.up(); // </ram:SpecifiedTradePaymentTerms>
// Monetary Summation
const monetarySummationEle = headerTradeSettlementEle.ele('ram:SpecifiedTradeSettlementHeaderMonetarySummation');
monetarySummationEle
.ele('ram:LineTotalAmount')
.txt(this.calcLineTotalNet(invoice).toFixed(2))
.up();
monetarySummationEle
.ele('ram:TaxTotalAmount')
.txt(this.sumAllVat(invoice).toFixed(2))
.up();
monetarySummationEle
.ele('ram:GrandTotalAmount')
.txt(this.calcGrandTotal(invoice).toFixed(2))
.up();
monetarySummationEle.up(); // </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
headerTradeSettlementEle.up(); // </ram:ApplicableHeaderTradeSettlement>
supplyChainEle.up(); // </rsm:SupplyChainTradeTransaction>
doc.up(); // </rsm:CrossIndustryInvoice>
// 6) Return the final XML string
return doc.end({ prettyPrint: true });
}
/**
* Helper: Determine if the letter is in draft or final.
*/
private isDraft(letterArg: plugins.tsclass.business.ILetter): boolean {
return letterArg.versionInfo?.type === 'draft';
}
/**
* Helper: Format date to certain patterns (very minimal example).
* e.g. 'yyyyMMdd' => '20231231'
*/
private formatDate(timestampMs: number): string {
const date = new Date(timestampMs);
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}${mm}${dd}`;
}
/**
* Helper: Map your custom 'unitType' to an ISO code or similar.
*/
private mapUnitType(unitType: string): string {
switch (unitType.toLowerCase()) {
case 'hour':
return 'HUR';
case 'piece':
return 'C62';
default:
return 'C62'; // fallback
}
}
/**
* Example: Sum all VAT amounts from items.
*/
private sumAllVat(invoice: plugins.tsclass.finance.IInvoice): number {
return invoice.items.reduce((acc, item) => {
const net = item.unitNetPrice * item.unitQuantity;
const vat = net * (item.vatPercentage / 100);
return acc + vat;
}, 0);
}
/**
* Example: Extract main (or highest) VAT rate from items as representative.
* In reality, you might list multiple 'ApplicableTradeTax' blocks by group.
*/
private extractMainVatRate(items: plugins.tsclass.finance.IInvoiceItem[]): number {
let max = 0;
items.forEach((item) => {
if (item.vatPercentage > max) max = item.vatPercentage;
});
return max;
}
/**
* Example: Sum net amounts (without VAT).
*/
private calcLineTotalNet(invoice: plugins.tsclass.finance.IInvoice): number {
return invoice.items.reduce((acc, item) => {
const net = item.unitNetPrice * item.unitQuantity;
return acc + net;
}, 0);
}
/**
* Example: net + VAT = grand total
*/
private calcGrandTotal(invoice: plugins.tsclass.finance.IInvoice): number {
const net = this.calcLineTotalNet(invoice);
const vat = this.sumAllVat(invoice);
return net + vat;
}
}

View File

@ -0,0 +1,80 @@
import { PDFDocument, AFRelationship } from 'pdf-lib';
import type { IPdf } from '../../interfaces/common.js';
/**
* Class for embedding XML into PDF files
*/
export class PDFEmbedder {
/**
* Embeds XML into a PDF
* @param pdfBuffer PDF buffer
* @param xmlContent XML content to embed
* @param filename Filename for the embedded XML
* @param description Description for the embedded XML
* @returns Modified PDF buffer
*/
public async embedXml(
pdfBuffer: Uint8Array | Buffer,
xmlContent: string,
filename: string = 'invoice.xml',
description: string = 'XML Invoice'
): Promise<Uint8Array> {
try {
// Load the PDF
const pdfDoc = await PDFDocument.load(pdfBuffer);
// Convert the XML string to a Uint8Array
const xmlBuffer = new TextEncoder().encode(xmlContent);
// Make sure filename is lowercase (as required by documentation)
filename = filename.toLowerCase();
// Use pdf-lib's .attach() to embed the XML
pdfDoc.attach(xmlBuffer, filename, {
mimeType: 'text/xml',
description: description,
creationDate: new Date(),
modificationDate: new Date(),
afRelationship: AFRelationship.Alternative,
});
// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.save();
return modifiedPdfBytes;
} catch (error) {
console.error('Error embedding XML into PDF:', error);
throw error;
}
}
/**
* Creates an IPdf object with embedded XML
* @param pdfBuffer PDF buffer
* @param xmlContent XML content to embed
* @param filename Filename for the embedded XML
* @param description Description for the embedded XML
* @param pdfName Name for the PDF
* @param pdfId ID for the PDF
* @returns IPdf object with embedded XML
*/
public async createPdfWithXml(
pdfBuffer: Uint8Array | Buffer,
xmlContent: string,
filename: string = 'invoice.xml',
description: string = 'XML Invoice',
pdfName: string = 'invoice.pdf',
pdfId: string = `invoice-${Date.now()}`
): Promise<IPdf> {
const modifiedPdfBytes = await this.embedXml(pdfBuffer, xmlContent, filename, description);
return {
name: pdfName,
id: pdfId,
metadata: {
textExtraction: ''
},
buffer: modifiedPdfBytes
};
}
}

View File

@ -0,0 +1,107 @@
import { PDFDocument, PDFDict, PDFName, PDFRawStream, PDFArray, PDFString } from 'pdf-lib';
import * as pako from 'pako';
/**
* Class for extracting XML from PDF files
*/
export class PDFExtractor {
/**
* Extracts XML from a PDF buffer
* @param pdfBuffer PDF buffer
* @returns XML content or null if not found
*/
public async extractXml(pdfBuffer: Uint8Array | Buffer): Promise<string | null> {
try {
const pdfDoc = await PDFDocument.load(pdfBuffer);
// Get the document's metadata dictionary
const namesDictObj = pdfDoc.catalog.lookup(PDFName.of('Names'));
if (!(namesDictObj instanceof PDFDict)) {
console.warn('No Names dictionary found in PDF! This PDF does not contain embedded files.');
return null;
}
const embeddedFilesDictObj = namesDictObj.lookup(PDFName.of('EmbeddedFiles'));
if (!(embeddedFilesDictObj instanceof PDFDict)) {
console.warn('No EmbeddedFiles dictionary found! This PDF does not contain embedded files.');
return null;
}
const filesSpecObj = embeddedFilesDictObj.lookup(PDFName.of('Names'));
if (!(filesSpecObj instanceof PDFArray)) {
console.warn('No files specified in EmbeddedFiles dictionary!');
return null;
}
// Try to find an XML file in the embedded files
let xmlFile: PDFRawStream | undefined;
let xmlFileName: string | undefined;
for (let i = 0; i < filesSpecObj.size(); i += 2) {
const fileNameObj = filesSpecObj.lookup(i);
const fileSpecObj = filesSpecObj.lookup(i + 1);
if (!(fileNameObj instanceof PDFString)) {
continue;
}
if (!(fileSpecObj instanceof PDFDict)) {
continue;
}
// Get the filename as string
const fileName = fileNameObj.toString();
// Check if it's an XML file (checking both extension and known standard filenames)
if (fileName.toLowerCase().includes('.xml') ||
fileName.toLowerCase().includes('factur-x') ||
fileName.toLowerCase().includes('zugferd') ||
fileName.toLowerCase().includes('xrechnung')) {
const efDictObj = fileSpecObj.lookup(PDFName.of('EF'));
if (!(efDictObj instanceof PDFDict)) {
continue;
}
const maybeStream = efDictObj.lookup(PDFName.of('F'));
if (maybeStream instanceof PDFRawStream) {
// Found an XML file - save it
xmlFile = maybeStream;
xmlFileName = fileName;
break;
}
}
}
// If no XML file was found, return null
if (!xmlFile) {
console.warn('No embedded XML file found in the PDF!');
return null;
}
// Decompress and decode the XML content
try {
const xmlCompressedBytes = xmlFile.getContents().buffer;
const xmlBytes = pako.inflate(xmlCompressedBytes);
const xmlContent = new TextDecoder('utf-8').decode(xmlBytes);
console.log(`Successfully extracted XML from PDF file. File name: ${xmlFileName}`);
return xmlContent;
} catch (decompressError) {
// Try without decompression
console.log('Decompression failed, trying without decompression...');
try {
const xmlBytes = xmlFile.getContents();
const xmlContent = new TextDecoder('utf-8').decode(xmlBytes);
console.log(`Successfully extracted uncompressed XML from PDF file. File name: ${xmlFileName}`);
return xmlContent;
} catch (decodeError) {
console.error('Error decoding XML content:', decodeError);
return null;
}
}
} catch (error) {
console.error('Error extracting or parsing embedded XML from PDF:', error);
return null;
}
}
}

View File

@ -1,382 +0,0 @@
import { BaseValidator } from './base.validator.js';
import { ValidationLevel } from '../interfaces.js';
import type { ValidationResult, ValidationError } from '../interfaces.js';
import * as xpath from 'xpath';
import { DOMParser } from 'xmldom';
/**
* Validator for UBL (Universal Business Language) invoice format
* Implements validation rules according to EN16931 and UBL 2.1 specification
*/
export class UBLValidator extends BaseValidator {
// XML namespaces for UBL
private static NS_INVOICE = 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2';
private static NS_CAC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2';
private static NS_CBC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2';
// XML document for processing
private xmlDoc: Document | null = null;
// UBL profile or customization ID
private customizationId: string = '';
constructor(xml: string) {
super(xml);
try {
// Parse XML document
this.xmlDoc = new DOMParser().parseFromString(xml, 'application/xml');
// Determine UBL customization ID (e.g. EN16931, XRechnung)
this.detectCustomizationId();
} catch (error) {
this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/');
}
}
/**
* Validates the UBL invoice against the specified level
* @param level Validation level
* @returns Validation result
*/
public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
// Reset errors
this.errors = [];
// Check if document was parsed successfully
if (!this.xmlDoc) {
return {
valid: false,
errors: this.errors,
level: level
};
}
// Perform validation based on level
let valid = true;
if (level === ValidationLevel.SYNTAX) {
valid = this.validateSchema();
} else if (level === ValidationLevel.SEMANTIC) {
valid = this.validateSchema() && this.validateStructure();
} else if (level === ValidationLevel.BUSINESS) {
valid = this.validateSchema() &&
this.validateStructure() &&
this.validateBusinessRules();
}
return {
valid,
errors: this.errors,
level
};
}
/**
* Validates XML against schema
* @returns True if schema validation passed
*/
protected validateSchema(): boolean {
// Basic schema validation (simplified for now)
if (!this.xmlDoc) return false;
// Check for root element
const root = this.xmlDoc.documentElement;
if (!root || (root.nodeName !== 'Invoice' && root.nodeName !== 'CreditNote')) {
this.addError('UBL-SCHEMA-1', 'Root element must be Invoice or CreditNote', '/');
return false;
}
// Check for required namespaces
if (!root.lookupNamespaceURI('cac') || !root.lookupNamespaceURI('cbc')) {
this.addError('UBL-SCHEMA-2', 'Required namespaces cac and cbc must be declared', '/');
return false;
}
return true;
}
/**
* Validates structure of the XML document
* @returns True if structure validation passed
*/
private validateStructure(): boolean {
if (!this.xmlDoc) return false;
let valid = true;
// Check for required main sections
const sections = [
'cbc:ID',
'cbc:IssueDate',
'cac:AccountingSupplierParty',
'cac:AccountingCustomerParty',
'cac:LegalMonetaryTotal'
];
for (const section of sections) {
if (!this.exists(`/${this.getRootNodeName()}/${section}`)) {
this.addError('UBL-STRUCT-1', `Required section ${section} is missing`, `/${this.getRootNodeName()}`);
valid = false;
}
}
// Check for TaxTotal section
if (this.exists(`/${this.getRootNodeName()}/cac:TaxTotal`)) {
const taxSubsections = [
'cbc:TaxAmount',
'cac:TaxSubtotal'
];
for (const subsection of taxSubsections) {
if (!this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/${subsection}`)) {
this.addError('UBL-STRUCT-2', `Required subsection ${subsection} is missing`,
`/${this.getRootNodeName()}/cac:TaxTotal`);
valid = false;
}
}
}
return valid;
}
/**
* Validates business rules
* @returns True if business rule validation passed
*/
protected validateBusinessRules(): boolean {
if (!this.xmlDoc) 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 where the VAT category code is "Standard rated"
// shall contain the Seller VAT Identifier or the Seller tax representative VAT identifier
valid = this.validateSellerVatIdentifier() && valid;
// XRechnung specific rules when customization ID matches
if (this.isXRechnung()) {
valid = this.validateXRechnungRules() && valid;
}
return valid;
}
/**
* Gets the root node name (Invoice or CreditNote)
* @returns Root node name
*/
private getRootNodeName(): string {
if (!this.xmlDoc || !this.xmlDoc.documentElement) return 'Invoice';
return this.xmlDoc.documentElement.nodeName;
}
/**
* Detects UBL customization ID from the XML
*/
private detectCustomizationId(): void {
if (!this.xmlDoc) return;
// Look for customization ID
const customizationNode = xpath.select1(
`string(/${this.getRootNodeName()}/cbc:CustomizationID)`,
this.xmlDoc
);
if (customizationNode) {
this.customizationId = customizationNode.toString();
}
}
/**
* Checks if invoice is an XRechnung
* @returns True if XRechnung customization ID is present
*/
private isXRechnung(): boolean {
return this.customizationId.includes('xrechnung') ||
this.customizationId.includes('XRechnung');
}
/**
* Validates amount calculations in the invoice
* @returns True if amount validation passed
*/
private validateAmounts(): boolean {
if (!this.xmlDoc) return false;
try {
// Extract amounts
const totalAmount = this.getNumberValue(
`/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount`
);
const paidAmount = this.getNumberValue(
`/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:PrepaidAmount`
) || 0;
const dueAmount = this.getNumberValue(
`/${this.getRootNodeName()}/cac:LegalMonetaryTotal/cbc:PayableAmount`
);
// 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})`,
`/${this.getRootNodeName()}/cac:LegalMonetaryTotal`
);
return false;
}
return true;
} catch (error) {
this.addError('UBL-AMOUNT', `Error validating amounts: ${error}`, '/');
return false;
}
}
/**
* Validates mutually exclusive fields
* @returns True if validation passed
*/
private validateMutuallyExclusiveFields(): boolean {
if (!this.xmlDoc) return false;
try {
// Check for VAT point date and code (BR-CO-3)
const vatPointDate = this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:TaxPointDate`);
const vatPointDateCode = this.exists(`/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReasonCode`);
if (vatPointDate && vatPointDateCode) {
this.addError(
'BR-CO-3',
'Value added tax point date and Value added tax point date code are mutually exclusive',
`/${this.getRootNodeName()}/cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory`
);
return false;
}
return true;
} catch (error) {
this.addError('UBL-MUTUAL', `Error validating mutually exclusive fields: ${error}`, '/');
return false;
}
}
/**
* Validates seller VAT identifier requirements
* @returns True if validation passed
*/
private validateSellerVatIdentifier(): boolean {
if (!this.xmlDoc) return false;
try {
// Check if there are any standard rated line items
const standardRatedItems = this.exists(
`/${this.getRootNodeName()}/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:ID[text()="S"]`
);
if (standardRatedItems) {
// Check for seller VAT identifier
const sellerVatId = this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID`);
const sellerTaxRepId = this.exists(`/${this.getRootNodeName()}/cac:TaxRepresentativeParty/cac:PartyTaxScheme/cbc:CompanyID`);
if (!sellerVatId && !sellerTaxRepId) {
this.addError(
'BR-S-1',
'An Invoice with standard rated items must contain the Seller VAT Identifier or Tax representative VAT identifier',
`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
);
return false;
}
}
return true;
} catch (error) {
this.addError('UBL-VAT', `Error validating seller VAT identifier: ${error}`, '/');
return false;
}
}
/**
* Validates XRechnung specific rules
* @returns True if validation passed
*/
private validateXRechnungRules(): boolean {
if (!this.xmlDoc) return false;
let valid = true;
try {
// BR-DE-1: Buyer reference must be present for German VAT compliance
if (!this.exists(`/${this.getRootNodeName()}/cbc:BuyerReference`)) {
this.addError(
'BR-DE-1',
'BuyerReference is mandatory for XRechnung',
`/${this.getRootNodeName()}`
);
valid = false;
}
// BR-DE-15: Contact information must be present
if (!this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cac:Contact`)) {
this.addError(
'BR-DE-15',
'Supplier contact information is mandatory for XRechnung',
`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
);
valid = false;
}
// BR-DE-16: Electronic address identifier scheme (e.g. PEPPOL) must be present
if (!this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID`) ||
!this.exists(`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID`)) {
this.addError(
'BR-DE-16',
'Supplier electronic address with scheme identifier is mandatory for XRechnung',
`/${this.getRootNodeName()}/cac:AccountingSupplierParty/cac:Party`
);
valid = false;
}
return valid;
} catch (error) {
this.addError('UBL-XRECHNUNG', `Error validating XRechnung rules: ${error}`, '/');
return false;
}
}
/**
* Helper method to check if a node exists
* @param xpathExpression XPath to check
* @returns True if node exists
*/
private exists(xpathExpression: string): boolean {
if (!this.xmlDoc) return false;
const nodes = xpath.select(xpathExpression, this.xmlDoc);
// Handle different return types from xpath.select()
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return nodes ? true : false;
}
/**
* Helper method to get a number value from XPath
* @param xpathExpression XPath to get number from
* @returns Number value or NaN if not found
*/
private getNumberValue(xpathExpression: string): number {
if (!this.xmlDoc) return NaN;
const node = xpath.select1(`string(${xpathExpression})`, this.xmlDoc);
return node ? parseFloat(node.toString()) : NaN;
}
}

View File

@ -0,0 +1,122 @@
import { BaseDecoder } from '../base/base.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
import { DOMParser } from 'xmldom';
import * as xpath from 'xpath';
/**
* Base decoder for UBL-based invoice formats
*/
export abstract class UBLBaseDecoder extends BaseDecoder {
protected doc: Document;
protected namespaces: Record<string, string>;
protected select: xpath.XPathSelect;
constructor(xml: string) {
super(xml);
// Parse XML document
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
// Set up namespaces for XPath queries
this.namespaces = {
cbc: UBL_NAMESPACES.CBC,
cac: UBL_NAMESPACES.CAC
};
// Create XPath selector with namespaces
this.select = xpath.useNamespaces(this.namespaces);
}
/**
* Decodes UBL XML into a TInvoice object
* @returns Promise resolving to a TInvoice object
*/
public async decode(): Promise<TInvoice> {
// Determine document type
const documentType = this.getDocumentType();
if (documentType === UBLDocumentType.CREDIT_NOTE) {
return this.decodeCreditNote();
} else {
return this.decodeDebitNote();
}
}
/**
* Gets the UBL document type
* @returns UBL document type
*/
protected getDocumentType(): UBLDocumentType {
const rootName = this.doc.documentElement.nodeName;
if (rootName === UBLDocumentType.CREDIT_NOTE) {
return UBLDocumentType.CREDIT_NOTE;
} else {
return UBLDocumentType.INVOICE;
}
}
/**
* Decodes a UBL credit note
* @returns Promise resolving to a TCreditNote object
*/
protected abstract decodeCreditNote(): Promise<TCreditNote>;
/**
* Decodes a UBL debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected abstract decodeDebitNote(): Promise<TDebitNote>;
/**
* Gets a text value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Text value or empty string if not found
*/
protected getText(xpathExpr: string, context?: Node): string {
const node = this.select(xpathExpr, context || this.doc)[0];
return node ? (node.textContent || '') : '';
}
/**
* Gets a number value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Number value or 0 if not found or not a number
*/
protected getNumber(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
const num = parseFloat(text);
return isNaN(num) ? 0 : num;
}
/**
* Gets a date value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Date timestamp or current time if not found or invalid
*/
protected getDate(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
if (!text) return Date.now();
const date = new Date(text);
return isNaN(date.getTime()) ? Date.now() : date.getTime();
}
/**
* Checks if a node exists
* @param xpath XPath expression
* @param context Optional context node
* @returns True if node exists
*/
protected exists(xpathExpr: string, context?: Node): boolean {
const nodes = this.select(xpathExpr, context || this.doc);
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return false;
}
}

View File

@ -0,0 +1,59 @@
import { BaseEncoder } from '../base/base.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.js';
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
/**
* Base encoder for UBL-based invoice formats
*/
export abstract class UBLBaseEncoder extends BaseEncoder {
/**
* Encodes a TInvoice object into UBL XML
* @param invoice TInvoice object to encode
* @returns UBL XML string
*/
public async encode(invoice: TInvoice): Promise<string> {
// Determine if it's a credit note or debit note
if (invoice.invoiceType === 'creditnote') {
return this.encodeCreditNote(invoice as TCreditNote);
} else {
return this.encodeDebitNote(invoice as TDebitNote);
}
}
/**
* Encodes a TCreditNote object into UBL XML
* @param creditNote TCreditNote object to encode
* @returns UBL XML string
*/
protected abstract encodeCreditNote(creditNote: TCreditNote): Promise<string>;
/**
* Encodes a TDebitNote object into UBL XML
* @param debitNote TDebitNote object to encode
* @returns UBL XML string
*/
protected abstract encodeDebitNote(debitNote: TDebitNote): Promise<string>;
/**
* Creates the XML declaration and root element
* @param documentType UBL document type
* @returns XML string with declaration and root element
*/
protected createXmlRoot(documentType: UBLDocumentType): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<${documentType} xmlns="urn:oasis:names:specification:ubl:schema:xsd:${documentType}-2"
xmlns:cac="${UBL_NAMESPACES.CAC}"
xmlns:cbc="${UBL_NAMESPACES.CBC}">
</${documentType}>`;
}
/**
* Formats a date as an ISO string (YYYY-MM-DD)
* @param timestamp Timestamp to format
* @returns Formatted date string
*/
protected formatDate(timestamp: number): string {
const date = new Date(timestamp);
return date.toISOString().split('T')[0];
}
}

View File

@ -0,0 +1,22 @@
/**
* UBL-specific types and constants
*/
// UBL namespaces
export const UBL_NAMESPACES = {
CBC: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
CAC: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
UBL: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
};
// UBL document types
export enum UBLDocumentType {
INVOICE = 'Invoice',
CREDIT_NOTE = 'CreditNote'
}
// UBL customization IDs for different formats
export const UBL_CUSTOMIZATION_IDS = {
XRECHNUNG: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0',
PEPPOL_BIS: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0'
};

View File

@ -0,0 +1,134 @@
import { BaseValidator } from '../base/base.validator.js';
import { ValidationLevel } from '../../interfaces/common.js';
import type { ValidationResult } from '../../interfaces/common.js';
import { UBLDocumentType } from './ubl.types.js';
import { DOMParser } from 'xmldom';
import * as xpath from 'xpath';
/**
* Base validator for UBL-based invoice formats
*/
export abstract class UBLBaseValidator extends BaseValidator {
protected doc: Document;
protected namespaces: Record<string, string>;
protected select: xpath.XPathSelect;
constructor(xml: string) {
super(xml);
try {
// Parse XML document
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
// Set up namespaces for XPath queries
this.namespaces = {
cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'
};
// Create XPath selector with namespaces
this.select = xpath.useNamespaces(this.namespaces);
} catch (error) {
this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/');
}
}
/**
* Validates UBL XML against the specified level of validation
* @param level Validation level
* @returns Result of validation
*/
public validate(level: ValidationLevel = ValidationLevel.SYNTAX): ValidationResult {
// Reset errors
this.errors = [];
// Check if document was parsed successfully
if (!this.doc) {
return {
valid: false,
errors: this.errors,
level: level
};
}
// Perform validation based on level
let valid = true;
if (level === ValidationLevel.SYNTAX) {
valid = this.validateSchema();
} else if (level === ValidationLevel.SEMANTIC) {
valid = this.validateSchema() && this.validateStructure();
} else if (level === ValidationLevel.BUSINESS) {
valid = this.validateSchema() &&
this.validateStructure() &&
this.validateBusinessRules();
}
return {
valid,
errors: this.errors,
level
};
}
/**
* Validates UBL XML against schema
* @returns True if schema validation passed
*/
protected validateSchema(): boolean {
// Basic schema validation (simplified for now)
if (!this.doc) return false;
// Check for root element
const root = this.doc.documentElement;
if (!root || (root.nodeName !== UBLDocumentType.INVOICE && root.nodeName !== UBLDocumentType.CREDIT_NOTE)) {
this.addError('UBL-SCHEMA-1', `Root element must be ${UBLDocumentType.INVOICE} or ${UBLDocumentType.CREDIT_NOTE}`, '/');
return false;
}
return true;
}
/**
* Validates structure of the UBL XML document
* @returns True if structure validation passed
*/
protected abstract validateStructure(): boolean;
/**
* Gets a text value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Text value or empty string if not found
*/
protected getText(xpathExpr: string, context?: Node): string {
const node = this.select(xpathExpr, context || this.doc)[0];
return node ? (node.textContent || '') : '';
}
/**
* Gets a number value from an XPath expression
* @param xpath XPath expression
* @param context Optional context node
* @returns Number value or 0 if not found or not a number
*/
protected getNumber(xpathExpr: string, context?: Node): number {
const text = this.getText(xpathExpr, context);
const num = parseFloat(text);
return isNaN(num) ? 0 : num;
}
/**
* Checks if a node exists
* @param xpath XPath expression
* @param context Optional context node
* @returns True if node exists
*/
protected exists(xpathExpr: string, context?: Node): boolean {
const nodes = this.select(xpathExpr, context || this.doc);
if (Array.isArray(nodes)) {
return nodes.length > 0;
}
return false;
}
}

View File

@ -0,0 +1,292 @@
import { UBLBaseDecoder } from '../ubl.decoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { business, finance } from '@tsclass/tsclass';
import { UBLDocumentType } from '../ubl.types.js';
/**
* Decoder for XRechnung (UBL) format
* Implements decoding of XRechnung invoices to TInvoice
*/
export class XRechnungDecoder extends UBLBaseDecoder {
/**
* Decodes a UBL credit note
* @returns Promise resolving to a TCreditNote object
*/
protected async decodeCreditNote(): Promise<TCreditNote> {
// Extract common data
const commonData = await this.extractCommonData();
// Return the invoice data as a credit note
return {
...commonData,
invoiceType: 'creditnote'
} as TCreditNote;
}
/**
* Decodes a UBL debit note (invoice)
* @returns Promise resolving to a TDebitNote object
*/
protected async decodeDebitNote(): Promise<TDebitNote> {
// Extract common data
const commonData = await this.extractCommonData();
// Return the invoice data as a debit note
return {
...commonData,
invoiceType: 'debitnote'
} as TDebitNote;
}
/**
* Extracts common invoice data from XRechnung XML
* @returns Common invoice data
*/
private async extractCommonData(): Promise<Partial<TInvoice>> {
try {
// Default values
const invoiceId = this.getText('//cbc:ID', this.doc) || `INV-${Date.now()}`;
const issueDateText = this.getText('//cbc:IssueDate', this.doc);
const issueDate = issueDateText ? new Date(issueDateText).getTime() : Date.now();
const currencyCode = this.getText('//cbc:DocumentCurrencyCode', this.doc) || 'EUR';
// Extract payment terms
let dueInDays = 30; // Default
const dueDateText = this.getText('//cac:PaymentTerms/cbc:PaymentDueDate', this.doc);
if (dueDateText) {
const dueDateObj = new Date(dueDateText);
const issueDateObj = new Date(issueDate);
const diffTime = Math.abs(dueDateObj.getTime() - issueDateObj.getTime());
dueInDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
// Extract items
const items: finance.TInvoiceItem[] = [];
const invoiceLines = this.select('//cac:InvoiceLine', this.doc);
if (invoiceLines && Array.isArray(invoiceLines)) {
for (let i = 0; i < invoiceLines.length; i++) {
const line = invoiceLines[i];
const position = i + 1;
const name = this.getText('./cac:Item/cbc:Name', line) || `Item ${position}`;
const articleNumber = this.getText('./cac:Item/cac:SellersItemIdentification/cbc:ID', line) || '';
const unitType = this.getText('./cbc:InvoicedQuantity/@unitCode', line) || 'EA';
let unitQuantity = 1;
const quantityText = this.getText('./cbc:InvoicedQuantity', line);
if (quantityText) {
unitQuantity = parseFloat(quantityText) || 1;
}
let unitNetPrice = 0;
const priceText = this.getText('./cac:Price/cbc:PriceAmount', line);
if (priceText) {
unitNetPrice = parseFloat(priceText) || 0;
}
let vatPercentage = 0;
const percentText = this.getText('./cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', line);
if (percentText) {
vatPercentage = parseFloat(percentText) || 0;
}
items.push({
position,
name,
articleNumber,
unitType,
unitQuantity,
unitNetPrice,
vatPercentage
});
}
}
// Extract notes
const notes: string[] = [];
const noteNodes = this.select('//cbc:Note', this.doc);
if (noteNodes && Array.isArray(noteNodes)) {
for (let i = 0; i < noteNodes.length; i++) {
const noteText = noteNodes[i].textContent || '';
if (noteText) {
notes.push(noteText);
}
}
}
// Extract seller and buyer information
const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party');
const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party');
// Create the common invoice data
return {
type: 'invoice',
id: invoiceId,
invoiceId: invoiceId,
date: issueDate,
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: invoiceId,
from: seller,
to: buyer,
subject: `Invoice ${invoiceId}`,
items: items,
dueInDays: dueInDays,
reverseCharge: false,
currency: currencyCode as finance.TCurrency,
notes: notes,
objectActions: []
};
} catch (error) {
console.error('Error extracting common data:', error);
// Return default data
return {
type: 'invoice',
id: `INV-${Date.now()}`,
invoiceId: `INV-${Date.now()}`,
date: Date.now(),
status: 'invoice',
versionInfo: {
type: 'final',
version: '1.0.0'
},
language: 'en',
incidenceId: `INV-${Date.now()}`,
from: this.createEmptyContact(),
to: this.createEmptyContact(),
subject: 'Invoice',
items: [],
dueInDays: 30,
reverseCharge: false,
currency: 'EUR',
notes: [],
objectActions: []
};
}
}
/**
* Extracts party information from XML
* @param partyPath XPath to the party element
* @returns TContact object
*/
private extractParty(partyPath: string): business.TContact {
try {
// Default values
let name = '';
let streetName = '';
let houseNumber = '0';
let city = '';
let postalCode = '';
let country = '';
let countryCode = '';
let vatId = '';
let registrationId = '';
let registrationName = '';
// Try to extract party information
const partyNodes = this.select(partyPath, this.doc);
if (partyNodes && Array.isArray(partyNodes) && partyNodes.length > 0) {
const party = partyNodes[0];
// Extract name
name = this.getText('./cac:PartyName/cbc:Name', party) || '';
// Extract address
const addressNodes = this.select('./cac:PostalAddress', party);
if (addressNodes && Array.isArray(addressNodes) && addressNodes.length > 0) {
const address = addressNodes[0];
streetName = this.getText('./cbc:StreetName', address) || '';
houseNumber = this.getText('./cbc:BuildingNumber', address) || '0';
city = this.getText('./cbc:CityName', address) || '';
postalCode = this.getText('./cbc:PostalZone', address) || '';
const countryNodes = this.select('./cac:Country', address);
if (countryNodes && Array.isArray(countryNodes) && countryNodes.length > 0) {
const countryNode = countryNodes[0];
country = this.getText('./cbc:Name', countryNode) || '';
countryCode = this.getText('./cbc:IdentificationCode', countryNode) || '';
}
}
// Extract tax information
const taxSchemeNodes = this.select('./cac:PartyTaxScheme', party);
if (taxSchemeNodes && Array.isArray(taxSchemeNodes) && taxSchemeNodes.length > 0) {
vatId = this.getText('./cbc:CompanyID', taxSchemeNodes[0]) || '';
}
// Extract registration information
const legalEntityNodes = this.select('./cac:PartyLegalEntity', party);
if (legalEntityNodes && Array.isArray(legalEntityNodes) && legalEntityNodes.length > 0) {
registrationId = this.getText('./cbc:CompanyID', legalEntityNodes[0]) || '';
registrationName = this.getText('./cbc:RegistrationName', legalEntityNodes[0]) || name;
}
}
return {
type: 'company',
name: name,
description: '',
address: {
streetName: streetName,
houseNumber: houseNumber,
city: city,
postalCode: postalCode,
country: country,
countryCode: countryCode
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: vatId,
registrationId: registrationId,
registrationName: registrationName
}
};
} catch (error) {
console.error('Error extracting party information:', error);
return this.createEmptyContact();
}
}
/**
* Creates an empty TContact object
* @returns Empty TContact object
*/
private createEmptyContact(): business.TContact {
return {
type: 'company',
name: '',
description: '',
address: {
streetName: '',
houseNumber: '0',
city: '',
country: '',
postalCode: ''
},
status: 'active',
foundedDate: {
year: 2000,
month: 1,
day: 1
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: ''
}
};
}
}

View File

@ -0,0 +1,144 @@
import { UBLBaseEncoder } from '../ubl.encoder.js';
import type { TInvoice, TCreditNote, TDebitNote } from '../../../interfaces/common.js';
import { UBLDocumentType } from '../ubl.types.js';
/**
* Encoder for XRechnung (UBL) format
* Implements encoding of TInvoice to XRechnung XML
*/
export class XRechnungEncoder extends UBLBaseEncoder {
/**
* Encodes a TCreditNote object to XRechnung XML
* @param creditNote TCreditNote object to encode
* @returns Promise resolving to XML string
*/
protected async encodeCreditNote(creditNote: TCreditNote): Promise<string> {
// For now, we'll just return a simple UBL credit note template
// In a real implementation, we would generate a proper UBL credit note
return `<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ID>${creditNote.id}</cbc:ID>
<cbc:IssueDate>${this.formatDate(creditNote.date)}</cbc:IssueDate>
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
<cbc:DocumentCurrencyCode>${creditNote.currency}</cbc:DocumentCurrencyCode>
<!-- Rest of the credit note XML would go here -->
</CreditNote>`;
}
/**
* Encodes a TDebitNote object to XRechnung XML
* @param debitNote TDebitNote object to encode
* @returns Promise resolving to XML string
*/
protected async encodeDebitNote(debitNote: TDebitNote): Promise<string> {
// For now, we'll just return a simple UBL invoice template
// In a real implementation, we would generate a proper UBL invoice
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0</cbc:CustomizationID>
<cbc:ID>${debitNote.id}</cbc:ID>
<cbc:IssueDate>${this.formatDate(debitNote.date)}</cbc:IssueDate>
<cbc:DueDate>${this.formatDate(debitNote.date + debitNote.dueInDays * 24 * 60 * 60 * 1000)}</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>${debitNote.currency}</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${debitNote.from.name}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>${debitNote.from.address.streetName || ''}</cbc:StreetName>
<cbc:BuildingNumber>${debitNote.from.address.houseNumber || ''}</cbc:BuildingNumber>
<cbc:CityName>${debitNote.from.address.city || ''}</cbc:CityName>
<cbc:PostalZone>${debitNote.from.address.postalCode || ''}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>${debitNote.from.address.countryCode || ''}</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
${debitNote.from.registrationDetails?.vatId ? `
<cac:PartyTaxScheme>
<cbc:CompanyID>${debitNote.from.registrationDetails.vatId}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>` : ''}
${debitNote.from.registrationDetails?.registrationId ? `
<cac:PartyLegalEntity>
<cbc:RegistrationName>${debitNote.from.registrationDetails.registrationName || debitNote.from.name}</cbc:RegistrationName>
<cbc:CompanyID>${debitNote.from.registrationDetails.registrationId}</cbc:CompanyID>
</cac:PartyLegalEntity>` : ''}
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>${debitNote.to.name}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>${debitNote.to.address.streetName || ''}</cbc:StreetName>
<cbc:BuildingNumber>${debitNote.to.address.houseNumber || ''}</cbc:BuildingNumber>
<cbc:CityName>${debitNote.to.address.city || ''}</cbc:CityName>
<cbc:PostalZone>${debitNote.to.address.postalCode || ''}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>${debitNote.to.address.countryCode || ''}</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
${debitNote.to.registrationDetails?.vatId ? `
<cac:PartyTaxScheme>
<cbc:CompanyID>${debitNote.to.registrationDetails.vatId}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>` : ''}
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentTerms>
<cbc:Note>Due in ${debitNote.dueInDays} days</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="${debitNote.currency}">0.00</cbc:TaxAmount>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">0.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="${debitNote.currency}">0.00</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="${debitNote.currency}">0.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
${debitNote.items.map((item, index) => `
<cac:InvoiceLine>
<cbc:ID>${index + 1}</cbc:ID>
<cbc:InvoicedQuantity unitCode="${item.unitType}">${item.unitQuantity}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="${debitNote.currency}">${item.unitNetPrice * item.unitQuantity}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>${item.name}</cbc:Name>
${item.articleNumber ? `
<cac:SellersItemIdentification>
<cbc:ID>${item.articleNumber}</cbc:ID>
</cac:SellersItemIdentification>` : ''}
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>${item.vatPercentage}</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="${debitNote.currency}">${item.unitNetPrice}</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`).join('')}
</Invoice>`;
}
}

View File

@ -0,0 +1,48 @@
import { InvoiceFormat } from '../../interfaces/common.js';
import { DOMParser } from 'xmldom';
/**
* Utility class for detecting invoice formats
*/
export class FormatDetector {
/**
* Detects the format of an XML document
* @param xml XML content to analyze
* @returns Detected invoice format
*/
public static detectFormat(xml: string): InvoiceFormat {
try {
const doc = new DOMParser().parseFromString(xml, 'application/xml');
const root = doc.documentElement;
if (!root) {
return InvoiceFormat.UNKNOWN;
}
// UBL detection (Invoice or CreditNote root element)
if (root.nodeName === 'Invoice' || root.nodeName === 'CreditNote') {
// For simplicity, we'll treat all UBL documents as XRechnung for now
// In a real implementation, we would check for specific customization IDs
return InvoiceFormat.XRECHNUNG;
}
// Factur-X/ZUGFeRD detection (CrossIndustryInvoice root element)
if (root.nodeName === 'rsm:CrossIndustryInvoice' || root.nodeName === 'CrossIndustryInvoice') {
// For simplicity, we'll treat all CII documents as Factur-X for now
// In a real implementation, we would check for specific profiles
return InvoiceFormat.FACTURX;
}
// FatturaPA detection would be implemented here
if (root.nodeName === 'FatturaElettronica' ||
(root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))) {
return InvoiceFormat.FATTURAPA;
}
return InvoiceFormat.UNKNOWN;
} catch (error) {
console.error('Error detecting format:', error);
return InvoiceFormat.UNKNOWN;
}
}
}

View File

@ -1,92 +0,0 @@
import { InvoiceFormat } from '../interfaces.js';
import type { IValidator } from '../interfaces.js';
import { BaseValidator } from './base.validator.js';
import { FacturXValidator } from './facturx.validator.js';
import { UBLValidator } from './ubl.validator.js';
import { DOMParser } from 'xmldom';
/**
* Factory to create the appropriate validator based on the XML format
*/
export class ValidatorFactory {
/**
* Creates a validator for the specified XML content
* @param xml XML content to validate
* @returns Appropriate validator instance
*/
public static createValidator(xml: string): BaseValidator {
const format = ValidatorFactory.detectFormat(xml);
switch (format) {
case InvoiceFormat.UBL:
case InvoiceFormat.XRECHNUNG:
return new UBLValidator(xml);
case InvoiceFormat.CII:
case InvoiceFormat.ZUGFERD:
case InvoiceFormat.FACTURX:
return new FacturXValidator(xml);
// FatturaPA and other formats would be implemented here
default:
throw new Error(`Unsupported invoice format: ${format}`);
}
}
/**
* Detects the invoice format from XML content
* @param xml XML content to analyze
* @returns Detected invoice format
*/
private static detectFormat(xml: string): InvoiceFormat {
try {
const doc = new DOMParser().parseFromString(xml, 'application/xml');
const root = doc.documentElement;
if (!root) {
return InvoiceFormat.UNKNOWN;
}
// UBL detection (Invoice or CreditNote root element)
if (root.nodeName === 'Invoice' || root.nodeName === 'CreditNote') {
// Check if it's XRechnung by looking at CustomizationID
const customizationNodes = root.getElementsByTagName('cbc:CustomizationID');
if (customizationNodes.length > 0) {
const customizationId = customizationNodes[0].textContent || '';
if (customizationId.includes('xrechnung') || customizationId.includes('XRechnung')) {
return InvoiceFormat.XRECHNUNG;
}
}
return InvoiceFormat.UBL;
}
// Factur-X/ZUGFeRD detection (CrossIndustryInvoice root element)
if (root.nodeName === 'rsm:CrossIndustryInvoice' || root.nodeName === 'CrossIndustryInvoice') {
// Check for profile to determine if it's Factur-X or ZUGFeRD
const profileNodes = root.getElementsByTagName('ram:ID');
for (let i = 0; i < profileNodes.length; i++) {
const profileText = profileNodes[i].textContent || '';
if (profileText.includes('factur-x') || profileText.includes('Factur-X')) {
return InvoiceFormat.FACTURX;
}
if (profileText.includes('zugferd') || profileText.includes('ZUGFeRD')) {
return InvoiceFormat.ZUGFERD;
}
}
// If no specific profile found, default to CII
return InvoiceFormat.CII;
}
// FatturaPA detection would be implemented here
return InvoiceFormat.UNKNOWN;
} catch (error) {
return InvoiceFormat.UNKNOWN;
}
}
}

View File

@ -1,326 +0,0 @@
import * as plugins from '../plugins.js';
import * as xmldom from 'xmldom';
import { BaseDecoder } from './base.decoder.js';
/**
* A decoder specifically for XInvoice/XRechnung format.
* XRechnung is the German implementation of the European standard EN16931
* for electronic invoices to the German public sector.
*/
export class XInvoiceDecoder extends BaseDecoder {
private xmlDoc: Document | null = null;
private namespaces: { [key: string]: string } = {
cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
ubl: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
};
constructor(xmlString: string) {
super(xmlString);
// Parse XML to DOM
try {
const parser = new xmldom.DOMParser();
this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml');
// Try to detect if this is actually UBL (which XRechnung is based on)
if (this.xmlString.includes('oasis:names:specification:ubl')) {
// Set up appropriate namespaces
this.setupNamespaces();
}
} catch (error) {
console.error('Error parsing XInvoice XML:', error);
}
}
/**
* Set up namespaces from the document
*/
private setupNamespaces(): void {
if (!this.xmlDoc) return;
// Try to extract namespaces from the document
const root = this.xmlDoc.documentElement;
if (root) {
// Look for common UBL namespaces
for (let i = 0; i < root.attributes.length; i++) {
const attr = root.attributes[i];
if (attr.name.startsWith('xmlns:')) {
const prefix = attr.name.substring(6);
this.namespaces[prefix] = attr.value;
}
}
}
}
/**
* Extract element text by tag name with namespace awareness
*/
private getElementText(tagName: string): string {
if (!this.xmlDoc) {
return '';
}
try {
// Handle namespace prefixes
if (tagName.includes(':')) {
const [nsPrefix, localName] = tagName.split(':');
// Find elements with this tag name
const elements = this.xmlDoc.getElementsByTagNameNS(this.namespaces[nsPrefix] || '', localName);
if (elements.length > 0) {
return elements[0].textContent || '';
}
}
// Fallback to direct tag name lookup
const elements = this.xmlDoc.getElementsByTagName(tagName);
if (elements.length > 0) {
return elements[0].textContent || '';
}
return '';
} catch (error) {
console.error(`Error extracting XInvoice element ${tagName}:`, error);
return '';
}
}
/**
* Converts XInvoice/XRechnung XML to a structured letter object
*/
public async getLetterData(): Promise<plugins.tsclass.business.ILetter> {
try {
// Extract invoice ID - typically in cbc:ID or Invoice/cbc:ID
let invoiceId = this.getElementText('cbc:ID');
if (!invoiceId) {
invoiceId = this.getElementText('Invoice/cbc:ID') || 'Unknown';
}
// Extract invoice issue date
const issueDateStr = this.getElementText('cbc:IssueDate') || '';
const issueDate = issueDateStr ? new Date(issueDateStr).getTime() : Date.now();
// Extract seller information
const sellerName = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') ||
this.getElementText('cac:SellerSupplierParty/cac:Party/cac:PartyName/cbc:Name') ||
'Unknown Seller';
// Extract seller address
const sellerStreet = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName') || 'Unknown';
const sellerCity = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:CityName') || 'Unknown';
const sellerPostcode = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:PostalZone') || 'Unknown';
const sellerCountry = this.getElementText('cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cac:Country/cbc:IdentificationCode') || 'Unknown';
// Extract buyer information
const buyerName = this.getElementText('cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') ||
this.getElementText('cac:BuyerCustomerParty/cac:Party/cac:PartyName/cbc:Name') ||
'Unknown Buyer';
// Create seller contact
const seller: plugins.tsclass.business.IContact = {
name: sellerName,
type: 'company',
description: sellerName,
address: {
streetName: sellerStreet,
houseNumber: '0', // Required by IAddress interface
city: sellerCity,
country: sellerCountry,
postalCode: sellerPostcode,
},
};
// Create buyer contact
const buyer: plugins.tsclass.business.IContact = {
name: buyerName,
type: 'company',
description: buyerName,
address: {
streetName: 'Unknown',
houseNumber: '0',
city: 'Unknown',
country: 'Unknown',
postalCode: 'Unknown',
},
};
// Extract invoice type
let invoiceType = 'debitnote';
const typeCode = this.getElementText('cbc:InvoiceTypeCode');
if (typeCode === '380') {
invoiceType = 'debitnote'; // Standard invoice
} else if (typeCode === '381') {
invoiceType = 'creditnote'; // Credit note
}
// Create invoice data
const invoiceData: plugins.tsclass.finance.IInvoice = {
id: invoiceId,
status: null,
type: invoiceType as 'debitnote' | 'creditnote',
billedBy: seller,
billedTo: buyer,
deliveryDate: issueDate,
dueInDays: 30,
periodOfPerformance: null,
printResult: null,
currency: (this.getElementText('cbc:DocumentCurrencyCode') || 'EUR') as plugins.tsclass.finance.TCurrency,
notes: [],
items: this.extractInvoiceItems(),
reverseCharge: false,
};
// Return a letter
return {
versionInfo: {
type: 'draft',
version: '1.0.0',
},
type: 'invoice',
date: issueDate,
subject: `XInvoice: ${invoiceId}`,
from: seller,
to: buyer,
content: {
invoiceData: invoiceData,
textData: null,
timesheetData: null,
contractData: null,
},
needsCoverSheet: false,
objectActions: [],
pdf: null,
incidenceId: null,
language: null,
legalContact: null,
logoUrl: null,
pdfAttachments: null,
accentColor: null,
};
} catch (error) {
console.error('Error converting XInvoice XML to letter data:', error);
return this.createDefaultLetter();
}
}
/**
* Extracts invoice items from XInvoice document
*/
private extractInvoiceItems(): plugins.tsclass.finance.IInvoiceItem[] {
if (!this.xmlDoc) {
return [
{
name: 'Unknown Item',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
];
}
try {
const items: plugins.tsclass.finance.IInvoiceItem[] = [];
// Get all invoice line elements
const lines = this.xmlDoc.getElementsByTagName('cac:InvoiceLine');
if (!lines || lines.length === 0) {
// Fallback to a default item
return [
{
name: 'Item from XInvoice XML',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
];
}
// Process each line
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Extract item details
let name = '';
let quantity = 1;
let price = 0;
let vatRate = 0;
// Find description element
const descElements = line.getElementsByTagName('cbc:Description');
if (descElements.length > 0) {
name = descElements[0].textContent || '';
}
// Fallback to item name if description is empty
if (!name) {
const itemNameElements = line.getElementsByTagName('cbc:Name');
if (itemNameElements.length > 0) {
name = itemNameElements[0].textContent || '';
}
}
// Find quantity
const quantityElements = line.getElementsByTagName('cbc:InvoicedQuantity');
if (quantityElements.length > 0) {
const quantityText = quantityElements[0].textContent || '1';
quantity = parseFloat(quantityText) || 1;
}
// Find price
const priceElements = line.getElementsByTagName('cbc:PriceAmount');
if (priceElements.length > 0) {
const priceText = priceElements[0].textContent || '0';
price = parseFloat(priceText) || 0;
}
// Find VAT rate - this is a bit more complex in UBL/XRechnung
const taxCategoryElements = line.getElementsByTagName('cac:ClassifiedTaxCategory');
if (taxCategoryElements.length > 0) {
const rateElements = taxCategoryElements[0].getElementsByTagName('cbc:Percent');
if (rateElements.length > 0) {
const rateText = rateElements[0].textContent || '0';
vatRate = parseFloat(rateText) || 0;
}
}
// Add the item to the list
items.push({
name: name || `Item ${i+1}`,
unitQuantity: quantity,
unitNetPrice: price,
vatPercentage: vatRate,
position: i,
unitType: 'units',
});
}
return items.length > 0 ? items : [
{
name: 'Item from XInvoice XML',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
];
} catch (error) {
console.error('Error extracting XInvoice items:', error);
return [
{
name: 'Error extracting items',
unitQuantity: 1,
unitNetPrice: 0,
vatPercentage: 0,
position: 0,
unitType: 'units',
}
];
}
}
}

View File

@ -1,335 +0,0 @@
import * as plugins from '../plugins.js';
/**
* A class to convert a given ILetter with invoice data
* into an XInvoice/XRechnung compliant XML (based on UBL).
*
* XRechnung is the German implementation of the European standard EN16931
* for electronic invoices to the German public sector.
*/
export class XInvoiceEncoder {
constructor() {}
/**
* Creates an XInvoice compliant XML based on the provided letter data.
*/
public createXInvoiceXml(letterArg: plugins.tsclass.business.ILetter): string {
// Use SmartXml for XML creation
const smartxmlInstance = new plugins.smartxml.SmartXml();
if (!letterArg?.content?.invoiceData) {
throw new Error('Letter does not contain invoice data.');
}
const invoice: plugins.tsclass.finance.IInvoice = letterArg.content.invoiceData;
const billedBy: plugins.tsclass.business.IContact = invoice.billedBy;
const billedTo: plugins.tsclass.business.IContact = invoice.billedTo;
// Create the XML document
const doc = smartxmlInstance
.create({ version: '1.0', encoding: 'UTF-8' })
.ele('Invoice', {
'xmlns': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'xmlns:cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'xmlns:cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'
});
// UBL Version ID
doc.ele('cbc:UBLVersionID').txt('2.1').up();
// CustomizationID for XRechnung
doc.ele('cbc:CustomizationID').txt('urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0').up();
// ID - Invoice number
doc.ele('cbc:ID').txt(invoice.id).up();
// Issue date
const issueDate = new Date(letterArg.date);
const issueDateStr = `${issueDate.getFullYear()}-${String(issueDate.getMonth() + 1).padStart(2, '0')}-${String(issueDate.getDate()).padStart(2, '0')}`;
doc.ele('cbc:IssueDate').txt(issueDateStr).up();
// Due date
const dueDate = new Date(letterArg.date);
dueDate.setDate(dueDate.getDate() + invoice.dueInDays);
const dueDateStr = `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, '0')}-${String(dueDate.getDate()).padStart(2, '0')}`;
doc.ele('cbc:DueDate').txt(dueDateStr).up();
// Invoice type code
const invoiceTypeCode = invoice.type === 'creditnote' ? '381' : '380';
doc.ele('cbc:InvoiceTypeCode').txt(invoiceTypeCode).up();
// Note - optional invoice note
if (invoice.notes && invoice.notes.length > 0) {
doc.ele('cbc:Note').txt(invoice.notes[0]).up();
}
// Document currency code
doc.ele('cbc:DocumentCurrencyCode').txt(invoice.currency).up();
// Tax currency code - same as document currency in this case
doc.ele('cbc:TaxCurrencyCode').txt(invoice.currency).up();
// Accounting supplier party (seller)
const supplierParty = doc.ele('cac:AccountingSupplierParty');
const supplierPartyDetails = supplierParty.ele('cac:Party');
// Seller VAT ID
if (billedBy.vatId) {
const partyTaxScheme = supplierPartyDetails.ele('cac:PartyTaxScheme');
partyTaxScheme.ele('cbc:CompanyID').txt(billedBy.vatId).up();
partyTaxScheme.ele('cac:TaxScheme')
.ele('cbc:ID').txt('VAT').up()
.up();
}
// Seller name
supplierPartyDetails.ele('cac:PartyName')
.ele('cbc:Name').txt(billedBy.name).up()
.up();
// Seller postal address
const supplierAddress = supplierPartyDetails.ele('cac:PostalAddress');
supplierAddress.ele('cbc:StreetName').txt(billedBy.address.streetName).up();
if (billedBy.address.houseNumber) {
supplierAddress.ele('cbc:BuildingNumber').txt(billedBy.address.houseNumber).up();
}
supplierAddress.ele('cbc:CityName').txt(billedBy.address.city).up();
supplierAddress.ele('cbc:PostalZone').txt(billedBy.address.postalCode).up();
supplierAddress.ele('cac:Country')
.ele('cbc:IdentificationCode').txt(billedBy.address.country || 'DE').up()
.up();
// Seller contact
const supplierContact = supplierPartyDetails.ele('cac:Contact');
if (billedBy.email) {
supplierContact.ele('cbc:ElectronicMail').txt(billedBy.email).up();
}
if (billedBy.phone) {
supplierContact.ele('cbc:Telephone').txt(billedBy.phone).up();
}
supplierParty.up(); // Close AccountingSupplierParty
// Accounting customer party (buyer)
const customerParty = doc.ele('cac:AccountingCustomerParty');
const customerPartyDetails = customerParty.ele('cac:Party');
// Buyer VAT ID
if (billedTo.vatId) {
const partyTaxScheme = customerPartyDetails.ele('cac:PartyTaxScheme');
partyTaxScheme.ele('cbc:CompanyID').txt(billedTo.vatId).up();
partyTaxScheme.ele('cac:TaxScheme')
.ele('cbc:ID').txt('VAT').up()
.up();
}
// Buyer name
customerPartyDetails.ele('cac:PartyName')
.ele('cbc:Name').txt(billedTo.name).up()
.up();
// Buyer postal address
const customerAddress = customerPartyDetails.ele('cac:PostalAddress');
customerAddress.ele('cbc:StreetName').txt(billedTo.address.streetName).up();
if (billedTo.address.houseNumber) {
customerAddress.ele('cbc:BuildingNumber').txt(billedTo.address.houseNumber).up();
}
customerAddress.ele('cbc:CityName').txt(billedTo.address.city).up();
customerAddress.ele('cbc:PostalZone').txt(billedTo.address.postalCode).up();
customerAddress.ele('cac:Country')
.ele('cbc:IdentificationCode').txt(billedTo.address.country || 'DE').up()
.up();
// Buyer contact
if (billedTo.email || billedTo.phone) {
const customerContact = customerPartyDetails.ele('cac:Contact');
if (billedTo.email) {
customerContact.ele('cbc:ElectronicMail').txt(billedTo.email).up();
}
if (billedTo.phone) {
customerContact.ele('cbc:Telephone').txt(billedTo.phone).up();
}
}
customerParty.up(); // Close AccountingCustomerParty
// Payment means
if (billedBy.sepaConnection) {
const paymentMeans = doc.ele('cac:PaymentMeans');
paymentMeans.ele('cbc:PaymentMeansCode').txt('58').up(); // 58 = SEPA credit transfer
paymentMeans.ele('cbc:PaymentID').txt(invoice.id).up();
// IBAN
if (billedBy.sepaConnection.iban) {
const payeeAccount = paymentMeans.ele('cac:PayeeFinancialAccount');
payeeAccount.ele('cbc:ID').txt(billedBy.sepaConnection.iban).up();
// BIC
if (billedBy.sepaConnection.bic) {
payeeAccount.ele('cac:FinancialInstitutionBranch')
.ele('cbc:ID').txt(billedBy.sepaConnection.bic).up()
.up();
}
}
}
// Payment terms
const paymentTerms = doc.ele('cac:PaymentTerms');
paymentTerms.ele('cbc:Note').txt(`Payment due in ${invoice.dueInDays} days`).up();
// Tax summary
// Group items by VAT rate
const vatRates: { [rate: number]: plugins.tsclass.finance.IInvoiceItem[] } = {};
// Collect items by VAT rate
invoice.items.forEach(item => {
if (!vatRates[item.vatPercentage]) {
vatRates[item.vatPercentage] = [];
}
vatRates[item.vatPercentage].push(item);
});
// Calculate tax subtotals for each rate
Object.entries(vatRates).forEach(([rate, items]) => {
const taxRate = parseFloat(rate);
// Calculate base amount for this rate
let taxableAmount = 0;
items.forEach(item => {
taxableAmount += item.unitNetPrice * item.unitQuantity;
});
// Calculate tax amount
const taxAmount = taxableAmount * (taxRate / 100);
// Create tax subtotal
const taxSubtotal = doc.ele('cac:TaxTotal')
.ele('cbc:TaxAmount').txt(taxAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
taxSubtotal.ele('cac:TaxSubtotal')
.ele('cbc:TaxableAmount')
.txt(taxableAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up()
.ele('cbc:TaxAmount')
.txt(taxAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up()
.ele('cac:TaxCategory')
.ele('cbc:ID').txt('S').up() // Standard rate
.ele('cbc:Percent').txt(taxRate.toFixed(2)).up()
.ele('cac:TaxScheme')
.ele('cbc:ID').txt('VAT').up()
.up()
.up()
.up();
});
// Calculate invoice totals
let lineExtensionAmount = 0;
let taxExclusiveAmount = 0;
let taxInclusiveAmount = 0;
let totalVat = 0;
// Sum all items
invoice.items.forEach(item => {
const net = item.unitNetPrice * item.unitQuantity;
const vat = net * (item.vatPercentage / 100);
lineExtensionAmount += net;
taxExclusiveAmount += net;
totalVat += vat;
});
taxInclusiveAmount = taxExclusiveAmount + totalVat;
// Legal monetary total
const legalMonetaryTotal = doc.ele('cac:LegalMonetaryTotal');
legalMonetaryTotal.ele('cbc:LineExtensionAmount')
.txt(lineExtensionAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
legalMonetaryTotal.ele('cbc:TaxExclusiveAmount')
.txt(taxExclusiveAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
legalMonetaryTotal.ele('cbc:TaxInclusiveAmount')
.txt(taxInclusiveAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
legalMonetaryTotal.ele('cbc:PayableAmount')
.txt(taxInclusiveAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
// Invoice lines
invoice.items.forEach((item, index) => {
const invoiceLine = doc.ele('cac:InvoiceLine');
invoiceLine.ele('cbc:ID').txt((index + 1).toString()).up();
// Quantity
invoiceLine.ele('cbc:InvoicedQuantity')
.txt(item.unitQuantity.toString())
.att('unitCode', this.mapUnitType(item.unitType))
.up();
// Line extension amount (net)
const lineAmount = item.unitNetPrice * item.unitQuantity;
invoiceLine.ele('cbc:LineExtensionAmount')
.txt(lineAmount.toFixed(2))
.att('currencyID', invoice.currency)
.up();
// Item details
const itemEle = invoiceLine.ele('cac:Item');
itemEle.ele('cbc:Description').txt(item.name).up();
itemEle.ele('cbc:Name').txt(item.name).up();
// Classified tax category
itemEle.ele('cac:ClassifiedTaxCategory')
.ele('cbc:ID').txt('S').up() // Standard rate
.ele('cbc:Percent').txt(item.vatPercentage.toFixed(2)).up()
.ele('cac:TaxScheme')
.ele('cbc:ID').txt('VAT').up()
.up()
.up();
// Price
invoiceLine.ele('cac:Price')
.ele('cbc:PriceAmount')
.txt(item.unitNetPrice.toFixed(2))
.att('currencyID', invoice.currency)
.up()
.up();
});
// Return the formatted XML
return doc.end({ prettyPrint: true });
}
/**
* Helper: Map your custom 'unitType' to an ISO code.
*/
private mapUnitType(unitType: string): string {
switch (unitType.toLowerCase()) {
case 'hour':
case 'hours':
return 'HUR';
case 'day':
case 'days':
return 'DAY';
case 'piece':
case 'pieces':
return 'C62';
default:
return 'C62'; // fallback for unknown unit types
}
}
}

View File

@ -1,67 +1,90 @@
import * as interfaces from './interfaces.js';
// Import main class
import { XInvoice } from './classes.xinvoice.js';
// Import format-specific encoder/decoder classes
import { FacturXEncoder } from './formats/facturx.encoder.js';
import { FacturXDecoder } from './formats/facturx.decoder.js';
import { XInvoiceEncoder } from './formats/xinvoice.encoder.js';
import { XInvoiceDecoder } from './formats/xinvoice.decoder.js';
import { DecoderFactory } from './formats/decoder.factory.js';
import { BaseDecoder } from './formats/base.decoder.js';
// Import interfaces
import * as common from './interfaces/common.js';
// Import validator classes
import { ValidatorFactory } from './formats/validator.factory.js';
import { BaseValidator } from './formats/base.validator.js';
import { FacturXValidator } from './formats/facturx.validator.js';
import { UBLValidator } from './formats/ubl.validator.js';
// Import factories
import { DecoderFactory } from './formats/factories/decoder.factory.js';
import { EncoderFactory } from './formats/factories/encoder.factory.js';
import { ValidatorFactory } from './formats/factories/validator.factory.js';
// Export specific interfaces for easier use
// Import base classes
import { BaseDecoder } from './formats/base/base.decoder.js';
import { BaseEncoder } from './formats/base/base.encoder.js';
import { BaseValidator } from './formats/base/base.validator.js';
// Import UBL base classes
import { UBLBaseDecoder } from './formats/ubl/ubl.decoder.js';
import { UBLBaseEncoder } from './formats/ubl/ubl.encoder.js';
import { UBLBaseValidator } from './formats/ubl/ubl.validator.js';
// Import CII base classes
import { CIIBaseDecoder } from './formats/cii/cii.decoder.js';
import { CIIBaseEncoder } from './formats/cii/cii.encoder.js';
import { CIIBaseValidator } from './formats/cii/cii.validator.js';
// Import PDF utilities
import { PDFEmbedder } from './formats/pdf/pdf.embedder.js';
import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
// Import format detector
import { FormatDetector } from './formats/utils/format.detector.js';
// Import Factur-X implementation
import { FacturXDecoder } from './formats/cii/facturx/facturx.decoder.js';
import { FacturXEncoder } from './formats/cii/facturx/facturx.encoder.js';
import { FacturXValidator } from './formats/cii/facturx/facturx.validator.js';
// Export interfaces
export type {
IXInvoice,
IParty,
IAddress,
IContact,
IInvoiceItem,
// Common interfaces
TInvoice,
TCreditNote,
TDebitNote,
TContact,
TLetterEnvelope,
TDocumentEnvelope,
IPdf,
// Validation interfaces
ValidationError,
ValidationResult,
ValidationLevel,
InvoiceFormat,
XInvoiceOptions,
IValidator
} from './interfaces.js';
IValidator,
// Format interfaces
ExportFormat,
XInvoiceOptions
} from './interfaces/common.js';
export { ValidationLevel, InvoiceFormat } from './interfaces/common.js';
// Export interfaces (legacy support)
export { interfaces };
export { common as interfaces };
// Export main class
export { XInvoice };
// Export format classes
export {
// Base classes
BaseDecoder,
DecoderFactory,
// Format-specific encoders
FacturXEncoder,
XInvoiceEncoder,
// Format-specific decoders
FacturXDecoder,
XInvoiceDecoder
};
// Export factories
export { DecoderFactory, EncoderFactory, ValidatorFactory };
// Export validator classes
export const Validators = {
ValidatorFactory,
BaseValidator,
FacturXValidator,
UBLValidator
};
// Export base classes
export { BaseDecoder, BaseEncoder, BaseValidator };
// For backward compatibility
export { FacturXEncoder as ZugferdXmlEncoder };
export { FacturXDecoder as ZUGFeRDXmlDecoder };
// Export UBL base classes
export { UBLBaseDecoder, UBLBaseEncoder, UBLBaseValidator };
// Export CII base classes
export { CIIBaseDecoder, CIIBaseEncoder, CIIBaseValidator };
// Export Factur-X implementation
export { FacturXDecoder, FacturXEncoder, FacturXValidator };
// Export PDF utilities
export { PDFEmbedder, PDFExtractor };
// Export format detector
export { FormatDetector };
/**
* Validates an XML string against the appropriate format rules
@ -71,8 +94,8 @@ export { FacturXDecoder as ZUGFeRDXmlDecoder };
*/
export function validateXml(
xml: string,
level: interfaces.ValidationLevel = interfaces.ValidationLevel.SYNTAX
): interfaces.ValidationResult {
level: common.ValidationLevel = common.ValidationLevel.SYNTAX
): common.ValidationResult {
try {
const validator = ValidatorFactory.createValidator(xml);
return validator.validate(level);
@ -94,4 +117,4 @@ export function validateXml(
*/
export function createXInvoice(): XInvoice {
return new XInvoice();
}
}

View File

@ -11,6 +11,7 @@ export interface IParty {
Name: string;
Address: IAddress;
Contact: IContact;
TaxRegistration?: string;
}
export interface IAddress {
@ -45,6 +46,13 @@ export enum InvoiceFormat {
FATTURAPA = 'fatturapa' // FatturaPA (Italian e-invoice format)
}
/**
* Formats supported for export operations
* This is a subset of InvoiceFormat that only includes formats
* that can be generated and embedded in PDFs
*/
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl';
/**
* Describes a validation level for invoice validation
*/

85
ts/interfaces/common.ts Normal file
View File

@ -0,0 +1,85 @@
import { business, finance } from '@tsclass/tsclass';
/**
* Supported electronic invoice formats
*/
export enum InvoiceFormat {
UNKNOWN = 'unknown',
UBL = 'ubl', // Universal Business Language
CII = 'cii', // Cross-Industry Invoice
ZUGFERD = 'zugferd', // ZUGFeRD (German e-invoice format)
FACTURX = 'facturx', // Factur-X (French e-invoice format)
XRECHNUNG = 'xrechnung', // XRechnung (German e-invoice implementation of EN16931)
FATTURAPA = 'fatturapa' // FatturaPA (Italian e-invoice format)
}
/**
* Formats supported for export operations
* This is a subset of InvoiceFormat that only includes formats
* that can be generated and embedded in PDFs
*/
export type ExportFormat = 'facturx' | 'zugferd' | 'xrechnung' | 'ubl';
/**
* Describes a validation level for invoice validation
*/
export enum ValidationLevel {
SYNTAX = 'syntax', // Schema validation only
SEMANTIC = 'semantic', // Semantic validation (field types, required fields, etc.)
BUSINESS = 'business' // Business rule validation
}
/**
* Describes a validation error
*/
export interface ValidationError {
code: string; // Error code (e.g. "BR-16")
message: string; // Error message
location?: string; // XPath or location in the document
}
/**
* Result of a validation operation
*/
export interface ValidationResult {
valid: boolean; // Overall validation result
errors: ValidationError[]; // List of validation errors
level: ValidationLevel; // The level that was validated
}
/**
* Options for the XInvoice class
*/
export interface XInvoiceOptions {
validateOnLoad?: boolean; // Whether to validate when loading an invoice
validationLevel?: ValidationLevel; // Level of validation to perform
}
/**
* Interface for validator implementations
*/
export interface IValidator {
validate(level?: ValidationLevel): ValidationResult;
isValid(): boolean;
getValidationErrors(): ValidationError[];
}
/**
* PDF interface
*/
export interface IPdf {
name: string;
id: string;
metadata: {
textExtraction: string;
};
buffer: Uint8Array;
}
// Re-export types from tsclass for convenience
export type { TInvoice } from '@tsclass/tsclass/dist_ts/finance';
export type { TCreditNote } from '@tsclass/tsclass/dist_ts/finance';
export type { TDebitNote } from '@tsclass/tsclass/dist_ts/finance';
export type { TContact } from '@tsclass/tsclass/dist_ts/business';
export type { TLetterEnvelope } from '@tsclass/tsclass/dist_ts/business';
export type { TDocumentEnvelope } from '@tsclass/tsclass/dist_ts/business';

View File

@ -1,31 +0,0 @@
// node native
import * as path from 'path';
export {
path
}
// @push.rocks scope
import * as smartfile from '@push.rocks/smartfile';
import * as smartxml from '@push.rocks/smartxml';
export {
smartfile,
smartxml
}
// third party
import * as pako from 'pako';
import * as pdfLib from 'pdf-lib';
export {
pako,
pdfLib
}
// tsclass scope
import * as tsclass from '@tsclass/tsclass';
export {
tsclass
}