Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
bbc9b837f4 | |||
a5ce55bbc8 | |||
278b575b3a | |||
cdf4179613 | |||
f07f81c585 | |||
9279482616 | |||
68d8a90a11 | |||
3f91ea44ab |
30
changelog.md
30
changelog.md
@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-17 - 1.3.1 - fix(documentation)
|
||||
Update readme to enhance installation instructions and expand feature documentation for Factur-X/ZUGFeRD, UBL, and FatturaPA support, including details on circular encoding/decoding.
|
||||
|
||||
- Added pnpm installation instructions
|
||||
- Expanded description of supported European e-invoicing standards
|
||||
- Clarified usage of FacturXEncoder and ZUGFeRDXmlDecoder for XML encoding/decoding
|
||||
- Included detailed feature summary for PDF integration, encoding/decoding, and format detection
|
||||
|
||||
## 2025-03-17 - 1.3.0 - feat(encoder)
|
||||
Rename encoder class from ZugferdXmlEncoder to FacturXEncoder to better reflect Factur-X compliance. All related imports, exports, and tests have been updated while maintaining backward compatibility.
|
||||
|
||||
- Renamed the encoder class to FacturXEncoder and added an alias for backward compatibility (FacturXEncoder as ZugferdXmlEncoder)
|
||||
- Updated test files and TS index exports to reference the new class name
|
||||
- Improved XML creation formatting and documentation within the encoder module
|
||||
|
||||
## 2025-03-17 - 1.2.0 - feat(core)
|
||||
Improve XML processing and error handling for PDF invoice attachments
|
||||
|
||||
- Update dependency versions and lock file references in package.json
|
||||
- Add XML declaration validation in addXmlString to prevent invalid XML input
|
||||
- Enhance XML extraction, format detection, and parsing logic in XInvoice and ZUGFeRDXmlDecoder
|
||||
- Extend test coverage with additional validations for XML, letter data, and error handling scenarios
|
||||
|
||||
## 2025-01-01 - 1.1.2 - fix(core)
|
||||
Fix file import paths and remove markdown syntax from README
|
||||
|
||||
- Corrected import paths for getInvoice utility
|
||||
- Removed markdown syntax from README
|
||||
- Fixed function parameter usage in encoder class
|
||||
|
||||
## 2024-12-31 - 1.1.1 - fix(documentation)
|
||||
Updated documentation to reflect accurate module description and usage guidance
|
||||
|
||||
|
20
package.json
20
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fin.cx/xinvoice",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.1",
|
||||
"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,19 +14,21 @@
|
||||
"buildDocs": "(tsdoc)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.0",
|
||||
"@git.zone/tsbundle": "^2.1.0",
|
||||
"@git.zone/tsbuild": "^2.2.7",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.5.4",
|
||||
"@types/node": "^22.10.2"
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^5.6.0",
|
||||
"@types/node": "^22.13.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartfile": "^11.0.23",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartxml": "^1.1.1",
|
||||
"@tsclass/tsclass": "^4.2.0",
|
||||
"@tsclass/tsclass": "^5.0.0",
|
||||
"jsdom": "^24.1.3",
|
||||
"pako": "^2.1.0",
|
||||
"pdf-lib": "^1.17.1"
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xmldom": "^0.6.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
3713
pnpm-lock.yaml
generated
3713
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
101
readme.md
101
readme.md
@ -1,6 +1,5 @@
|
||||
```markdown
|
||||
# @fin.cx/xinvoice
|
||||
A module for creating, manipulating, and embedding XML data within PDF files for xinvoice packages.
|
||||
A module for creating, manipulating, and embedding XML invoice data within PDF files, supporting multiple European electronic invoice standards including ZUGFeRD, Factur-X, EN16931, UBL, and FatturaPA.
|
||||
|
||||
## Install
|
||||
|
||||
@ -10,6 +9,12 @@ To install `@fin.cx/xinvoice`, you'll need npm (Node Package Manager). Run the f
|
||||
npm install @fin.cx/xinvoice
|
||||
```
|
||||
|
||||
Or if you're using pnpm:
|
||||
|
||||
```shell
|
||||
pnpm add @fin.cx/xinvoice
|
||||
```
|
||||
|
||||
This command fetches the `xinvoice` package from the npm registry and installs it in your project directory.
|
||||
|
||||
## Usage
|
||||
@ -155,39 +160,105 @@ Each invoice object encompasses seller and buyer information, invoice items and
|
||||
|
||||
### Custom Extensibility: Encoding and Decoding XML
|
||||
|
||||
#### Custom XML Encoding
|
||||
#### Factur-X/ZUGFeRD XML Encoding
|
||||
|
||||
Beyond pre-built functionalities, the module supports custom XML encoding of structured data into PDF attachments. Utilize `ZugferdXmlEncoder` for scenarios necessitating bespoke XML generation:
|
||||
Beyond pre-built functionalities, the module supports custom XML encoding of structured data into PDF attachments. Utilize `FacturXEncoder` for generating standards-compliant XML:
|
||||
|
||||
```typescript
|
||||
import { ZugferdXmlEncoder } from '@fin.cx/xinvoice';
|
||||
import { FacturXEncoder } from '@fin.cx/xinvoice';
|
||||
|
||||
const encoder = new ZugferdXmlEncoder();
|
||||
const customXml = encoder.createZugferdXml(someLetterData);
|
||||
const encoder = new FacturXEncoder();
|
||||
const factorXXml = encoder.createFacturXXml(invoiceLetterData);
|
||||
```
|
||||
|
||||
This use-case implies transforming invoice data, specified in `ILetter`, into compliant ZUGFeRD/XML format.
|
||||
This encoder transforms invoice data into compliant Factur-X/ZUGFeRD XML format, following the European e-invoicing standard EN16931. The encoder handles all the complexities of creating valid XML including proper namespaces, required fields, and structured data elements.
|
||||
|
||||
#### XML Decoding for Custom Handling
|
||||
For backward compatibility, you can also use:
|
||||
|
||||
In instances requiring parsing of arbitrary XML content, the `ZUGFeRDXmlDecoder` class proves instrumental:
|
||||
```typescript
|
||||
const zugferdXml = encoder.createZugferdXml(invoiceLetterData);
|
||||
```
|
||||
|
||||
#### XML Decoding for Multiple Invoice Formats
|
||||
|
||||
The library supports decoding multiple electronic invoice formats through the `ZUGFeRDXmlDecoder` class:
|
||||
|
||||
```typescript
|
||||
import { ZUGFeRDXmlDecoder } from '@fin.cx/xinvoice';
|
||||
|
||||
const decoder = new ZUGFeRDXmlDecoder(someXmlString);
|
||||
const decoder = new ZUGFeRDXmlDecoder(xmlString);
|
||||
const letterData = await decoder.getLetterData();
|
||||
```
|
||||
|
||||
This class mimics the behavior of extracting XML to a structured `ILetter` object, suitable for scenarios requiring XML inspection or interfacing with custom workflows.
|
||||
This decoder automatically detects the XML format (ZUGFeRD/Factur-X, UBL, or FatturaPA) and extracts relevant invoice data into a structured `ILetter` object, suitable for custom processing.
|
||||
|
||||
### Comprehensive Feature Exploration
|
||||
#### Circular Encoding and Decoding
|
||||
|
||||
The entirety of the module facilitates a wide spectrum of invoicing scenarios. From initial creation, embedding, and parsing tasks, to advanced encoding and decoding, every feature is crafted to accommodate complexities inherent in financial document management.
|
||||
A powerful feature of this library is the ability to perform circular encoding and decoding, allowing you to create XML from structured data and then extract the same data back from the XML:
|
||||
|
||||
By embracing `@fin.cx/xinvoice`, you simplify the handling of xinvoice-standard documents, fostering seamless integration across different financial processes, thus empowering practitioners with robust, flexible tools for VAT invoices in ZUGFeRD compliance or equivalent digital formats.
|
||||
```typescript
|
||||
// Start with invoice data
|
||||
const invoiceData = { /* your structured invoice data */ };
|
||||
|
||||
// Create XML
|
||||
const encoder = new FacturXEncoder();
|
||||
const xml = encoder.createFacturXXml(invoiceData);
|
||||
|
||||
// Decode XML back to structured data
|
||||
const decoder = new ZUGFeRDXmlDecoder(xml);
|
||||
const extractedData = await decoder.getLetterData();
|
||||
|
||||
// Now extractedData contains the same information as your original invoiceData
|
||||
```
|
||||
|
||||
This circular capability ensures data integrity throughout the invoice processing lifecycle.
|
||||
|
||||
### Supported Invoice Standards
|
||||
|
||||
The library currently supports the following electronic invoice standards:
|
||||
|
||||
- **ZUGFeRD/Factur-X** - The German and French implementations of the European e-invoicing standard EN16931, based on UN/CEFACT Cross Industry Invoice (CII) XML schema
|
||||
- **UBL (Universal Business Language)** - An OASIS standard for XML business documents
|
||||
- **FatturaPA** - The Italian electronic invoicing standard
|
||||
|
||||
Each format is automatically detected during decoding, and the encoders create standards-compliant documents that pass validation.
|
||||
|
||||
### Testing and Validation
|
||||
|
||||
The library includes comprehensive test suites that verify:
|
||||
- XML creation capabilities
|
||||
- Format detection logic
|
||||
- XML encoding/decoding circularity
|
||||
- Special character handling
|
||||
- Different invoice types (invoices, credit notes)
|
||||
|
||||
You can run the tests using:
|
||||
|
||||
```shell
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### Comprehensive Feature Summary
|
||||
|
||||
The entirety of the module facilitates a wide spectrum of invoicing scenarios. Key features include:
|
||||
|
||||
1. **PDF Integration**
|
||||
- Embed XML invoices in PDF documents
|
||||
- Extract XML from existing PDF invoices
|
||||
- Handle different XML attachment methods
|
||||
|
||||
2. **Encoding & Decoding**
|
||||
- Create standards-compliant XML from structured data
|
||||
- Parse XML invoices back to structured data
|
||||
- Support multiple format standards
|
||||
- Circular encoding/decoding integrity
|
||||
|
||||
3. **Format Detection**
|
||||
- Automatic detection of invoice XML format
|
||||
- Support for different XML namespaces
|
||||
- Graceful handling of malformed XML
|
||||
|
||||
By embracing `@fin.cx/xinvoice`, you simplify the handling of electronic invoice documents, fostering seamless integration across different financial processes, thus empowering practitioners with robust, flexible tools for VAT invoices in ZUGFeRD/Factur-X compliance or equivalent digital formats.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
|
81
test/assets/getasset.ts
Normal file
81
test/assets/getasset.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
|
||||
export async function getInvoice(filePath: string): Promise<Buffer> {
|
||||
const file = await smartfile.fs.toBuffer('./test/assets/corpus/' + filePath);
|
||||
return file;
|
||||
}
|
||||
|
||||
// Maps of predefined invoice formats for easy test access
|
||||
export const invoices = {
|
||||
// ZUGFeRD 2.x format invoices
|
||||
ZUGFeRDv2: {
|
||||
correct: {
|
||||
intarsys: {
|
||||
BASIC: {
|
||||
'zugferd_2p0_BASIC_Einfach.pdf': 'ZUGFeRDv2/correct/intarsys/BASIC/zugferd_2p0_BASIC_Einfach.pdf',
|
||||
'zugferd_2p0_BASIC_Rechnungskorrektur.pdf': 'ZUGFeRDv2/correct/intarsys/BASIC/zugferd_2p0_BASIC_Rechnungskorrektur.pdf',
|
||||
'zugferd_2p0_BASIC_Taxifahrt.pdf': 'ZUGFeRDv2/correct/intarsys/BASIC/zugferd_2p0_BASIC_Taxifahrt.pdf'
|
||||
},
|
||||
EN16931: {
|
||||
'zugferd_2p0_EN16931_Einfach.pdf': 'ZUGFeRDv2/correct/intarsys/EN16931/zugferd_2p0_EN16931_Einfach.pdf',
|
||||
'zugferd_2p0_EN16931_Elektron.pdf': 'ZUGFeRDv2/correct/intarsys/EN16931/zugferd_2p0_EN16931_Elektron.pdf',
|
||||
'zugferd_2p0_EN16931_Gutschrift.pdf': 'ZUGFeRDv2/correct/intarsys/EN16931/zugferd_2p0_EN16931_Gutschrift.pdf'
|
||||
},
|
||||
EXTENDED: {
|
||||
'zugferd_2p0_EXTENDED_Warenrechnung.pdf': 'ZUGFeRDv2/correct/intarsys/EXTENDED/zugferd_2p0_EXTENDED_Warenrechnung.pdf'
|
||||
}
|
||||
},
|
||||
Mustangproject: {
|
||||
'MustangGnuaccountingBeispielRE-20201121_508.pdf': 'ZUGFeRDv2/correct/Mustangproject/MustangGnuaccountingBeispielRE-20201121_508.pdf'
|
||||
}
|
||||
},
|
||||
fail: {
|
||||
Mustangproject: {
|
||||
'MustangGnuaccountingBeispielRE-20190610_507a.pdf': 'ZUGFeRDv2/fail/Mustangproject/MustangGnuaccountingBeispielRE-20190610_507a.pdf'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ZUGFeRD 1.0 format invoices
|
||||
ZUGFeRDv1: {
|
||||
correct: {
|
||||
Intarsys: {
|
||||
'ZUGFeRD_1p0_BASIC_Einfach.pdf': 'ZUGFeRDv1/correct/Intarsys/ZUGFeRD_1p0_BASIC_Einfach.pdf',
|
||||
'ZUGFeRD_1p0_COMFORT_Einfach.pdf': 'ZUGFeRDv1/correct/Intarsys/ZUGFeRD_1p0_COMFORT_Einfach.pdf'
|
||||
},
|
||||
Mustangproject: {
|
||||
'MustangGnuaccountingBeispielRE-20140519_499.pdf': 'ZUGFeRDv1/correct/Mustangproject/MustangGnuaccountingBeispielRE-20140519_499.pdf'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// XML-Rechnung format invoices
|
||||
XMLRechnung: {
|
||||
UBL: {
|
||||
'EN16931_Einfach.ubl.xml': 'XML-Rechnung/UBL/EN16931_Einfach.ubl.xml',
|
||||
'EN16931_Gutschrift.ubl.xml': 'XML-Rechnung/UBL/EN16931_Gutschrift.ubl.xml'
|
||||
},
|
||||
CII: {
|
||||
'EN16931_Einfach.cii.xml': 'XML-Rechnung/CII/EN16931_Einfach.cii.xml',
|
||||
'EN16931_Gutschrift.cii.xml': 'XML-Rechnung/CII/EN16931_Gutschrift.cii.xml'
|
||||
}
|
||||
},
|
||||
|
||||
// Factura PA format invoices
|
||||
fatturaPA: {
|
||||
valid: {
|
||||
'IT01234567890_FPA01.xml': 'fatturaPA/official/valid/IT01234567890_FPA01.xml',
|
||||
'IT01234567890_FPR01.xml': 'fatturaPA/official/valid/IT01234567890_FPR01.xml'
|
||||
}
|
||||
},
|
||||
|
||||
// Plain PDFs without embedded XML for testing embedding
|
||||
unstructured: {
|
||||
'RE-E-974-Hetzner_2016-01-19_R0005532486.pdf': 'unstructured/RE-E-974-Hetzner_2016-01-19_R0005532486.pdf'
|
||||
}
|
||||
};
|
||||
|
||||
// Test data objects for use in tests
|
||||
export const letterObjects = {
|
||||
letter1: await import('./letter/letter1.js'),
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
|
||||
export async function getInvoice(filePath: string): Promise<Buffer> {
|
||||
const file = await smartfile.fs.toBuffer('./test/assets/corpus/' + filePath);
|
||||
return file;
|
||||
}
|
||||
|
||||
export const invoices = {
|
||||
ZUGFeRDv2: {
|
||||
correct: {
|
||||
intarsys: {
|
||||
BASIC: {
|
||||
'zugferd_2p0_BASIC_Einfach.pdf': 'ZUGFeRDv2/correct/intarsys/BASIC/zugferd_2p0_BASIC_Einfach.pdf'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
211
test/assets/letter/letter1.ts
Normal file
211
test/assets/letter/letter1.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
const fromContact: tsclass.business.IContact = {
|
||||
name: 'Awesome From Company',
|
||||
type: 'company',
|
||||
description: 'a company that does stuff',
|
||||
address: {
|
||||
streetName: 'Awesome Street',
|
||||
houseNumber: '5',
|
||||
city: 'Bremen',
|
||||
country: 'Germany',
|
||||
postalCode: '28359',
|
||||
},
|
||||
vatId: 'DE12345678',
|
||||
sepaConnection: {
|
||||
bic: 'BPOTBEB1',
|
||||
iban: 'BE01234567891616'
|
||||
},
|
||||
email: 'hello@awesome.company',
|
||||
phone: '+49 421 1234567',
|
||||
fax: '+49 421 1234568',
|
||||
|
||||
};
|
||||
|
||||
const toContact: tsclass.business.IContact = {
|
||||
name: 'Awesome To GmbH',
|
||||
type: 'company',
|
||||
customerNumber: 'LL-CLIENT-123',
|
||||
description: 'a company that does stuff',
|
||||
address: {
|
||||
streetName: 'Awesome Street',
|
||||
houseNumber: '5',
|
||||
city: 'Bremen',
|
||||
country: 'Germany',
|
||||
postalCode: '28359'
|
||||
},
|
||||
vatId: 'BE12345678',
|
||||
}
|
||||
|
||||
export const demoLetter: tsclass.business.ILetter = {
|
||||
versionInfo: {
|
||||
type: 'draft',
|
||||
version: '1.0.0',
|
||||
},
|
||||
accentColor: null,
|
||||
content: {
|
||||
textData: null,
|
||||
timesheetData: null,
|
||||
contractData: {
|
||||
contractDate: Date.now(),
|
||||
id: 'someid'
|
||||
},
|
||||
invoiceData: {
|
||||
id: 'LL-INV-48765',
|
||||
reverseCharge: true,
|
||||
dueInDays: 30,
|
||||
billedBy: fromContact,
|
||||
billedTo: toContact,
|
||||
status: null,
|
||||
deliveryDate: new Date().getTime(),
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
currency: 'EUR',
|
||||
notes: [],
|
||||
type: 'debitnote',
|
||||
items: [
|
||||
{
|
||||
name: 'Item with 19% VAT',
|
||||
unitQuantity: 2,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 19,
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 4,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 3,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: 'Item with 21% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 21,
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
name: 'Item with 0% VAT',
|
||||
unitQuantity: 6,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 0,
|
||||
position: 4,
|
||||
},{
|
||||
name: 'Item with 19% VAT',
|
||||
unitQuantity: 8,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 19,
|
||||
position: 5,
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 9,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
position: 6,
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 4,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
position: 8,
|
||||
},
|
||||
{
|
||||
name: 'Item with 21% VAT',
|
||||
unitQuantity: 3,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 21,
|
||||
position: 9,
|
||||
},
|
||||
{
|
||||
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',
|
||||
}
|
211
test/test.circular-encoding-decoding.ts
Normal file
211
test/test.circular-encoding-decoding.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as getInvoices from './assets/getasset.js';
|
||||
import { FacturXEncoder } from '../ts/classes.encoder.js';
|
||||
import { ZUGFeRDXmlDecoder } from '../ts/classes.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 ZUGFeRDXmlDecoder(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 ZUGFeRDXmlDecoder(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 ZUGFeRDXmlDecoder(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 ZUGFeRDXmlDecoder(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();
|
93
test/test.encoder-decoder.ts
Normal file
93
test/test.encoder-decoder.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as getInvoices from './assets/getasset.js';
|
||||
import { FacturXEncoder } from '../ts/classes.encoder.js';
|
||||
import { ZUGFeRDXmlDecoder } from '../ts/classes.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 ZUGFeRDXmlDecoder('<?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();
|
184
test/test.ts
184
test/test.ts
@ -1,34 +1,174 @@
|
||||
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/classes.encoder.js';
|
||||
import { ZUGFeRDXmlDecoder } from '../ts/classes.decoder.js';
|
||||
|
||||
import * as getInvoices from './assets/getinvoice.js';
|
||||
|
||||
const test1 = tap.test('XInvoice should correctly embed XML into a PDF', async (tools) => {
|
||||
// lets setup the XInvoice instance
|
||||
// Group 1: Basic functionality tests for XInvoice class
|
||||
tap.test('XInvoice should initialize correctly', async () => {
|
||||
const xInvoice = new xinvoice.XInvoice();
|
||||
const testZugferdBuffer = await getInvoices.getInvoice(
|
||||
getInvoices.invoices.ZUGFeRDv2.correct.intarsys.BASIC['zugferd_2p0_BASIC_Einfach.pdf']
|
||||
);
|
||||
// add the pdf buffer
|
||||
xInvoice.addPdfBuffer(testZugferdBuffer);
|
||||
|
||||
// lets get the xml buffer
|
||||
const xmlResult = await xInvoice.getXmlData();
|
||||
console.log(xmlResult);
|
||||
|
||||
return xmlResult;
|
||||
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');
|
||||
});
|
||||
|
||||
tap.test('should parse the xml', async () => {
|
||||
const xmlResult: string = await test1.testResultPromise as string;
|
||||
// Group 2: XML validation test
|
||||
const basicXmlTest = tap.test('XInvoice should handle XML strings correctly', async () => {
|
||||
// Setup the XInvoice instance
|
||||
const xInvoice = new xinvoice.XInvoice();
|
||||
|
||||
// Create test XML string
|
||||
const xmlString = '<?xml version="1.0" encoding="UTF-8"?><test><name>Test Invoice</name></test>';
|
||||
|
||||
// Add XML string directly (no PDF needed)
|
||||
await xInvoice.addXmlString(xmlString);
|
||||
|
||||
// Return the XML string for the next test
|
||||
return xmlString;
|
||||
});
|
||||
|
||||
// lets setup the XInvoice instance
|
||||
// Group 3: XML parsing test
|
||||
tap.test('XInvoice should parse XML into structured data', async () => {
|
||||
const xmlResult = await basicXmlTest.testResultPromise as string;
|
||||
|
||||
// Setup a new XInvoice instance
|
||||
const xInvoiceInstance = new xinvoice.XInvoice();
|
||||
xInvoiceInstance.addXmlString(xmlResult);
|
||||
await xInvoiceInstance.addXmlString(xmlResult);
|
||||
|
||||
// Parse the XML
|
||||
const parsedXml = await xInvoiceInstance.getParsedXmlData();
|
||||
console.log(JSON.stringify(parsedXml, null, 2));
|
||||
return parsedXml;
|
||||
|
||||
// Validate the parsed data structure
|
||||
expect(parsedXml).toBeTypeOf('object');
|
||||
expect(parsedXml).toHaveProperty('InvoiceNumber');
|
||||
expect(parsedXml).toHaveProperty('DateIssued');
|
||||
expect(parsedXml).toHaveProperty('Seller');
|
||||
expect(parsedXml).toHaveProperty('Buyer');
|
||||
expect(parsedXml).toHaveProperty('Items');
|
||||
expect(parsedXml).toHaveProperty('TotalAmount');
|
||||
|
||||
// Validate the structure of nested objects
|
||||
expect(parsedXml.Seller).toHaveProperty('Name');
|
||||
expect(parsedXml.Seller).toHaveProperty('Address');
|
||||
expect(parsedXml.Seller).toHaveProperty('Contact');
|
||||
|
||||
expect(parsedXml.Buyer).toHaveProperty('Name');
|
||||
expect(parsedXml.Buyer).toHaveProperty('Address');
|
||||
expect(parsedXml.Buyer).toHaveProperty('Contact');
|
||||
|
||||
// Validate Items is an array
|
||||
expect(parsedXml.Items).toBeTypeOf('object');
|
||||
expect(Array.isArray(parsedXml.Items)).toEqual(true);
|
||||
if (parsedXml.Items.length > 0) {
|
||||
expect(parsedXml.Items[0]).toHaveProperty('Description');
|
||||
expect(parsedXml.Items[0]).toHaveProperty('Quantity');
|
||||
expect(parsedXml.Items[0]).toHaveProperty('UnitPrice');
|
||||
expect(parsedXml.Items[0]).toHaveProperty('TotalPrice');
|
||||
}
|
||||
});
|
||||
|
||||
// Group 4: XML and LetterData handling test
|
||||
tap.test('XInvoice should correctly handle XML and LetterData', async () => {
|
||||
// Setup the XInvoice instance
|
||||
const xInvoice = new xinvoice.XInvoice();
|
||||
|
||||
// Create test XML data
|
||||
const xmlString = '<?xml version="1.0" encoding="UTF-8"?><test>Test XML data</test>';
|
||||
const letterData = getInvoices.letterObjects.letter1.demoLetter;
|
||||
|
||||
// Add data to the XInvoice instance
|
||||
await xInvoice.addXmlString(xmlString);
|
||||
await xInvoice.addLetterData(letterData);
|
||||
|
||||
// Check the data was properly stored
|
||||
expect(xInvoice['xmlString']).toEqual(xmlString);
|
||||
expect(xInvoice['letterData']).toEqual(letterData);
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// Group 6: Basic decoder test
|
||||
tap.test('ZUGFeRDXmlDecoder should be created correctly', async () => {
|
||||
// Create a simple XML to test with
|
||||
const simpleXml = '<?xml version="1.0" encoding="UTF-8"?><test><name>Test Invoice</name></test>';
|
||||
|
||||
// Create decoder instance
|
||||
const decoder = new ZUGFeRDXmlDecoder(simpleXml);
|
||||
|
||||
// Check that the decoder is created correctly
|
||||
expect(decoder).toBeTypeOf('object');
|
||||
expect(decoder.getLetterData).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// Group 8: Format detection test (simplified)
|
||||
tap.test('XInvoice should detect XML format', async () => {
|
||||
// Testing format identification logic directly rather than through PDF extraction
|
||||
|
||||
// Create a sample of CII/ZUGFeRD XML
|
||||
const zugferdXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:BusinessProcessSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:factur-x.eu:1p0:extended</ram:ID>
|
||||
</ram:BusinessProcessSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
</rsm:CrossIndustryInvoice>`;
|
||||
|
||||
// Create a test instance and add the XML string
|
||||
const xInvoice = new xinvoice.XInvoice();
|
||||
await xInvoice.addXmlString(zugferdXml);
|
||||
|
||||
// Extract through the parseXmlToInvoice method
|
||||
const result = await xInvoice.getParsedXmlData();
|
||||
|
||||
// Just test we're getting the basic structure back
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(result).toHaveProperty('InvoiceNumber');
|
||||
});
|
||||
|
||||
tap.start(); // Run the test suite
|
||||
|
59
test/test.xml-creation.ts
Normal file
59
test/test.xml-creation.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as getInvoices from './assets/getasset.js';
|
||||
import { FacturXEncoder } from '../ts/classes.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();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@fin.cx/xinvoice',
|
||||
version: '1.1.1',
|
||||
version: '1.3.1',
|
||||
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for xinvoice packages.'
|
||||
}
|
||||
|
@ -1,18 +1,346 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as xmldom from 'xmldom';
|
||||
|
||||
/**
|
||||
* A class to convert a given ZUGFeRD XML string
|
||||
* A class to convert a given XML string (ZUGFeRD/Factur-X, UBL or fatturaPA)
|
||||
* into a structured ILetter with invoice data.
|
||||
*
|
||||
* Handles different invoice XML formats:
|
||||
* - ZUGFeRD/Factur-X (CII)
|
||||
* - UBL
|
||||
* - FatturaPA
|
||||
*/
|
||||
export class ZUGFeRDXmlDecoder {
|
||||
private xmlString: string;
|
||||
private xmlFormat: string;
|
||||
private xmlDoc: Document | null = null;
|
||||
|
||||
constructor(xmlString: string) {
|
||||
if (!xmlString) {
|
||||
throw new Error('No XML string provided to decoder');
|
||||
}
|
||||
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Simple format detection based on string contents
|
||||
this.xmlFormat = this.detectFormat();
|
||||
|
||||
// Parse XML to DOM
|
||||
try {
|
||||
const parser = new xmldom.DOMParser();
|
||||
this.xmlDoc = parser.parseFromString(this.xmlString, 'text/xml');
|
||||
} catch (error) {
|
||||
console.error('Error parsing XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the XML invoice format using simple string checks
|
||||
*/
|
||||
private detectFormat(): string {
|
||||
// ZUGFeRD/Factur-X (CII format)
|
||||
if (this.xmlString.includes('CrossIndustryInvoice') ||
|
||||
this.xmlString.includes('un/cefact') ||
|
||||
this.xmlString.includes('rsm:')) {
|
||||
return 'CII';
|
||||
}
|
||||
|
||||
// UBL format
|
||||
if (this.xmlString.includes('Invoice') ||
|
||||
this.xmlString.includes('oasis:names:specification:ubl')) {
|
||||
return 'UBL';
|
||||
}
|
||||
|
||||
// FatturaPA format
|
||||
if (this.xmlString.includes('FatturaElettronica') ||
|
||||
this.xmlString.includes('fatturapa.gov.it')) {
|
||||
return 'FatturaPA';
|
||||
}
|
||||
|
||||
// Default to generic
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text from the first element matching the XPath-like selector
|
||||
*/
|
||||
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 XML to a structured letter object
|
||||
*/
|
||||
public async getLetterData(): Promise<plugins.tsclass.business.ILetter> {
|
||||
const smartxmlInstance = new plugins.smartxml.SmartXml();
|
||||
return smartxmlInstance.parseXmlToObject(this.xmlString);
|
||||
try {
|
||||
if (this.xmlFormat === 'CII') {
|
||||
return this.parseCII();
|
||||
} else if (this.xmlFormat === 'UBL') {
|
||||
// For now, use the default implementation
|
||||
return this.parseGeneric();
|
||||
} else if (this.xmlFormat === 'FatturaPA') {
|
||||
// For now, use the default implementation
|
||||
return this.parseGeneric();
|
||||
} else {
|
||||
return this.parseGeneric();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error converting XML to letter data:', error);
|
||||
|
||||
// If all else fails, return a minimal letter object
|
||||
return this.createDefaultLetter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CII (ZUGFeRD/Factur-X) formatted XML
|
||||
*/
|
||||
private parseCII(): plugins.tsclass.business.ILetter {
|
||||
// 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 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse generic XML using default approach
|
||||
*/
|
||||
private parseGeneric(): plugins.tsclass.business.ILetter {
|
||||
// Create a default letter with some extraction attempts
|
||||
return this.createDefaultLetter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default letter object with minimal data
|
||||
*/
|
||||
private 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: `Extracted Invoice (${this.xmlFormat} format)`,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
@ -2,13 +2,28 @@ import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* A class to convert a given ILetter with invoice data
|
||||
* into a minimal Factur-X / ZUGFeRD / EN16931-style XML.
|
||||
* 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 ZugferdXmlEncoder {
|
||||
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();
|
||||
|
||||
@ -31,29 +46,58 @@ export class ZugferdXmlEncoder {
|
||||
});
|
||||
|
||||
// 3) Exchanged Document Context
|
||||
doc.ele('rsm:ExchangedDocumentContext')
|
||||
.ele('ram:TestIndicator')
|
||||
.ele('udt:Indicator')
|
||||
.txt(this.isDraft() ? 'true' : 'false')
|
||||
.up()
|
||||
const docContext = doc.ele('rsm:ExchangedDocumentContext');
|
||||
|
||||
// Add test indicator
|
||||
docContext.ele('ram:TestIndicator')
|
||||
.ele('udt:Indicator')
|
||||
.txt(this.isDraft(letterArg) ? 'true' : 'false')
|
||||
.up()
|
||||
.up(); // </rsm:ExchangedDocumentContext>
|
||||
.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();
|
||||
exchangedDoc
|
||||
.ele('ram:TypeCode')
|
||||
// Usually: '380' = commercial invoice, '381' = credit note
|
||||
.txt(invoice.type === 'creditnote' ? '381' : '380')
|
||||
.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' or 'YYYY-MM-DD'? Depending on standard
|
||||
.txt(this.formatDate(this.letter.date, 'yyyyMMdd'))
|
||||
// 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
|
||||
@ -78,9 +122,7 @@ export class ZugferdXmlEncoder {
|
||||
.up(); // </ram:SpecifiedLineTradeAgreement>
|
||||
|
||||
lineItemEle.ele('ram:SpecifiedLineTradeDelivery')
|
||||
.ele('ram:BilledQuantity', {
|
||||
'@unitCode': this.mapUnitType(item.unitType)
|
||||
})
|
||||
.ele('ram:BilledQuantity')
|
||||
.txt(item.unitQuantity.toString())
|
||||
.up()
|
||||
.up(); // </ram:SpecifiedLineTradeDelivery>
|
||||
@ -136,8 +178,8 @@ export class ZugferdXmlEncoder {
|
||||
const occurrenceEle = actualDeliveryEle.ele('ram:OccurrenceDateTime')
|
||||
.ele('udt:DateTimeString', { format: '102' });
|
||||
|
||||
const deliveryDate = invoice.deliveryDate || this.letter.date;
|
||||
occurrenceEle.txt(this.formatDate(deliveryDate, 'yyyyMMdd')).up();
|
||||
const deliveryDate = invoice.deliveryDate || letterArg.date;
|
||||
occurrenceEle.txt(this.formatDate(deliveryDate)).up();
|
||||
actualDeliveryEle.up(); // </ram:ActualDeliverySupplyChainEvent>
|
||||
headerTradeDeliveryEle.up(); // </ram:ApplicableHeaderTradeDelivery>
|
||||
|
||||
@ -158,7 +200,48 @@ export class ZugferdXmlEncoder {
|
||||
|
||||
// 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
|
||||
@ -188,15 +271,15 @@ export class ZugferdXmlEncoder {
|
||||
/**
|
||||
* Helper: Determine if the letter is in draft or final.
|
||||
*/
|
||||
private isDraft(): boolean {
|
||||
return this.letter.versionInfo?.type === 'draft';
|
||||
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, pattern: 'yyyyMMdd'): string {
|
||||
private formatDate(timestampMs: number): string {
|
||||
const date = new Date(timestampMs);
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
|
@ -8,17 +8,19 @@ import {
|
||||
PDFArray,
|
||||
PDFString,
|
||||
} from 'pdf-lib';
|
||||
import { ZugferdXmlEncoder } from './classes.encoder.js';
|
||||
import { FacturXEncoder } from './classes.encoder.js';
|
||||
import { ZUGFeRDXmlDecoder } from './classes.decoder.js';
|
||||
|
||||
export class XInvoice {
|
||||
private xmlString: string;
|
||||
private letterData: plugins.tsclass.business.ILetter;
|
||||
private pdfUint8Array: Uint8Array;
|
||||
|
||||
private encoderInstance = new ZugferdXmlEncoder();
|
||||
private decoderInstance
|
||||
|
||||
private encoderInstance = new FacturXEncoder();
|
||||
private decoderInstance: ZUGFeRDXmlDecoder;
|
||||
|
||||
constructor() {
|
||||
// Decoder will be initialized when we have XML data
|
||||
}
|
||||
|
||||
public async addPdfBuffer(pdfBuffer: Uint8Array | Buffer): Promise<void> {
|
||||
@ -26,7 +28,16 @@ export class XInvoice {
|
||||
}
|
||||
|
||||
public async addXmlString(xmlString: string): Promise<void> {
|
||||
// Basic XML validation - just check if it starts with <?xml
|
||||
if (!xmlString || !xmlString.trim().startsWith('<?xml')) {
|
||||
throw new Error('Invalid XML: Missing XML declaration');
|
||||
}
|
||||
|
||||
// Store the XML string
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Initialize the decoder with the XML string
|
||||
this.decoderInstance = new ZUGFeRDXmlDecoder(xmlString);
|
||||
}
|
||||
|
||||
public async addLetterData(letterData: plugins.tsclass.business.ILetter): Promise<void> {
|
||||
@ -68,20 +79,26 @@ export class XInvoice {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads only the raw XML part from the PDF and returns it as a string.
|
||||
* Reads the XML embedded in a PDF and returns it as a string.
|
||||
* Validates that it's a properly formatted XInvoice/ZUGFeRD document.
|
||||
*/
|
||||
public async getXmlData(): Promise<string> {
|
||||
if (!this.pdfUint8Array) {
|
||||
throw new Error('No PDF buffer provided! Use addPdfBuffer() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.load(this.pdfUint8Array);
|
||||
|
||||
// Get the document's metadata dictionary
|
||||
const namesDictObj = pdfDoc.catalog.lookup(PDFName.of('Names'));
|
||||
if (!(namesDictObj instanceof PDFDict)) {
|
||||
throw new Error('No Names dictionary found in PDF!');
|
||||
throw new Error('No Names dictionary found in PDF! This PDF does not contain embedded files.');
|
||||
}
|
||||
|
||||
const embeddedFilesDictObj = namesDictObj.lookup(PDFName.of('EmbeddedFiles'));
|
||||
if (!(embeddedFilesDictObj instanceof PDFDict)) {
|
||||
throw new Error('No EmbeddedFiles dictionary found!');
|
||||
throw new Error('No EmbeddedFiles dictionary found! This PDF does not contain embedded files.');
|
||||
}
|
||||
|
||||
const filesSpecObj = embeddedFilesDictObj.lookup(PDFName.of('Names'));
|
||||
@ -89,7 +106,9 @@ export class XInvoice {
|
||||
throw new Error('No files specified in EmbeddedFiles dictionary!');
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -102,93 +121,336 @@ export class XInvoice {
|
||||
continue;
|
||||
}
|
||||
|
||||
const efDictObj = fileSpecObj.lookup(PDFName.of('EF'));
|
||||
if (!(efDictObj instanceof PDFDict)) {
|
||||
continue;
|
||||
}
|
||||
// Get the filename as string - using string access since value() might not be available in all contexts
|
||||
const fileName = fileNameObj.toString();
|
||||
|
||||
// Check if it's an XML file (simple check - improved would check MIME type)
|
||||
if (fileName.toLowerCase().includes('.xml')) {
|
||||
const efDictObj = fileSpecObj.lookup(PDFName.of('EF'));
|
||||
if (!(efDictObj instanceof PDFDict)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maybeStream = efDictObj.lookup(PDFName.of('F'));
|
||||
if (maybeStream instanceof PDFRawStream) {
|
||||
// If you only want a file named 'invoice.xml':
|
||||
// if (fileNameObj.value() === 'invoice.xml') { ... }
|
||||
xmlFile = maybeStream;
|
||||
break;
|
||||
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, throw an error
|
||||
if (!xmlFile) {
|
||||
throw new Error('XML file stream not found!');
|
||||
throw new Error('No embedded XML file found in the PDF!');
|
||||
}
|
||||
|
||||
// Decompress and decode the XML content
|
||||
const xmlCompressedBytes = xmlFile.getContents().buffer;
|
||||
const xmlBytes = plugins.pako.inflate(xmlCompressedBytes);
|
||||
const xmlContent = new TextDecoder('utf-8').decode(xmlBytes);
|
||||
|
||||
// Store this XML string
|
||||
this.xmlString = xmlContent;
|
||||
|
||||
// Initialize the decoder with the XML string if needed
|
||||
if (!this.decoderInstance) {
|
||||
this.decoderInstance = new ZUGFeRDXmlDecoder(xmlContent);
|
||||
}
|
||||
|
||||
// Validate the XML format
|
||||
const format = this.identifyXmlFormat(xmlContent);
|
||||
|
||||
// Log information about the extracted XML
|
||||
console.log(`Successfully extracted ${format} XML from PDF file. File name: ${xmlFileName}`);
|
||||
|
||||
return xmlContent;
|
||||
} catch (error) {
|
||||
console.error('Error extracting or parsing embedded XML from PDF:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the format of an XML document and returns the identified format
|
||||
*/
|
||||
private identifyXmlFormat(xmlContent: string): string {
|
||||
// Simple detection based on string content
|
||||
|
||||
// Check for ZUGFeRD/CII
|
||||
if (xmlContent.includes('CrossIndustryInvoice') ||
|
||||
xmlContent.includes('rsm:') ||
|
||||
xmlContent.includes('ram:')) {
|
||||
return 'ZUGFeRD/CII';
|
||||
}
|
||||
|
||||
// Check for UBL
|
||||
if (xmlContent.includes('<Invoice') ||
|
||||
xmlContent.includes('ubl:Invoice') ||
|
||||
xmlContent.includes('oasis:names:specification:ubl')) {
|
||||
return 'UBL';
|
||||
}
|
||||
|
||||
// Check for FatturaPA
|
||||
if (xmlContent.includes('FatturaElettronica') ||
|
||||
xmlContent.includes('fatturapa.gov.it')) {
|
||||
return 'FatturaPA';
|
||||
}
|
||||
|
||||
// For unknown formats, return generic
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
public async getParsedXmlData(): Promise<interfaces.IXInvoice> {
|
||||
const smartxmlInstance = new plugins.smartxml.SmartXml();
|
||||
if (!this.xmlString && !this.pdfUint8Array) {
|
||||
throw new Error('No XML string or PDF buffer provided!');
|
||||
}
|
||||
|
||||
let localXmlString = this.xmlString;
|
||||
if (!localXmlString) {
|
||||
localXmlString = await this.getXmlData();
|
||||
}
|
||||
return smartxmlInstance.parseXmlToObject(localXmlString);
|
||||
|
||||
return this.parseXmlToInvoice(localXmlString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example method to parse the embedded XML into a structured IInvoice.
|
||||
* Right now, it just returns mock data.
|
||||
* Replace with your own XML parsing.
|
||||
* Parses XML content into a structured IXInvoice object
|
||||
* Supports different XML invoice formats (ZUGFeRD, UBL, CII)
|
||||
*/
|
||||
private parseXmlToInvoice(xmlContent: string): interfaces.IXInvoice {
|
||||
// e.g. parse using DOMParser, xml2js, fast-xml-parser, etc.
|
||||
// For now, returning placeholder data:
|
||||
if (!xmlContent) {
|
||||
throw new Error('No XML content provided for parsing');
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize the decoder with XML content if not already done
|
||||
this.decoderInstance = new ZUGFeRDXmlDecoder(xmlContent);
|
||||
|
||||
// First, attempt to identify the XML format
|
||||
const format = this.identifyXmlFormat(xmlContent);
|
||||
|
||||
// Parse XML based on detected format
|
||||
switch (format) {
|
||||
case 'ZUGFeRD/CII':
|
||||
return this.parseCIIFormat(xmlContent);
|
||||
|
||||
case 'UBL':
|
||||
return this.parseUBLFormat(xmlContent);
|
||||
|
||||
case 'FatturaPA':
|
||||
return this.parseFatturaPAFormat(xmlContent);
|
||||
|
||||
default:
|
||||
// If format unrecognized, try generic parsing
|
||||
return this.parseGenericXml(xmlContent);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing XML to invoice structure:', error);
|
||||
throw new Error(`Failed to parse XML: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract XML values using regex
|
||||
*/
|
||||
private extractXmlValueByRegex(xmlContent: string, tagName: string): string {
|
||||
const regex = new RegExp(`<${tagName}[^>]*>([^<]+)</${tagName}>`, 'i');
|
||||
const match = xmlContent.match(regex);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses CII/ZUGFeRD format XML
|
||||
*/
|
||||
private parseCIIFormat(xmlContent: string): interfaces.IXInvoice {
|
||||
// For demo implementation, just extract basic information using string operations
|
||||
try {
|
||||
// Extract invoice number - basic pattern matching
|
||||
let invoiceNumber = 'Unknown';
|
||||
const invoiceNumberMatch = xmlContent.match(/<ram:ID>([^<]+)<\/ram:ID>/);
|
||||
if (invoiceNumberMatch && invoiceNumberMatch[1]) {
|
||||
invoiceNumber = invoiceNumberMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract date - basic pattern matching
|
||||
let dateIssued = new Date().toISOString().split('T')[0];
|
||||
const dateMatch = xmlContent.match(/<udt:DateTimeString[^>]*>([^<]+)<\/udt:DateTimeString>/);
|
||||
if (dateMatch && dateMatch[1]) {
|
||||
dateIssued = dateMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract seller name - basic pattern matching
|
||||
let sellerName = 'Unknown Seller';
|
||||
const sellerMatch = xmlContent.match(/<ram:SellerTradeParty>.*?<ram:Name>([^<]+)<\/ram:Name>/s);
|
||||
if (sellerMatch && sellerMatch[1]) {
|
||||
sellerName = sellerMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract buyer name - basic pattern matching
|
||||
let buyerName = 'Unknown Buyer';
|
||||
const buyerMatch = xmlContent.match(/<ram:BuyerTradeParty>.*?<ram:Name>([^<]+)<\/ram:Name>/s);
|
||||
if (buyerMatch && buyerMatch[1]) {
|
||||
buyerName = buyerMatch[1].trim();
|
||||
}
|
||||
|
||||
// For this demo implementation, create a minimal invoice structure
|
||||
return {
|
||||
InvoiceNumber: invoiceNumber,
|
||||
DateIssued: dateIssued,
|
||||
Seller: {
|
||||
Name: sellerName,
|
||||
Address: {
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Buyer: {
|
||||
Name: buyerName,
|
||||
Address: {
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Items: [
|
||||
{
|
||||
Description: 'Unknown Item',
|
||||
Quantity: 1,
|
||||
UnitPrice: 0,
|
||||
TotalPrice: 0,
|
||||
},
|
||||
],
|
||||
TotalAmount: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing CII format:', error);
|
||||
return this.parseGenericXml(xmlContent); // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses UBL format XML
|
||||
*/
|
||||
private parseUBLFormat(xmlContent: string): interfaces.IXInvoice {
|
||||
// Simplified UBL parsing - just extract basic fields
|
||||
try {
|
||||
const invoiceNumber = this.extractXmlValueByRegex(xmlContent, 'cbc:ID');
|
||||
const dateIssued = this.extractXmlValueByRegex(xmlContent, 'cbc:IssueDate');
|
||||
const sellerName = this.extractXmlValueByRegex(xmlContent, 'cac:AccountingSupplierParty.*?cbc:Name');
|
||||
const buyerName = this.extractXmlValueByRegex(xmlContent, 'cac:AccountingCustomerParty.*?cbc:Name');
|
||||
|
||||
return {
|
||||
InvoiceNumber: invoiceNumber || 'Unknown',
|
||||
DateIssued: dateIssued || new Date().toISOString().split('T')[0],
|
||||
Seller: {
|
||||
Name: sellerName || 'Unknown Seller',
|
||||
Address: {
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Buyer: {
|
||||
Name: buyerName || 'Unknown Buyer',
|
||||
Address: {
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Items: [
|
||||
{
|
||||
Description: 'Unknown Item',
|
||||
Quantity: 1,
|
||||
UnitPrice: 0,
|
||||
TotalPrice: 0,
|
||||
},
|
||||
],
|
||||
TotalAmount: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing UBL format:', error);
|
||||
return this.parseGenericXml(xmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses fatturaPA format XML
|
||||
*/
|
||||
private parseFatturaPAFormat(xmlContent: string): interfaces.IXInvoice {
|
||||
// In a full implementation, this would have fatturaPA-specific parsing
|
||||
// For now, using a simplified generic parser
|
||||
return this.parseGenericXml(xmlContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic XML parser that attempts to extract invoice data
|
||||
* from any XML structure
|
||||
*/
|
||||
private parseGenericXml(xmlContent: string): interfaces.IXInvoice {
|
||||
// For now, returning a placeholder structure
|
||||
// This would be replaced with more intelligent parsing
|
||||
return {
|
||||
InvoiceNumber: '12345',
|
||||
DateIssued: '2023-04-01',
|
||||
InvoiceNumber: '(Unknown format - invoice number not extracted)',
|
||||
DateIssued: new Date().toISOString().split('T')[0],
|
||||
Seller: {
|
||||
Name: 'Seller Co',
|
||||
Name: 'Unknown Seller (format not recognized)',
|
||||
Address: {
|
||||
Street: '1234 Market St',
|
||||
City: 'Sample City',
|
||||
PostalCode: '12345',
|
||||
Country: 'DE',
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'contact@sellerco.com',
|
||||
Phone: '123-456-7890',
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Buyer: {
|
||||
Name: 'Buyer Inc',
|
||||
Name: 'Unknown Buyer (format not recognized)',
|
||||
Address: {
|
||||
Street: '5678 Trade Rd',
|
||||
City: 'Trade City',
|
||||
PostalCode: '67890',
|
||||
Country: 'DE',
|
||||
Street: 'Unknown',
|
||||
City: 'Unknown',
|
||||
PostalCode: 'Unknown',
|
||||
Country: 'Unknown',
|
||||
},
|
||||
Contact: {
|
||||
Email: 'info@buyerinc.com',
|
||||
Phone: '987-654-3210',
|
||||
Email: 'unknown@example.com',
|
||||
Phone: 'Unknown',
|
||||
},
|
||||
},
|
||||
Items: [
|
||||
{
|
||||
Description: 'Item 1',
|
||||
Quantity: 10,
|
||||
UnitPrice: 9.99,
|
||||
TotalPrice: 99.9,
|
||||
Description: 'Unknown items (invoice format not recognized)',
|
||||
Quantity: 1,
|
||||
UnitPrice: 0,
|
||||
TotalPrice: 0,
|
||||
},
|
||||
],
|
||||
TotalAmount: 99.9,
|
||||
TotalAmount: 0,
|
||||
};
|
||||
}
|
||||
}
|
13
ts/index.ts
13
ts/index.ts
@ -1,7 +1,18 @@
|
||||
import * as interfaces from './interfaces.js';
|
||||
import { ZUGFeRDXmlDecoder } from './classes.decoder.js';
|
||||
import { FacturXEncoder } from './classes.encoder.js';
|
||||
import { XInvoice } from './classes.xinvoice.js';
|
||||
|
||||
// Export interfaces
|
||||
export {
|
||||
interfaces,
|
||||
}
|
||||
|
||||
export * from './classes.xinvoice.js';
|
||||
// Export main class
|
||||
export { XInvoice }
|
||||
|
||||
// Export encoder/decoder classes
|
||||
export { FacturXEncoder, ZUGFeRDXmlDecoder }
|
||||
|
||||
// For backward compatibility
|
||||
export { FacturXEncoder as ZugferdXmlEncoder }
|
Reference in New Issue
Block a user