feat(core): improve in-memory validation, FatturaPA detection coverage, and published type compatibility
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
@@ -23,13 +23,19 @@
|
||||
"esm",
|
||||
"financial technology"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
# EN16931 Conformance Testing Implementation
|
||||
|
||||
## Overview
|
||||
Successfully implemented a comprehensive conformance test harness for EN16931 validation, following GPT-5's recommendations as the highest priority after Schematron integration.
|
||||
|
||||
## Implementation Date
|
||||
2025-01-11
|
||||
|
||||
## Components Created
|
||||
|
||||
### 1. Conformance Test Harness (`ts/formats/validation/conformance.harness.ts`)
|
||||
The core testing infrastructure that:
|
||||
- Loads and runs official test samples
|
||||
- Validates using all available validators (TypeScript, Schematron, VAT categories, code lists)
|
||||
- Generates BR coverage matrix
|
||||
- Produces HTML coverage reports
|
||||
- Tracks performance metrics
|
||||
|
||||
Key features:
|
||||
- Automatic test sample discovery
|
||||
- Parallel validator execution
|
||||
- Rule coverage tracking
|
||||
- Error aggregation and reporting
|
||||
- Focus rule support for targeted testing
|
||||
|
||||
### 2. Test Sample Downloader (`scripts/download-test-samples.ts`)
|
||||
Automated tool to fetch official test samples:
|
||||
- Downloads from OpenPEPPOL/peppol-bis-invoice-3
|
||||
- Downloads from ConnectingEurope/eInvoicing-EN16931
|
||||
- Supports multiple standards (EN16931, PEPPOL BIS 3.0)
|
||||
- Metadata tracking for downloaded files
|
||||
|
||||
Successfully downloaded:
|
||||
- 6 PEPPOL BIS 3.0 example files (VAT categories, allowances, corrections)
|
||||
- 9 CEN TC434 UBL examples
|
||||
- Total: 15 official test samples
|
||||
|
||||
### 3. XML to EInvoice Converter (`ts/formats/converters/xml-to-einvoice.converter.ts`)
|
||||
Basic converter for testing:
|
||||
- Parses UBL and CII formats
|
||||
- Extracts essential invoice fields
|
||||
- Integrates with conformance harness
|
||||
- Uses @xmldom/xmldom for Node.js compatibility
|
||||
|
||||
### 4. VAT Categories Validator (`ts/formats/validation/vat-categories.validator.ts`)
|
||||
Complete implementation of all VAT category rules:
|
||||
- **BR-S-*** : Standard rate VAT (8 rules)
|
||||
- **BR-Z-*** : Zero rated VAT (8 rules)
|
||||
- **BR-E-*** : Exempt from tax (8 rules)
|
||||
- **BR-AE-***: VAT Reverse Charge (8 rules)
|
||||
- **BR-K-*** : Intra-community supply (10 rules)
|
||||
- **BR-G-*** : Export outside EU (8 rules)
|
||||
- **BR-O-*** : Out of scope services (8 rules)
|
||||
- Cross-category validation rules
|
||||
|
||||
Total: ~58 VAT-specific business rules implemented
|
||||
|
||||
## BR Coverage Matrix
|
||||
|
||||
The conformance harness generates a comprehensive coverage matrix showing:
|
||||
|
||||
### Overall Metrics
|
||||
- Total EN16931 rules defined: ~150
|
||||
- Rules currently covered: ~75%
|
||||
- Coverage by category:
|
||||
- Document level: ~80%
|
||||
- Calculation rules: 100%
|
||||
- VAT rules: ~95%
|
||||
- Line level: 100%
|
||||
- Code lists: 100%
|
||||
|
||||
### Coverage Visualization
|
||||
HTML reports generated at `coverage-report.html` include:
|
||||
- Overall coverage percentage bar
|
||||
- Category breakdown table
|
||||
- Test sample results
|
||||
- Uncovered rules list
|
||||
- Sample-to-rule mapping
|
||||
|
||||
## Usage
|
||||
|
||||
### Download Test Samples
|
||||
```bash
|
||||
npm run download-test-samples
|
||||
```
|
||||
|
||||
### Run Conformance Tests
|
||||
```bash
|
||||
npm run test:conformance
|
||||
```
|
||||
|
||||
### Generate Coverage Report
|
||||
The conformance test automatically generates an HTML coverage report showing:
|
||||
- Which rules are tested
|
||||
- Which samples trigger which rules
|
||||
- Overall coverage percentage
|
||||
- Gaps in test coverage
|
||||
|
||||
## Test Sample Structure
|
||||
```
|
||||
test-samples/
|
||||
├── peppol-bis3/
|
||||
│ ├── Allowance-example.xml
|
||||
│ ├── base-example.xml
|
||||
│ ├── base-negative-inv-correction.xml
|
||||
│ ├── vat-category-E.xml
|
||||
│ ├── vat-category-O.xml
|
||||
│ └── vat-category-Z.xml
|
||||
├── cen-tc434/
|
||||
│ ├── ubl-tc434-example1.xml
|
||||
│ ├── ubl-tc434-example2.xml
|
||||
│ └── ... (9 files total)
|
||||
└── metadata.json
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. With Schematron Validator
|
||||
The conformance harness can load and use official Schematron rules:
|
||||
```typescript
|
||||
await harness.loadSchematron('EN16931', 'UBL');
|
||||
```
|
||||
|
||||
### 2. With TypeScript Validators
|
||||
Integrates all TypeScript validators:
|
||||
- EN16931BusinessRulesValidator
|
||||
- CodeListValidator
|
||||
- VATCategoriesValidator
|
||||
|
||||
### 3. With CI/CD
|
||||
Can be integrated into CI pipelines:
|
||||
```yaml
|
||||
- name: Run Conformance Tests
|
||||
run: |
|
||||
npm run download-test-samples
|
||||
npm run test:conformance
|
||||
```
|
||||
|
||||
## Results Analysis
|
||||
|
||||
### Successful Validations
|
||||
- Document structure validation
|
||||
- Mandatory field presence
|
||||
- Code list conformance
|
||||
- VAT category consistency
|
||||
|
||||
### Common Issues Found
|
||||
- Missing optional but recommended fields
|
||||
- Calculation precision differences
|
||||
- VAT exemption reason requirements
|
||||
- Cross-border transaction rules
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Short Term
|
||||
1. Implement decimal arithmetic for absolute precision
|
||||
2. Add more test samples (XRechnung, Factur-X)
|
||||
3. Improve XML parser for complete field extraction
|
||||
|
||||
### Medium Term
|
||||
1. Add XRechnung CIUS overlay
|
||||
2. Implement PEPPOL BIS 3.0 specific rules
|
||||
3. Create profile-specific test suites
|
||||
|
||||
### Long Term
|
||||
1. Achieve 100% BR coverage
|
||||
2. Add mutation testing
|
||||
3. Performance optimization for large batches
|
||||
4. Real-time validation API
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
Current performance (on standard hardware):
|
||||
- Single invoice validation: ~50-200ms
|
||||
- With Schematron: +50-200ms
|
||||
- Batch of 100 invoices: ~5-10 seconds
|
||||
- Coverage report generation: <1 second
|
||||
|
||||
## Standards Alignment
|
||||
|
||||
This implementation follows:
|
||||
- EN16931-1:2017 (Semantic model)
|
||||
- ISO/IEC 19757-3:2016 (Schematron)
|
||||
- OASIS UBL 2.1 specifications
|
||||
- UN/CEFACT Cross Industry Invoice
|
||||
|
||||
## Success Metrics
|
||||
|
||||
✅ Conformance test harness operational
|
||||
✅ Official test samples integrated
|
||||
✅ BR coverage matrix generation
|
||||
✅ VAT category rules complete
|
||||
✅ HTML reporting functional
|
||||
✅ Performance within targets
|
||||
|
||||
## Conclusion
|
||||
|
||||
The conformance test harness provides a robust foundation for achieving 100% EN16931 compliance. With ~75% coverage already achieved and clear visibility into gaps, the path to full compliance is well-defined.
|
||||
|
||||
The combination of:
|
||||
- Official test samples
|
||||
- Comprehensive validators
|
||||
- Coverage tracking
|
||||
- Performance metrics
|
||||
|
||||
Creates a production-ready validation system that can be continuously improved and extended to support additional standards and CIUS implementations.
|
||||
@@ -1,113 +0,0 @@
|
||||
# Currency-Aware Rounding Implementation
|
||||
|
||||
## Overview
|
||||
Implemented ISO 4217 currency-aware rounding utilities to replace the flat 0.01 tolerance approach, as recommended by GPT-5 for EN16931 compliance.
|
||||
|
||||
## Implementation Date
|
||||
2025-01-11
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### 1. Currency Utilities (`ts/formats/utils/currency.utils.ts`)
|
||||
- Complete ISO 4217 currency minor units mapping
|
||||
- Multiple rounding modes (HALF_UP, HALF_DOWN, HALF_EVEN, UP, DOWN, CEILING, FLOOR)
|
||||
- Currency-aware tolerance calculations
|
||||
- `CurrencyCalculator` class for EN16931 calculations
|
||||
|
||||
### 2. EN16931 Business Rules Validator Integration
|
||||
- Modified `ts/formats/validation/en16931.business-rules.validator.ts`
|
||||
- Integrated `CurrencyCalculator` for all monetary calculations
|
||||
- Currency-aware comparison using `areEqual()` method
|
||||
- Proper rounding at calculation points
|
||||
|
||||
### 3. Test Suite (`test/test.currency-utils.ts`)
|
||||
- 7 comprehensive test cases
|
||||
- All tests passing (100% coverage)
|
||||
- Tests for edge cases including negative numbers and zero-decimal currencies
|
||||
|
||||
## Key Features
|
||||
|
||||
### ISO 4217 Currency Support
|
||||
- 74 currencies with proper minor units
|
||||
- Handles 0-decimal currencies (JPY, KRW, etc.)
|
||||
- Handles 3-decimal currencies (KWD, TND, etc.)
|
||||
- Handles 4-decimal currencies (CLF, UYW)
|
||||
|
||||
### Rounding Modes
|
||||
1. **HALF_UP**: Round half values away from zero (default)
|
||||
2. **HALF_DOWN**: Round half values toward zero
|
||||
3. **HALF_EVEN**: Banker's rounding
|
||||
4. **UP**: Always round away from zero
|
||||
5. **DOWN**: Always round toward zero (truncate)
|
||||
6. **CEILING**: Round toward positive infinity
|
||||
7. **FLOOR**: Round toward negative infinity
|
||||
|
||||
### CurrencyCalculator Methods
|
||||
- `round()`: Round value according to currency rules
|
||||
- `calculateLineNet()`: Calculate line net with proper rounding
|
||||
- `calculateVAT()`: Calculate VAT amount with rounding
|
||||
- `areEqual()`: Compare values with currency-aware tolerance
|
||||
- `getTolerance()`: Get comparison tolerance
|
||||
- `format()`: Format value for display
|
||||
|
||||
## Tolerance Calculation
|
||||
Tolerance is calculated as half of the smallest representable unit:
|
||||
- EUR (2 decimals): tolerance = 0.005
|
||||
- JPY (0 decimals): tolerance = 0.5
|
||||
- KWD (3 decimals): tolerance = 0.0005
|
||||
|
||||
## Bug Fixes
|
||||
Fixed two critical rounding issues:
|
||||
1. **HALF_DOWN mode**: Now correctly rounds 0.5 toward zero
|
||||
2. **HALF_UP with negatives**: Now correctly rounds -0.5 away from zero
|
||||
|
||||
## Usage Example
|
||||
```typescript
|
||||
// Create calculator for EUR
|
||||
const calc = new CurrencyCalculator('EUR');
|
||||
|
||||
// Calculate line net
|
||||
const lineNet = calc.calculateLineNet(5, 19.99, 2.50);
|
||||
// Result: 97.45 (properly rounded to 2 decimals)
|
||||
|
||||
// Calculate VAT
|
||||
const vat = calc.calculateVAT(100, 19);
|
||||
// Result: 19.00 (properly rounded)
|
||||
|
||||
// Compare values with tolerance
|
||||
const isEqual = calc.areEqual(10.234, 10.236);
|
||||
// Result: false (difference exceeds EUR tolerance of 0.005)
|
||||
```
|
||||
|
||||
## Impact on Validation
|
||||
The EN16931 Business Rules Validator now:
|
||||
- Uses currency-specific rounding for all calculations
|
||||
- Compares values with currency-aware tolerance
|
||||
- Properly handles edge cases in different currencies
|
||||
- Provides more accurate validation results
|
||||
|
||||
## Next Steps
|
||||
As identified by GPT-5, the next priorities are:
|
||||
1. ✅ Currency-aware rounding (COMPLETE)
|
||||
2. Saxon-JS for Schematron integration
|
||||
3. Complete VAT category rules
|
||||
4. Add decimal arithmetic library for even more precision
|
||||
|
||||
## Test Results
|
||||
```
|
||||
Currency Utils Tests: 7/7 PASSED
|
||||
- Different currency decimal places ✅
|
||||
- Rounding values correctly ✅
|
||||
- Different rounding modes ✅
|
||||
- Correct tolerance calculation ✅
|
||||
- Monetary value comparison ✅
|
||||
- EN16931 calculations ✅
|
||||
- Edge cases handling ✅
|
||||
```
|
||||
|
||||
## Standards Compliance
|
||||
This implementation aligns with:
|
||||
- ISO 4217:2015 currency codes
|
||||
- EN16931 calculation requirements
|
||||
- European e-invoicing best practices
|
||||
- Financial industry rounding standards
|
||||
@@ -1,178 +0,0 @@
|
||||
# E-Invoice Standards Implementation Summary
|
||||
|
||||
## Executive Summary
|
||||
We've successfully improved the einvoice module from ~10% to ~35% EN16931 compliance by implementing core validation infrastructure, business rules, and code list validators with a feature flag system for gradual rollout.
|
||||
|
||||
## Accomplishments (2025-01-11)
|
||||
|
||||
### 1. EN16931 Business Rules Validator
|
||||
**File**: `ts/formats/validation/en16931.business-rules.validator.ts`
|
||||
|
||||
Implemented ~40 of 120+ business rules:
|
||||
- **Document Rules** (BR-01 to BR-11, BR-16)
|
||||
- Mandatory field validation
|
||||
- Seller/buyer information requirements
|
||||
- Invoice line presence checks
|
||||
|
||||
- **Calculation Rules** (BR-CO-*)
|
||||
- BR-CO-10: Sum of invoice lines validation
|
||||
- BR-CO-13: Tax exclusive calculations
|
||||
- BR-CO-14: Total VAT amount verification
|
||||
- BR-CO-15: Tax inclusive totals
|
||||
- BR-CO-16: Amount due for payment
|
||||
|
||||
- **VAT Rules** (Partial)
|
||||
- BR-S-01 to BR-S-03: Standard rated VAT
|
||||
- BR-Z-01: Zero rated VAT
|
||||
|
||||
- **Line Rules** (BR-21 to BR-30)
|
||||
- All line-level validation rules implemented
|
||||
- Quantity, unit code, pricing validation
|
||||
|
||||
### 2. Code List Validator
|
||||
**File**: `ts/formats/validation/codelist.validator.ts`
|
||||
|
||||
Complete implementation of standard code lists:
|
||||
- ISO 4217 Currency codes (BR-CL-03, BR-CL-04)
|
||||
- ISO 3166 Country codes (BR-CL-14, BR-CL-15, BR-CL-16)
|
||||
- UNCL5305 VAT category codes (BR-CL-10)
|
||||
- UNCL1001 Document type codes (BR-CL-01)
|
||||
- UNCL4461 Payment means codes (BR-CL-16)
|
||||
- UNECE Rec 20 Unit codes (BR-CL-23)
|
||||
|
||||
### 3. Enhanced Validation Infrastructure
|
||||
**Files**:
|
||||
- `ts/formats/validation/validation.types.ts`
|
||||
- `ts/interfaces/en16931-metadata.ts`
|
||||
|
||||
Features:
|
||||
- Business Term (BT) and Business Group (BG) references
|
||||
- Semantic model mapping for EN16931 fields
|
||||
- Code list metadata and versioning
|
||||
- Remediation hints for errors
|
||||
- Extended metadata interface for all EN16931 fields
|
||||
|
||||
### 4. Feature Flag System
|
||||
Enables gradual rollout without breaking changes:
|
||||
- `EN16931_BUSINESS_RULES` - Enables business rule validation
|
||||
- `CODE_LIST_VALIDATION` - Enables code list checks
|
||||
- Report-only mode for non-blocking validation
|
||||
|
||||
### 5. Test Coverage
|
||||
**File**: `test/test.en16931-validators.ts`
|
||||
- Unit tests for all validators
|
||||
- Integration with existing test suite
|
||||
- 480/481 tests passing
|
||||
|
||||
## GPT-5 Assessment
|
||||
|
||||
### Strengths
|
||||
✅ Clear layered validation architecture
|
||||
✅ Feature flags for safe rollout
|
||||
✅ Early code list coverage (often neglected)
|
||||
✅ Enhanced ValidationResult with BT/BG references
|
||||
✅ Developer-friendly error messages
|
||||
|
||||
### Critical Next Steps (Priority Order)
|
||||
|
||||
#### 1. Schematron Integration (Highest Priority)
|
||||
- Integrate official EN16931 Schematron from ConnectingEurope/eInvoicing-EN16931
|
||||
- Run in parallel with code validators (hybrid approach)
|
||||
- Use Saxon-JS in worker threads for Node.js
|
||||
|
||||
#### 2. Currency-Aware Rounding
|
||||
- Replace flat 0.01 tolerance with ISO 4217 minor units
|
||||
- Implement decimal arithmetic (big.js/decimal.js)
|
||||
- Explicit rounding at defined calculation points
|
||||
|
||||
#### 3. Complete VAT Rules
|
||||
- Enforce all VAT categories (S, Z, E, AE, K, G, O)
|
||||
- Validate exemption reasons and reverse charge
|
||||
- Cross-field validation for VAT breakdowns
|
||||
|
||||
#### 4. Conformance Test Harness
|
||||
- Import official CEN test cases
|
||||
- PEPPOL BIS Billing 3 samples
|
||||
- XRechnung test packs
|
||||
- Coverage matrix per BR-ID
|
||||
|
||||
## Recommended Architecture (from GPT-5)
|
||||
|
||||
### Hybrid Validation Pipeline
|
||||
```
|
||||
Stage 0: XSD validation (optional, fast fail)
|
||||
Stage 1: TS validators on TInvoice (real-time UX)
|
||||
Stage 2: Schematron on native XML (conformance)
|
||||
Stage 3: Merge and normalize results
|
||||
```
|
||||
|
||||
### Key Decisions
|
||||
- **Run both validators**: Schematron for conformance, TS for UX
|
||||
- **Validate native XML**: Don't adapt Schematron to internal model
|
||||
- **Feature flags**: Control when Schematron runs (submit vs interactive)
|
||||
|
||||
## Comparison to Other Implementations
|
||||
|
||||
### We Compare Well On:
|
||||
- Developer ergonomics (ValidationResult, feature flags)
|
||||
- TypeScript/Node.js ecosystem (rare for e-invoicing)
|
||||
- Gradual rollout capability
|
||||
|
||||
### To Match Maturity:
|
||||
- Add official Schematron validation
|
||||
- Complete test pack coverage
|
||||
- Implement CIUS overlays (PEPPOL, XRechnung)
|
||||
|
||||
## Resources Found
|
||||
|
||||
### Official Repositories
|
||||
- **ConnectingEurope/eInvoicing-EN16931** - v1.3.14.2 with UBL/CII Schematron
|
||||
- **OpenPEPPOL/tc434-validation** - CEN/TC 434 artefacts
|
||||
- **itplr-kosit/xrechnung-schematron** - German CIUS
|
||||
|
||||
### Reference Implementations
|
||||
- **Philip Helger's PHIVE** - Comprehensive Java validator
|
||||
- **KoSIT XRechnung Validator** - Official German validator
|
||||
- **Mustangproject** - ZUGFeRD/Factur-X focus
|
||||
|
||||
## Next Sprint Plan (2 Weeks)
|
||||
|
||||
### Week 1
|
||||
- [ ] Set up Saxon-JS worker pool for Schematron
|
||||
- [ ] Integrate ConnectingEurope EN16931 Schematron
|
||||
- [ ] Implement ISO 4217 currency minor units
|
||||
- [ ] Replace tolerance with currency-aware rounding
|
||||
|
||||
### Week 2
|
||||
- [ ] Complete VAT category/exemption rules
|
||||
- [ ] Add conformance test harness
|
||||
- [ ] Import official test packs
|
||||
- [ ] Create BR-ID coverage matrix
|
||||
|
||||
## Long-term Roadmap
|
||||
|
||||
### Phase 1: Core Compliance (Current → 60%)
|
||||
- Complete remaining EN16931 business rules
|
||||
- Full Schematron integration
|
||||
- Conformance test coverage
|
||||
|
||||
### Phase 2: CIUS Support (60% → 80%)
|
||||
- PEPPOL BIS 3.0 overlay
|
||||
- XRechnung 3.0 CIUS
|
||||
- Profile-based validation
|
||||
|
||||
### Phase 3: Production Ready (80% → 100%)
|
||||
- Performance optimization
|
||||
- Security hardening (XXE, limits)
|
||||
- Comprehensive documentation
|
||||
- CI/CD integration
|
||||
|
||||
## Success Metrics
|
||||
- ✅ Pass official EN16931 test suite
|
||||
- ✅ 100% BR-ID coverage
|
||||
- ✅ <100ms validation performance
|
||||
- ✅ Clear remediation messages
|
||||
- ✅ PEPPOL/XRechnung certification ready
|
||||
|
||||
## Conclusion
|
||||
We've built a solid foundation with ~35% compliance and clear architecture. The path to 100% is well-defined with official Schematron integration as the critical next step. Our TypeScript implementation with enhanced developer experience positions us well in the ecosystem.
|
||||
@@ -1,194 +0,0 @@
|
||||
# Schematron Validation Implementation
|
||||
|
||||
## Overview
|
||||
Successfully implemented Saxon-JS based Schematron validation infrastructure for official EN16931 standards compliance, as recommended by GPT-5 as the highest priority for achieving compliance.
|
||||
|
||||
## Implementation Date
|
||||
2025-01-11
|
||||
|
||||
## Components Created
|
||||
|
||||
### 1. Core Schematron Validator (`ts/formats/validation/schematron.validator.ts`)
|
||||
- Saxon-JS integration for XSLT 3.0 processing
|
||||
- Schematron to XSLT compilation
|
||||
- SVRL (Schematron Validation Report Language) parsing
|
||||
- Phase support for selective validation
|
||||
- Hybrid validator combining TypeScript and Schematron
|
||||
|
||||
### 2. Worker Pool Implementation (`ts/formats/validation/schematron.worker.ts`)
|
||||
- Non-blocking validation in worker threads
|
||||
- Prevents main thread blocking during complex validations
|
||||
- Configurable worker pool size
|
||||
- Task queue management
|
||||
|
||||
### 3. Schematron Downloader (`ts/formats/validation/schematron.downloader.ts`)
|
||||
- Automatic download from official repositories
|
||||
- Caching with version management
|
||||
- Support for multiple standards:
|
||||
- EN16931 (ConnectingEurope/eInvoicing-EN16931)
|
||||
- PEPPOL BIS 3.0 (OpenPEPPOL repositories)
|
||||
- XRechnung (itplr-kosit/xrechnung-schematron)
|
||||
|
||||
### 4. Integration Layer (`ts/formats/validation/schematron.integration.ts`)
|
||||
- Unified validation interface
|
||||
- Automatic format detection (UBL/CII)
|
||||
- Combines TypeScript and Schematron validators
|
||||
- Comprehensive validation reports
|
||||
|
||||
### 5. Download Script (`scripts/download-schematron.ts`)
|
||||
- CLI tool to fetch official Schematron files
|
||||
- Version tracking and metadata storage
|
||||
|
||||
## Official Schematron Files Downloaded
|
||||
|
||||
Successfully downloaded from official repositories:
|
||||
- ✅ EN16931-UBL v1.3.14
|
||||
- ✅ EN16931-CII v1.3.14
|
||||
- ✅ EN16931-EDIFACT v1.3.14
|
||||
- ✅ PEPPOL-EN16931-UBL v3.0.17
|
||||
|
||||
Stored in: `assets/schematron/`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Hybrid Validation Pipeline
|
||||
```
|
||||
Stage 1: TypeScript validators (fast, real-time UX)
|
||||
├── EN16931 Business Rules (~40 rules)
|
||||
├── Code List Validation (complete)
|
||||
└── Currency-aware calculations
|
||||
|
||||
Stage 2: Schematron validation (official conformance)
|
||||
├── EN16931 official rules
|
||||
├── PEPPOL BIS overlays
|
||||
└── XRechnung CIUS rules
|
||||
|
||||
Stage 3: Result merging and deduplication
|
||||
└── Unified ValidationReport
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Standards Support
|
||||
- EN16931 core validation
|
||||
- PEPPOL BIS 3.0 ready
|
||||
- XRechnung CIUS ready
|
||||
- Factur-X profile support
|
||||
|
||||
### 2. Performance Optimizations
|
||||
- Worker thread pool for non-blocking validation
|
||||
- Cached compiled stylesheets
|
||||
- Lazy loading of Schematron rules
|
||||
|
||||
### 3. Developer Experience
|
||||
- Automatic format detection
|
||||
- Comprehensive validation reports
|
||||
- BT/BG semantic references
|
||||
- Clear error messages with remediation hints
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
import { IntegratedValidator } from './ts/formats/validation/schematron.integration.js';
|
||||
|
||||
// Create validator
|
||||
const validator = new IntegratedValidator();
|
||||
|
||||
// Load EN16931 Schematron for UBL
|
||||
await validator.loadSchematron('EN16931', 'UBL');
|
||||
|
||||
// Validate invoice
|
||||
const report = await validator.validate(invoice, xmlContent, {
|
||||
profile: 'EN16931',
|
||||
checkCalculations: true,
|
||||
checkVAT: true,
|
||||
checkCodeLists: true
|
||||
});
|
||||
|
||||
console.log(`Valid: ${report.valid}`);
|
||||
console.log(`Errors: ${report.errorCount}`);
|
||||
console.log(`Coverage: ${report.coverage}%`);
|
||||
```
|
||||
|
||||
## Validation Coverage
|
||||
|
||||
Current implementation covers:
|
||||
- **TypeScript Validators**: ~40% of EN16931 rules
|
||||
- Document level rules: BR-01 to BR-16
|
||||
- Calculation rules: BR-CO-* (complete)
|
||||
- VAT rules: BR-S-*, BR-Z-* (partial)
|
||||
- Line rules: BR-21 to BR-30 (complete)
|
||||
- Code lists: All major lists
|
||||
|
||||
- **Schematron Validators**: 100% of official rules
|
||||
- EN16931 complete rule set
|
||||
- PEPPOL BIS 3.0 overlays
|
||||
- XRechnung CIUS constraints
|
||||
|
||||
## Next Steps
|
||||
|
||||
As identified by GPT-5, the priorities after Schematron are:
|
||||
|
||||
1. ✅ Saxon-JS for Schematron (COMPLETE)
|
||||
2. ✅ Download official Schematron (COMPLETE)
|
||||
3. Complete remaining VAT category rules
|
||||
4. Add conformance test harness
|
||||
5. Implement decimal arithmetic
|
||||
6. Create production-ready orchestrator
|
||||
|
||||
## Testing
|
||||
|
||||
All Schematron infrastructure tests passing:
|
||||
```
|
||||
✅ Schematron Infrastructure - initialization
|
||||
✅ Schematron Infrastructure - rule loading
|
||||
✅ Schematron Infrastructure - phase detection
|
||||
✅ Schematron Downloader - initialization
|
||||
✅ Schematron Downloader - source listing
|
||||
✅ Hybrid Validator - validator combination
|
||||
✅ Schematron Worker Pool - initialization
|
||||
✅ Schematron Validator - SVRL parsing
|
||||
✅ Schematron Integration - error handling
|
||||
```
|
||||
|
||||
## Impact on Compliance
|
||||
|
||||
With Schematron integration:
|
||||
- **Before**: ~40% compliance (TypeScript validators only)
|
||||
- **After**: ~70% compliance (TypeScript + Schematron)
|
||||
- **Gap**: Remaining 30% requires:
|
||||
- Complete VAT category rules
|
||||
- Conformance test coverage
|
||||
- CIUS overlays (PEPPOL, XRechnung)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Schematron validation adds ~50-200ms per document
|
||||
- Worker threads prevent UI blocking
|
||||
- Cached compilations reduce overhead
|
||||
- Hybrid approach allows graceful degradation
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Downloaded Schematron files are validated
|
||||
- XSLT execution is sandboxed
|
||||
- No external entity resolution (XXE prevention)
|
||||
- Size limits on processed documents
|
||||
|
||||
## Standards Alignment
|
||||
|
||||
This implementation follows:
|
||||
- ISO/IEC 19757-3:2016 (Schematron)
|
||||
- EN16931-1:2017 (Semantic model)
|
||||
- OASIS UBL 2.1 specifications
|
||||
- UN/CEFACT Cross Industry Invoice
|
||||
|
||||
## Conclusion
|
||||
|
||||
Successfully implemented the highest priority item from GPT-5's recommendations. The Schematron infrastructure provides:
|
||||
1. Official standards validation
|
||||
2. Non-blocking performance
|
||||
3. Extensible architecture
|
||||
4. Clear path to 100% compliance
|
||||
|
||||
The combination of TypeScript validators for UX and Schematron for conformance creates a robust, production-ready validation system.
|
||||
@@ -1,367 +0,0 @@
|
||||
# E-Invoice Standards Compliance Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
Current compliance: **100% of required rules** ✅
|
||||
Achieved: **100% compliance** with EN16931, XRechnung, Peppol BIS 3.0, and Factur-X profiles
|
||||
|
||||
**FINAL UPDATE (2025-01-11 - Session 5) - 100% COMPLIANCE ACHIEVED**:
|
||||
- Implemented complete EN16931 semantic model with all 162 Business Terms (BT-1 to BT-162)
|
||||
- Created all 32 Business Groups (BG-1 to BG-32) with full field mappings
|
||||
- Built SemanticModelAdapter for bidirectional EInvoice conversion
|
||||
- Implemented SemanticModelValidator with BT/BG-level validation
|
||||
- Added complete mapping between EInvoice TContact structure and semantic model
|
||||
- Fixed all test failures - 100% of semantic model tests passing
|
||||
- **ACHIEVEMENT: 100% EN16931 compliance across all standards and profiles**
|
||||
|
||||
**Previous Update (2025-01-11 - Session 4)**:
|
||||
- Implemented complete Factur-X profile support (MINIMUM, BASIC, BASIC_WL, EN16931, EXTENDED)
|
||||
- Added profile-specific field cardinality validation for each Factur-X profile
|
||||
- Created automatic profile detection for Factur-X and ZUGFeRD formats
|
||||
- Implemented profile-specific business rules and compliance levels
|
||||
- Integrated Factur-X validator into MainValidator with automatic detection
|
||||
- Added support for both calculated fields (EInvoice getters) and direct properties
|
||||
- 15/15 Factur-X tests passing, achieving full profile validation coverage
|
||||
|
||||
**Previous Update (2025-01-11 - Session 3)**:
|
||||
- Implemented complete PEPPOL BIS 3.0 validator with all required validation rules
|
||||
- Added endpoint ID validation with GLN checksum verification (0088:xxxxxxxxx format)
|
||||
- Implemented document type ID and process ID validation for PEPPOL network
|
||||
- Added party identification scheme validation against ISO 6523 ICD list
|
||||
- Created comprehensive PEPPOL business rules (buyer reference, payment means, etc.)
|
||||
- Integrated PEPPOL validator into MainValidator with automatic profile detection
|
||||
- 16/16 PEPPOL tests passing, overall test suite 158/160 passing (98.8% pass rate)
|
||||
|
||||
**Previous Update (2025-01-11 - Session 2)**:
|
||||
- Implemented integrated validator combining all validation capabilities
|
||||
- XRechnung CIUS validator with German-specific rules (Leitweg-ID, IBAN/BIC, VAT ID)
|
||||
- Integrated Schematron validation into main pipeline
|
||||
- Fixed all test failures - 157/158 tests passing (99.4% pass rate)
|
||||
- Created MainValidator class for unified validation with profile detection
|
||||
|
||||
**Previous Update (2025-01-11)**:
|
||||
- Completed Saxon-JS Schematron integration with official EN16931 rules
|
||||
- Implemented comprehensive VAT category validator (all BR-S-*, BR-Z-*, BR-E-*, BR-AE-*, BR-K-*, BR-G-*, BR-O-* rules)
|
||||
- Added conformance test harness with official test samples
|
||||
- Created BR coverage matrix generation
|
||||
- Implemented arbitrary precision decimal arithmetic for EN16931-compliant monetary calculations
|
||||
- Created DecimalCurrencyCalculator with ISO 4217 currency-aware rounding
|
||||
- Integrated decimal arithmetic with all validators to eliminate floating-point errors
|
||||
|
||||
## Scale of Work
|
||||
- EN16931 core: ~120-150 business rules
|
||||
- XRechnung CIUS: 100-200+ format-specific constraints
|
||||
- Peppol BIS 3.0: Additional Schematron layer
|
||||
- Factur-X profiles: Profile-specific cardinalities
|
||||
- **Total: 300-500+ validations needed**
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 0: Baseline Infrastructure ✅ COMPLETE
|
||||
- [x] Create rule registry with all EN16931, XRechnung, Peppol rule IDs (partial - EN16931 done)
|
||||
- [x] Build coverage tracking system (ValidationReport with coverage metrics)
|
||||
- [x] Set up validation result data model (ValidationResult interface with BT/BG references)
|
||||
- [x] Implement Schematron engine integration ✅ (Saxon-JS with official rules)
|
||||
|
||||
### Phase 1: Core EN16931 Business Rules ✅ COMPLETE
|
||||
- [x] Create EN16931BusinessRulesValidator class
|
||||
- [x] Implement calculation rules (BR-CO-*)
|
||||
- BR-CO-10: Sum of invoice lines = Line extension amount ✅
|
||||
- BR-CO-13: Tax exclusive = Lines - Allowances + Charges ✅
|
||||
- BR-CO-15: Tax inclusive = Tax exclusive + VAT ✅
|
||||
- BR-CO-14: Invoice total VAT amount ✅
|
||||
- BR-CO-16: Amount due for payment ✅
|
||||
- [x] Implement VAT rules (BR-S-*, BR-Z-*, partial)
|
||||
- BR-S-01 to BR-S-03: Standard rated VAT ✅
|
||||
- BR-Z-01: Zero rated VAT ✅
|
||||
- [x] Add document level rules (BR-01 to BR-65) - ~25 rules implemented
|
||||
- BR-01 to BR-11: Mandatory fields ✅
|
||||
- BR-16: Invoice lines ✅
|
||||
- [x] Add line level rules (BR-21 to BR-30) - All implemented ✅
|
||||
|
||||
### Phase 2: Calculation Engine ✅ COMPLETE
|
||||
- [x] Build canonical semantic model (BT/BG fields) ✅
|
||||
- [x] Create UBL/CII adapters to semantic model ✅
|
||||
- [x] Implement calculation verification:
|
||||
- Line totals (quantity × price) ✅
|
||||
- Tax base per category ✅
|
||||
- Header allowance/charge distribution ✅
|
||||
- Rounding and tolerance handling ✅ (ISO 4217 currency-aware)
|
||||
- [x] Handle edge cases:
|
||||
- Mixed VAT categories ✅
|
||||
- Reverse charge (partial)
|
||||
- Multi-currency ✅ (ISO 4217 support)
|
||||
- [x] Implement decimal arithmetic library ✅ COMPLETE
|
||||
- Arbitrary precision using BigInt
|
||||
- All rounding modes supported
|
||||
- Currency-aware calculations
|
||||
|
||||
### Phase 3: XRechnung CIUS ✅ COMPLETE
|
||||
- [x] Integrate XRechnung Schematron pack ✅ (integrated into pipeline)
|
||||
- [x] Implement Leitweg-ID validation (pattern: [0-9]{2,3}-[0-9]{1,12}-[0-9]{2,30}) ✅
|
||||
- [x] Enforce mandatory buyer reference (BT-10) ✅
|
||||
- [ ] Add German-specific payment terms validation
|
||||
- [x] IBAN/BIC validation for SEPA ✅
|
||||
- [x] German VAT ID format validation ✅
|
||||
- [x] Seller contact mandatory fields ✅
|
||||
- [x] B2G invoice detection and requirements ✅
|
||||
|
||||
### Phase 4: Peppol BIS 3.0 (Week 5) ✅ COMPLETE
|
||||
- [x] Add Peppol Schematron layer (integrated via MainValidator)
|
||||
- [x] Implement endpoint ID validation (0088:xxxxxxxxx) ✅
|
||||
- [x] Add document type ID validation ✅
|
||||
- [x] Party identification scheme validation ✅
|
||||
- [x] Process ID validation ✅
|
||||
- [x] GLN checksum validation (modulo 10) ✅
|
||||
- [x] GTIN validation for item identifiers ✅
|
||||
- [x] B2G detection and requirements ✅
|
||||
- [x] UNCL4461 payment means validation ✅
|
||||
- [x] Complete ISO 6523 ICD scheme validation ✅
|
||||
|
||||
### Phase 5: Factur-X Profiles (Week 6) ✅ COMPLETE
|
||||
- [x] Implement profile detection ✅
|
||||
- [x] Add profile-specific validators: ✅
|
||||
- MINIMUM: Only BT-1, BT-2, BT-3 ✅
|
||||
- BASIC: Core fields ✅
|
||||
- BASIC_WL: Basic without lines ✅
|
||||
- EN16931: Full compliance ✅
|
||||
- EXTENDED: Additional structured data ✅
|
||||
- [x] Profile-based field cardinality enforcement ✅
|
||||
- [x] ZUGFeRD compatibility support ✅
|
||||
- [x] Profile compliance level tracking ✅
|
||||
|
||||
### Phase 6: Code List Validators ✅ COMPLETE
|
||||
- [x] ISO 4217 currency codes (BR-CL-03, BR-CL-04) ✅
|
||||
- [x] ISO 3166 country codes (BR-CL-14, BR-CL-15, BR-CL-16) ✅
|
||||
- [x] UN/ECE 4461 payment means codes (BR-CL-16) ✅
|
||||
- [x] UNTDID 1001 document type codes (BR-CL-01) ✅
|
||||
- [x] VAT category codes (UNCL5305 - BR-CL-10) ✅
|
||||
- [x] UNECE Rec 20 unit codes (BR-CL-23) ✅
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Layered Validation Approach:
|
||||
1. **Schema validation**: XSD for UBL/CII
|
||||
2. **Schematron packs**: EN16931, CIUS, code lists
|
||||
3. **Programmatic engine**: Calculations and relationships
|
||||
|
||||
### API Design:
|
||||
```typescript
|
||||
interface ValidationOptions {
|
||||
format?: 'ubl' | 'cii';
|
||||
profile?: 'EN16931' | 'XRechnung_3.0' | 'Peppol_BIS_3.0' | 'FacturX_Basic';
|
||||
tolerance?: number; // Default 0.01
|
||||
strictMode?: boolean;
|
||||
}
|
||||
|
||||
interface ValidationResult {
|
||||
ruleId: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
message: string;
|
||||
location?: string; // XPath
|
||||
context?: any;
|
||||
}
|
||||
```
|
||||
|
||||
### Security Considerations:
|
||||
- Disable DTD/XXE in XML parsing
|
||||
- Enforce document size limits
|
||||
- Sandbox XSLT execution
|
||||
- Validate only trusted rule packs
|
||||
|
||||
## Success Criteria
|
||||
- Pass official test suites for each standard
|
||||
- 100% coverage of mandatory rules
|
||||
- Performance: <100ms for full validation
|
||||
- Clear error messages with rule IDs and locations
|
||||
|
||||
## Resources Needed
|
||||
- EN16931 Schematron from official sources
|
||||
- XRechnung artifacts for current version
|
||||
- Peppol BIS 3.0 Schematron
|
||||
- Factur-X profile documentation
|
||||
- Official test invoices for each standard
|
||||
|
||||
## Risk Mitigation
|
||||
- Version pinning for rule packs
|
||||
- Snapshot testing for regression detection
|
||||
- Configurable tolerances for calculations
|
||||
- Layer precedence for conflicting rules
|
||||
|
||||
## Immediate Next Steps
|
||||
1. ~~Set up Saxon-JS for Schematron integration~~ ✅ Complete
|
||||
2. ~~Download official EN16931 Schematron files~~ ✅ Complete
|
||||
3. ~~Create hybrid validation pipeline~~ ✅ Complete
|
||||
4. ~~Implement ISO 4217 currency-aware rounding~~ ✅ Complete
|
||||
5. ~~Complete remaining VAT category rules~~ ✅ Complete
|
||||
6. ~~Add conformance test harness~~ ✅ Complete
|
||||
7. ~~Implement decimal arithmetic library~~ ✅ Complete (2025-01-11)
|
||||
8. Add XRechnung CIUS support (next priority)
|
||||
9. Implement PEPPOL BIS 3.0 overlay
|
||||
|
||||
## Accomplishments (2025-01-11)
|
||||
|
||||
### Implemented Components:
|
||||
1. **EN16931BusinessRulesValidator** (`ts/formats/validation/en16931.business-rules.validator.ts`)
|
||||
- ~40 business rules implemented
|
||||
- Document, calculation, VAT, and line-level validation
|
||||
- Currency-aware calculation verification with ISO 4217 support
|
||||
|
||||
2. **CodeListValidator** (`ts/formats/validation/codelist.validator.ts`)
|
||||
- All major code lists validated
|
||||
- Currency, country, tax category, payment means, unit codes
|
||||
- Context-aware validation with exemption reasons
|
||||
|
||||
3. **Enhanced ValidationResult Interface** (`ts/formats/validation/validation.types.ts`)
|
||||
- Business Term (BT) and Business Group (BG) references
|
||||
- Semantic model mapping
|
||||
- Code list metadata
|
||||
- Remediation hints
|
||||
|
||||
4. **Extended Metadata Support** (`ts/interfaces/en16931-metadata.ts`)
|
||||
- Comprehensive EN16931 metadata fields
|
||||
- Delivery addresses, payment accounts
|
||||
- Allowances and charges structure
|
||||
|
||||
5. **Feature Flag Integration**
|
||||
- Gradual rollout capability
|
||||
- Backward compatibility maintained
|
||||
- Separate flags for EN16931 and code lists
|
||||
|
||||
6. **ISO 4217 Currency-Aware Rounding** (`ts/formats/utils/currency.utils.ts`)
|
||||
- Complete ISO 4217 currency minor units database
|
||||
- 7 rounding modes (HALF_UP, HALF_DOWN, HALF_EVEN, UP, DOWN, CEILING, FLOOR)
|
||||
- CurrencyCalculator class for EN16931 calculations
|
||||
- Replaces flat 0.01 tolerance with currency-specific tolerances
|
||||
|
||||
7. **Saxon-JS Schematron Integration** (`ts/formats/validation/schematron.*.ts`)
|
||||
- Saxon-JS for XSLT 3.0 processing
|
||||
- Official EN16931 Schematron files downloaded (v1.3.14)
|
||||
- Worker thread pool for non-blocking validation
|
||||
- Hybrid validator combining TypeScript and Schematron
|
||||
- Automatic format detection (UBL/CII)
|
||||
- SVRL parsing and result integration
|
||||
|
||||
### Test Coverage:
|
||||
- Unit tests for validators (`test/test.en16931-validators.ts`)
|
||||
- Currency utilities tests (`test/test.currency-utils.ts`) - 7/7 passing
|
||||
- Schematron infrastructure tests (`test/test.schematron-validator.ts`) - 9/9 passing
|
||||
- Integration with existing test suite
|
||||
- 496/497 tests passing overall (added 16 new tests)
|
||||
|
||||
8. **VAT Categories Validator** (`ts/formats/validation/vat-categories.validator.ts`)
|
||||
- Complete implementation of all VAT category business rules
|
||||
- BR-S-* (Standard rate), BR-Z-* (Zero rated), BR-E-* (Exempt)
|
||||
- BR-AE-* (Reverse charge), BR-K-* (Intra-community), BR-G-* (Export)
|
||||
- BR-O-* (Out of scope services)
|
||||
- Cross-category validation rules
|
||||
|
||||
9. **Conformance Test Harness** (`ts/formats/validation/conformance.harness.ts`)
|
||||
- Automated testing against official samples
|
||||
- BR coverage matrix generation
|
||||
- HTML coverage reports
|
||||
- Support for PEPPOL and CEN test suites
|
||||
- Performance metrics collection
|
||||
|
||||
10. **Test Sample Downloader** (`scripts/download-test-samples.ts`)
|
||||
- Automated download from official repositories
|
||||
- PEPPOL BIS 3.0 examples
|
||||
- CEN TC434 test files
|
||||
- Metadata tracking
|
||||
|
||||
11. **XML to EInvoice Converter** (`ts/formats/converters/xml-to-einvoice.converter.ts`)
|
||||
- Basic UBL and CII parsing
|
||||
- Integration with conformance testing
|
||||
|
||||
12. **Decimal Arithmetic Library** (`ts/formats/utils/decimal.ts`) ✅ COMPLETE
|
||||
- Arbitrary precision decimal arithmetic using BigInt
|
||||
- Eliminates all floating-point errors in financial calculations
|
||||
- Complete implementation with all arithmetic operations
|
||||
- Multiple rounding modes (HALF_UP, HALF_DOWN, HALF_EVEN, UP, DOWN, CEILING, FLOOR)
|
||||
- Full test coverage - 10/10 tests passing
|
||||
|
||||
13. **DecimalCurrencyCalculator** (`ts/formats/utils/currency.calculator.decimal.ts`) ✅ COMPLETE
|
||||
- Currency-aware calculations using Decimal arithmetic
|
||||
- ISO 4217 currency minor units integration
|
||||
- Line item calculations, VAT calculations, amount distribution
|
||||
- Compound adjustments and payment discount calculations
|
||||
- Validation helpers for EN16931 compliance
|
||||
- Full test coverage - 10/10 tests passing
|
||||
|
||||
14. **XRechnung CIUS Validator** (`ts/formats/validation/xrechnung.validator.ts`) ✅ COMPLETE
|
||||
- Leitweg-ID validation for German B2G invoicing
|
||||
- IBAN/BIC validation with SEPA zone checking (mod-97 checksum algorithm)
|
||||
- Mandatory field validations (buyer reference, seller contact)
|
||||
- German VAT ID and Tax ID format validation
|
||||
- Profile-based automatic activation
|
||||
- SEPA zone validation (36 countries)
|
||||
- Full test coverage - 15/15 tests passing
|
||||
|
||||
15. **Integrated Validator** (`ts/formats/validation/integrated.validator.ts`) ✅ COMPLETE
|
||||
- MainValidator class combining all validation capabilities
|
||||
- Automatic profile detection (EN16931, XRechnung, PEPPOL, Factur-X)
|
||||
- Schematron integration with fallback to TypeScript validators
|
||||
- Deduplication of validation results
|
||||
- Coverage tracking and reporting
|
||||
- Format detection (UBL/CII) from XML content
|
||||
- Capabilities reporting for feature discovery
|
||||
- Full test coverage - 6/6 tests passing
|
||||
|
||||
16. **PEPPOL BIS 3.0 Validator** (`ts/formats/validation/peppol.validator.ts`) ✅ COMPLETE
|
||||
- Complete PEPPOL BIS 3.0 validation overlay on EN16931
|
||||
- Endpoint ID validation with scheme:identifier format (e.g., 0088:1234567890128)
|
||||
- GLN (Global Location Number) checksum validation using modulo 10
|
||||
- Document type ID validation for PEPPOL network compatibility
|
||||
- Process ID validation for billing processes
|
||||
- Party identification scheme validation against ISO 6523 ICD list (80+ schemes)
|
||||
- GTIN (Global Trade Item Number) validation for item identifiers
|
||||
- PEPPOL-specific business rules (buyer reference, seller email, etc.)
|
||||
- B2G (Business to Government) detection and requirements
|
||||
- UNCL4461 payment means code validation
|
||||
- Transport protocol validation (AS2/AS4)
|
||||
- Singleton pattern implementation
|
||||
- Full test coverage - 16/16 tests passing
|
||||
|
||||
17. **Factur-X Validator** (`ts/formats/validation/facturx.validator.ts`) ✅ COMPLETE
|
||||
- Complete Factur-X profile support with all 5 profiles
|
||||
- Profile detection and automatic validation selection
|
||||
- MINIMUM profile: Essential fields only (BT-1, BT-2, BT-3, totals)
|
||||
- BASIC profile: Core invoice fields with line items
|
||||
- BASIC_WL profile: Basic without lines for summary invoices
|
||||
- EN16931 profile: Full EN16931 compliance requirements
|
||||
- EXTENDED profile: Support for additional structured data
|
||||
- Field cardinality enforcement per profile
|
||||
- ZUGFeRD format compatibility (German variant)
|
||||
- Profile compliance level tracking (1-5 scale)
|
||||
- Special handling for calculated vs direct field values
|
||||
- Support for both EInvoice getters and test properties
|
||||
- Full test coverage - 15/15 tests passing
|
||||
|
||||
18. **EN16931 Semantic Model** (`ts/formats/semantic/`) ✅ COMPLETE
|
||||
- **BT/BG Model** (`bt-bg.model.ts`): Complete EN16931 semantic model
|
||||
- All 162 Business Terms (BT-1 to BT-162) defined
|
||||
- All 32 Business Groups (BG-1 to BG-32) structured
|
||||
- Full TypeScript interfaces for type safety
|
||||
- **Semantic Adapter** (`semantic.adapter.ts`): Bidirectional conversion
|
||||
- EInvoice to EN16931SemanticModel conversion
|
||||
- EN16931SemanticModel to EInvoice conversion
|
||||
- Support for complex TContact structures
|
||||
- VAT breakdown and document totals mapping
|
||||
- Payment instructions and references handling
|
||||
- **Semantic Validator** (`semantic.validator.ts`): BT/BG validation
|
||||
- Mandatory business term validation
|
||||
- Business group cardinality checking
|
||||
- Conditional rule validation
|
||||
- BT/BG mapping for reporting
|
||||
- Full test coverage - 9/9 tests passing
|
||||
|
||||
### Compliance Achievement Summary:
|
||||
1. ~~Set up Saxon-JS for Schematron integration~~ ✅ COMPLETE
|
||||
2. ~~Integrate official EN16931 Schematron from ConnectingEurope~~ ✅ COMPLETE
|
||||
3. ~~Complete remaining VAT category rules~~ ✅ COMPLETE
|
||||
4. ~~Add conformance test harness with official test packs~~ ✅ COMPLETE
|
||||
5. ~~Implement decimal arithmetic for precision~~ ✅ COMPLETE (2025-01-11)
|
||||
6. ~~Add XRechnung CIUS layer~~ ✅ MOSTLY COMPLETE (2025-01-11)
|
||||
7. ~~Integrate Schematron into main validation pipeline~~ ✅ COMPLETE (2025-01-11)
|
||||
8. ~~Implement PEPPOL BIS 3.0 support~~ ✅ COMPLETE (2025-01-11)
|
||||
9. ~~Add Factur-X Profiles support~~ ✅ COMPLETE (2025-01-11)
|
||||
10. ~~Build canonical semantic model (BT/BG fields)~~ ✅ COMPLETE (2025-01-11)
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-16 - 5.2.0 - feat(core)
|
||||
improve in-memory validation, FatturaPA detection coverage, and published type compatibility
|
||||
|
||||
- add validation support for programmatically created invoices without requiring loaded XML, including structured EN16931 mandatory-field errors and validation caching
|
||||
- tighten FatturaPA handling by asserting explicit format detection while keeping decode unsupported with clear test coverage
|
||||
- improve published consumer compatibility with typed vendor wrappers, lazy PDF utility initialization, stricter invoice type enforcement, and a strict typecheck consumer test
|
||||
- update package tooling and metadata configuration, including newer build/test dependencies and a published typecheck script
|
||||
|
||||
## 2025-08-11 - 5.1.1 - fix(build/publishing)
|
||||
Remove migration guide and update publishing and schematron download configurations
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+12
-11
@@ -11,6 +11,7 @@
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"typecheck:published": "pnpm exec tsc -p test/strict-consumer/tsconfig.json",
|
||||
"buildDocs": "(tsdoc)",
|
||||
"postinstall": "node dist_ts_install/index.js 2>/dev/null || true",
|
||||
"download-schematron": "tsx ts_install/download-schematron.ts",
|
||||
@@ -18,18 +19,18 @@
|
||||
"test:conformance": "tstest test/test.conformance-harness.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@types/node": "^22.15.23"
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartfile": "^11.2.5",
|
||||
"@push.rocks/smartxml": "^1.1.1",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"jsdom": "^26.1.0",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartxml": "^2.0.0",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
"@xmldom/xmldom": "^0.9.9",
|
||||
"jsdom": "^29.0.2",
|
||||
"pako": "^2.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"saxon-js": "^2.7.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"keywords": [
|
||||
|
||||
Generated
+4242
-4589
File diff suppressed because it is too large
Load Diff
+24
-8
@@ -1,11 +1,11 @@
|
||||
For testing use
|
||||
|
||||
```typescript
|
||||
import {tap, expect} @push.rocks/tapbundle
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
```
|
||||
|
||||
tapbundle exports expect from @push.rocks/smartexpect
|
||||
You can find the readme here: https://code.foss.global/push.rocks/smartexpect/src/branch/master/readme.md
|
||||
tapbundle is provided by `@git.zone/tstest`.
|
||||
You can find the readme here: https://code.foss.global/git.zone/tstest
|
||||
|
||||
This module also uses @tsclass/tsclass: You can find the TInvoice type here: https://code.foss.global/tsclass/tsclass/src/branch/master/ts/finance/invoice.ts
|
||||
|
||||
@@ -15,6 +15,22 @@ It is ok to ask questions, if you are unsure about something.
|
||||
|
||||
---
|
||||
|
||||
# Upgrade Notes (2026-04-16)
|
||||
|
||||
- Command: `/c-upgrade`
|
||||
- Files modified: 2
|
||||
- Dependency status: `pnpm outdated --format json` returned `{}`, so no package version bumps were needed.
|
||||
- Decorators: no decorator usage was found in `*.ts`, so TC39 decorator migration was not required.
|
||||
- Pattern changes:
|
||||
- Removed obsolete `experimentalDecorators` and `useDefineForClassFields` compiler options from `tsconfig.json`.
|
||||
- Updated the stale test import hint from `@push.rocks/tapbundle` to `@git.zone/tstest/tapbundle`.
|
||||
- Verification:
|
||||
- `pnpm run build`: passed
|
||||
- `pnpm test`: failed due to pre-existing test issues in `test/test.conformance-harness.ts` (no tests defined) and `test/suite/einvoice_security/test.sec-06.memory-dos.ts` (assertion failure at line 142)
|
||||
- Issues encountered: full test suite is not green before or after this minimal upgrade because of the pre-existing failures above.
|
||||
|
||||
---
|
||||
|
||||
# Architecture Analysis (2025-01-31)
|
||||
|
||||
## Overall Architecture
|
||||
@@ -490,7 +506,7 @@ countryCode: country
|
||||
|
||||
### **MILESTONE REACHED: The module now achieves 100% data preservation in round-trip conversions!**
|
||||
|
||||
This makes the module fully spec-compliant and suitable as the default open-source e-invoicing solution.
|
||||
This materially improved round-trip data preservation, but it did not by itself prove full standards compliance across every supported format and profile.
|
||||
|
||||
### Data Preservation Improvements:
|
||||
- Initial preservation score: 51%
|
||||
@@ -800,7 +816,7 @@ Successfully fixed all remaining test failures to achieve 100% test pass rate:
|
||||
- Format detection: <5ms average for most formats
|
||||
- PDF extraction: Successfully extracts from ZUGFeRD v1/v2 and Factur-X PDFs
|
||||
|
||||
All tests are now passing, making the library fully spec-compliant and production-ready.
|
||||
The targeted test suites available at that point were passing, but that still did not establish full standards compliance or production readiness across every supported format/profile.
|
||||
|
||||
---
|
||||
|
||||
@@ -855,10 +871,10 @@ for (const idNode of partyIdNodes) {
|
||||
```
|
||||
|
||||
### FatturaPA (Italian Standard)
|
||||
While not fully implemented as decoder/encoder, the library detects FatturaPA format:
|
||||
FatturaPA currently has format detection, but not full decoder/encoder support:
|
||||
- Detects root element `<FatturaElettronica>`
|
||||
- Recognizes namespace `fatturapa.gov.it`
|
||||
- Supports mixed UBL+FatturaPA documents
|
||||
- May classify mixed UBL+FatturaPA documents as FatturaPA during detection
|
||||
|
||||
## 3. Advanced Validation Architecture
|
||||
|
||||
@@ -1104,4 +1120,4 @@ Each format has its own implementation strategy while maintaining common interfa
|
||||
5. **Extensible Design**: New formats can be added without core changes
|
||||
6. **Production Ready**: Handles edge cases, malformed input, and large files
|
||||
|
||||
The library represents a mature, well-architected solution for European e-invoicing with careful attention to both standards compliance and practical usage scenarios.
|
||||
The library represents a mature, well-architected solution for European e-invoicing with careful attention to both standards compliance and practical usage scenarios.
|
||||
|
||||
@@ -1,443 +1,310 @@
|
||||
# @fin.cx/einvoice 🚀
|
||||
# @fin.cx/einvoice ⚡
|
||||
|
||||
**The Ultimate TypeScript E-Invoicing Library for Europe** - Now with **100% EN16931 Compliance** ✅
|
||||
TypeScript e-invoicing for the real world: load invoice XML or hybrid PDFs, detect the format, map it into a typed invoice model, validate it, convert it, and embed XML back into PDFs.
|
||||
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.cen.eu/work/areas/ict/ebusiness/pages/einvoicing.aspx)
|
||||
[](https://github.com/fin-cx/einvoice)
|
||||
[](./license)
|
||||
`@fin.cx/einvoice` is built for programmers who need a practical toolkit around European e-invoice formats without writing a parser zoo from scratch.
|
||||
|
||||
Transform the chaos of European e-invoicing into pure TypeScript elegance. **@fin.cx/einvoice** is your battle-tested solution for creating, validating, and converting electronic invoices across all major European standards - with blazing fast performance and enterprise-grade reliability.
|
||||
## Issue Reporting and Security
|
||||
|
||||
## 🎯 Why @fin.cx/einvoice?
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
- **🏆 100% EN16931 Compliant**: Full implementation of all 162 Business Terms and 32 Business Groups
|
||||
- **⚡ Blazing Fast**: Validate invoices in ~2.2ms, convert formats in ~0.6ms
|
||||
- **🔐 Enterprise Security**: XXE prevention, resource limits, path traversal protection
|
||||
- **🌍 Multi-Standard Support**: ZUGFeRD, Factur-X, XRechnung, PEPPOL BIS 3.0, UBL, and more
|
||||
- **💎 Decimal Precision**: Arbitrary precision arithmetic for perfect financial calculations
|
||||
- **🔄 Lossless Conversion**: 100% data preservation in round-trip conversions
|
||||
- **📦 PDF Magic**: Extract and embed XML in PDF/A-3 documents seamlessly
|
||||
- **🛠️ TypeScript First**: Fully typed with IntelliSense support throughout
|
||||
## Why this library?
|
||||
|
||||
## 🚀 Quick Start
|
||||
- 🚀 Load invoices from XML strings, files, or PDFs with embedded XML.
|
||||
- 🧭 Detect `ubl`, `xrechnung`, `cii`, `facturx`, `zugferd`, and `fatturapa` documents.
|
||||
- 🧾 Work on a typed in-memory invoice model based on `@tsclass/tsclass`.
|
||||
- ✅ Validate invoices on syntax, semantic, and business-rule levels.
|
||||
- 🔄 Export invoices as `facturx`, `zugferd`, `xrechnung`, `ubl`, or `cii`.
|
||||
- 📎 Extract XML from invoice PDFs and attach XML back into existing PDFs.
|
||||
- 🧱 Use the high-level `EInvoice` class or lower-level decoders, encoders, validators, and PDF extractors directly.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Using pnpm (recommended)
|
||||
pnpm add @fin.cx/einvoice
|
||||
|
||||
# Using npm
|
||||
npm install @fin.cx/einvoice
|
||||
|
||||
# Using yarn
|
||||
yarn add @fin.cx/einvoice
|
||||
```
|
||||
|
||||
### One-Minute Example
|
||||
`postinstall` will try to download Schematron validation resources on a best-effort basis when the package is installed in a normal online environment. Install will not fail if the resources cannot be downloaded.
|
||||
|
||||
```typescript
|
||||
import { EInvoice } from '@fin.cx/einvoice';
|
||||
Useful commands:
|
||||
|
||||
// Load from any source
|
||||
const invoice = await EInvoice.fromFile('invoice.xml'); // From file
|
||||
const invoice2 = await EInvoice.fromXml(xmlString); // From XML string
|
||||
const invoice3 = await EInvoice.fromPdf(pdfBuffer); // From PDF with embedded XML
|
||||
|
||||
// Validate with comprehensive EN16931 rules
|
||||
const validation = await invoice.validate();
|
||||
console.log(`Valid: ${validation.valid}`);
|
||||
|
||||
// Convert between any formats - losslessly!
|
||||
const xrechnung = await invoice.exportXml('xrechnung'); // For German B2G
|
||||
const peppol = await invoice.exportXml('ubl'); // For PEPPOL network
|
||||
const facturx = await invoice.exportXml('facturx'); // For France/Germany
|
||||
const zugferd = await invoice.exportXml('zugferd'); // For German standard
|
||||
|
||||
// Embed into PDF for hybrid invoices
|
||||
const pdfWithXml = await invoice.exportPdf('facturx');
|
||||
```bash
|
||||
pnpm download-schematron
|
||||
pnpm download-test-samples
|
||||
```
|
||||
|
||||
## 🏗️ Complete Invoice Creation
|
||||
Useful environment flag:
|
||||
|
||||
```typescript
|
||||
```bash
|
||||
EINVOICE_SKIP_RESOURCES=1
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';
|
||||
|
||||
const invoice = await EInvoice.fromFile('./invoice.xml');
|
||||
|
||||
console.log(invoice.getFormat());
|
||||
console.log(invoice.id);
|
||||
console.log(invoice.from.name, '->', invoice.to.name);
|
||||
|
||||
const validation = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
if (!validation.valid) {
|
||||
console.log(validation.errors);
|
||||
}
|
||||
|
||||
const xrechnungXml = await invoice.exportXml('xrechnung');
|
||||
```
|
||||
|
||||
## What it can do
|
||||
|
||||
### Load and inspect invoices
|
||||
|
||||
```ts
|
||||
import { EInvoice } from '@fin.cx/einvoice';
|
||||
|
||||
const fromXml = await EInvoice.fromXml(xmlString);
|
||||
const fromFile = await EInvoice.fromFile('./invoice.xml');
|
||||
const fromPdf = await EInvoice.fromPdf(pdfBuffer);
|
||||
|
||||
console.log(fromXml.subject);
|
||||
console.log(fromXml.items.length);
|
||||
console.log(fromXml.currency);
|
||||
```
|
||||
|
||||
### Create invoices in code
|
||||
|
||||
```ts
|
||||
import { EInvoice } from '@fin.cx/einvoice';
|
||||
|
||||
// Create a fully compliant invoice from scratch
|
||||
const invoice = new EInvoice();
|
||||
|
||||
// Essential metadata
|
||||
invoice.accountingDocId = 'INV-2025-001';
|
||||
invoice.issueDate = new Date('2025-01-15');
|
||||
invoice.accountingDocType = 'invoice';
|
||||
invoice.accountingDocId = 'INV-2026-001';
|
||||
invoice.issueDate = new Date('2026-04-16');
|
||||
invoice.currency = 'EUR';
|
||||
invoice.dueInDays = 30;
|
||||
invoice.dueInDays = 14;
|
||||
|
||||
// Seller information
|
||||
invoice.from = {
|
||||
type: 'company',
|
||||
name: 'Tech Solutions GmbH',
|
||||
name: 'Sender GmbH',
|
||||
description: '',
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
address: {
|
||||
streetName: 'Innovation Street',
|
||||
houseNumber: '42',
|
||||
streetName: 'Example Street',
|
||||
houseNumber: '1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10115',
|
||||
country: 'DE'
|
||||
country: 'DE',
|
||||
},
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 123456',
|
||||
registrationName: 'Tech Solutions GmbH'
|
||||
registrationName: 'Sender GmbH',
|
||||
},
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
// Buyer information
|
||||
invoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer Corp SAS',
|
||||
name: 'Receiver SAS',
|
||||
description: '',
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
address: {
|
||||
streetName: 'Rue de la Paix',
|
||||
streetName: 'Rue Example',
|
||||
houseNumber: '10',
|
||||
city: 'Paris',
|
||||
postalCode: '75001',
|
||||
country: 'FR'
|
||||
country: 'FR',
|
||||
},
|
||||
registrationDetails: {
|
||||
vatId: 'FR987654321',
|
||||
registrationId: 'RCS Paris 987654321'
|
||||
}
|
||||
vatId: 'FR12345678901',
|
||||
registrationId: 'RCS 123456789',
|
||||
registrationName: 'Receiver SAS',
|
||||
},
|
||||
};
|
||||
|
||||
// Payment details - SEPA ready
|
||||
invoice.paymentAccount = {
|
||||
iban: 'DE89370400440532013000',
|
||||
bic: 'COBADEFFXXX',
|
||||
accountName: 'Tech Solutions GmbH',
|
||||
institutionName: 'Commerzbank'
|
||||
};
|
||||
|
||||
// Line items with automatic calculations
|
||||
invoice.items = [
|
||||
{
|
||||
position: 1,
|
||||
name: 'Cloud Infrastructure Services',
|
||||
description: 'Monthly cloud hosting and support',
|
||||
articleNumber: 'CLOUD-PRO-001',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 2500.00,
|
||||
name: 'Implementation work',
|
||||
articleNumber: 'IMPL-001',
|
||||
unitType: 'HUR',
|
||||
unitQuantity: 8,
|
||||
unitNetPrice: 120,
|
||||
vatPercentage: 19,
|
||||
unitType: 'MON' // Month
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
name: 'Professional Consulting',
|
||||
description: 'Architecture review and optimization',
|
||||
articleNumber: 'CONSULT-001',
|
||||
unitQuantity: 16,
|
||||
unitNetPrice: 150.00,
|
||||
vatPercentage: 19,
|
||||
unitType: 'HUR' // Hour
|
||||
}
|
||||
];
|
||||
|
||||
// Export to any format you need
|
||||
const zugferdXml = await invoice.exportXml('zugferd');
|
||||
const pdfWithXml = await invoice.exportPdf('facturx');
|
||||
const xml = await invoice.exportXml('facturx');
|
||||
```
|
||||
|
||||
## 🎨 Supported Standards & Formats
|
||||
### Validate at different levels
|
||||
|
||||
| Standard | Version | Status | Use Case |
|
||||
|----------|---------|--------|----------|
|
||||
| **EN16931** | 2017 | ✅ 100% Complete | Core European standard |
|
||||
| **ZUGFeRD** | 1.0, 2.0, 2.1 | ✅ Full Support | German B2B/B2C |
|
||||
| **Factur-X** | 1.0 (all profiles) | ✅ Full Support | France/Germany |
|
||||
| **XRechnung** | 2.0, 3.0 | ✅ Full Support | German public sector |
|
||||
| **PEPPOL BIS 3.0** | 3.0 | ✅ Full Support | Cross-border B2G |
|
||||
| **UBL** | 2.1 | ✅ Full Support | International |
|
||||
| **CII** | D16B | ✅ Full Support | Cross Industry |
|
||||
```ts
|
||||
import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';
|
||||
|
||||
### 📋 Factur-X Profile Support
|
||||
const invoice = await EInvoice.fromXml(xmlString);
|
||||
|
||||
```typescript
|
||||
// Automatic profile detection and validation
|
||||
const profiles = {
|
||||
MINIMUM: 'Essential fields only (BT-1, BT-2, BT-3)',
|
||||
BASIC: 'Core invoice with line items',
|
||||
BASIC_WL: 'Basic without lines (summary invoices)',
|
||||
EN16931: 'Full EN16931 compliance',
|
||||
EXTENDED: 'Additional structured data'
|
||||
};
|
||||
```
|
||||
|
||||
## 🔥 Power Features
|
||||
|
||||
### 🧮 Decimal Precision for Financial Accuracy
|
||||
|
||||
No more floating-point errors! Built-in arbitrary precision arithmetic:
|
||||
|
||||
```typescript
|
||||
// Perfect financial calculations every time
|
||||
const calculator = new DecimalCurrencyCalculator('EUR');
|
||||
const result = calculator.calculateLineNet(
|
||||
'3.14159', // Quantity
|
||||
'999.99', // Unit price
|
||||
'0' // Discount (optional)
|
||||
);
|
||||
// Result: 3141.56 (correctly rounded for EUR)
|
||||
```
|
||||
|
||||
### 🔍 Multi-Level Validation
|
||||
|
||||
```typescript
|
||||
// Three-layer validation with detailed diagnostics
|
||||
const syntaxResult = await invoice.validate(ValidationLevel.SYNTAX);
|
||||
const semanticResult = await invoice.validate(ValidationLevel.SEMANTIC);
|
||||
const businessResult = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
|
||||
// Get specific rule violations
|
||||
businessResult.errors.forEach(error => {
|
||||
console.log(`Rule ${error.ruleId}: ${error.message}`);
|
||||
console.log(`Business Term: ${error.btReference}`);
|
||||
console.log(`Field: ${error.field}`);
|
||||
const syntax = await invoice.validate(ValidationLevel.SYNTAX);
|
||||
const semantic = await invoice.validate(ValidationLevel.SEMANTIC);
|
||||
const business = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
|
||||
reportOnly: true,
|
||||
});
|
||||
|
||||
console.log(business.valid);
|
||||
console.log(business.errors);
|
||||
console.log(business.warnings ?? []);
|
||||
```
|
||||
|
||||
### 🔄 Format Detection & Conversion
|
||||
### Convert between supported XML targets
|
||||
|
||||
```typescript
|
||||
// Automatic format detection
|
||||
const format = FormatDetector.detectFormat(xmlString);
|
||||
console.log(`Detected: ${format}`); // 'zugferd', 'facturx', 'xrechnung', etc.
|
||||
```ts
|
||||
import { EInvoice } from '@fin.cx/einvoice';
|
||||
|
||||
// Intelligent conversion preserves all data
|
||||
const zugferd = await EInvoice.fromFile('zugferd.xml');
|
||||
const xrechnung = await zugferd.exportXml('xrechnung');
|
||||
const backToZugferd = await EInvoice.fromXml(xrechnung);
|
||||
// All data preserved through round-trip!
|
||||
const invoice = await EInvoice.fromFile('./source.xml');
|
||||
|
||||
const facturxXml = await invoice.exportXml('facturx');
|
||||
const zugferdXml = await invoice.exportXml('zugferd');
|
||||
const xrechnungXml = await invoice.exportXml('xrechnung');
|
||||
const ublXml = await invoice.exportXml('ubl');
|
||||
const ciiXml = await invoice.exportXml('cii');
|
||||
```
|
||||
|
||||
### 📄 PDF Operations
|
||||
### Work with PDFs
|
||||
|
||||
```typescript
|
||||
// Extract XML from PDF invoices
|
||||
const extractor = new PDFExtractor();
|
||||
const result = await extractor.extractXml(pdfBuffer);
|
||||
if (result.success) {
|
||||
console.log(`Found ${result.format} invoice`);
|
||||
const invoice = await EInvoice.fromXml(result.xml);
|
||||
```ts
|
||||
import { EInvoice, PDFExtractor } from '@fin.cx/einvoice';
|
||||
|
||||
const extracted = await new PDFExtractor().extractXml(pdfBuffer);
|
||||
if (extracted.success && extracted.xml) {
|
||||
const invoice = await EInvoice.fromXml(extracted.xml);
|
||||
console.log(extracted.format, invoice.id);
|
||||
}
|
||||
|
||||
// Embed XML into PDF for hybrid invoices
|
||||
const embedder = new PDFEmbedder();
|
||||
const pdfWithXml = await embedder.createPdfWithXml(
|
||||
existingPdf,
|
||||
xmlContent,
|
||||
'factur-x.xml',
|
||||
'Factur-X Invoice'
|
||||
);
|
||||
const invoice = await EInvoice.fromXml(xmlString);
|
||||
const pdfWithXml = await invoice.embedInPdf(existingPdfBuffer, 'facturx');
|
||||
```
|
||||
|
||||
## 🌍 Country-Specific Requirements
|
||||
## Supported formats
|
||||
|
||||
### 🇩🇪 German XRechnung
|
||||
| Format | Detect | Import | Export | Validation | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `ubl` | Yes | Yes | Yes | Yes | Generic UBL flow |
|
||||
| `xrechnung` | Yes | Yes | Yes | Yes | UBL-based profile |
|
||||
| `cii` | Yes | Yes | Yes | Partial | Generic CII export currently uses the Factur-X encoder path |
|
||||
| `facturx` | Yes | Yes | Yes | Yes | Main CII-based generation path |
|
||||
| `zugferd` | Yes | Yes | Yes | Partial | Input supports v1 and v2+, export focuses on the current encoder |
|
||||
| `fatturapa` | Yes | No | No | Basic placeholder | Detection exists, decoder/encoder do not |
|
||||
|
||||
```typescript
|
||||
invoice.metadata = {
|
||||
customizationId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
|
||||
extensions: {
|
||||
leitwegId: '991-12345-67', // Required routing ID
|
||||
buyerReference: 'DE-BUYER-REF', // Mandatory
|
||||
sellerContact: {
|
||||
name: 'Max Mustermann',
|
||||
phone: '+49 30 12345678',
|
||||
email: 'invoice@company.de'
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
### Important format notes
|
||||
|
||||
### 🇪🇺 PEPPOL BIS 3.0
|
||||
- There is no dedicated root-level `peppol` export format. PEPPOL-specific checks exist, but the XML export targets are still `ubl` / `xrechnung`.
|
||||
- `FatturaPA` should be documented as detection-only right now.
|
||||
- `creditnote` / `debitnote` handling exists in parts of the lower-level code, but the high-level `invoiceType` setter on `EInvoice` only accepts `'invoice'`.
|
||||
|
||||
```typescript
|
||||
invoice.metadata = {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
|
||||
extensions: {
|
||||
endpointId: '0088:1234567890128', // GLN with checksum
|
||||
documentTypeId: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017',
|
||||
processId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'
|
||||
}
|
||||
};
|
||||
```
|
||||
## Public API overview
|
||||
|
||||
### 🇫🇷 French Chorus Pro
|
||||
Top-level exports from `@fin.cx/einvoice` include:
|
||||
|
||||
```typescript
|
||||
invoice.metadata = {
|
||||
extensions: {
|
||||
siret: '12345678901234',
|
||||
serviceCode: 'SERVICE-2025',
|
||||
engagementNumber: 'ENG-123456',
|
||||
marketReference: 'MARKET-REF-001'
|
||||
}
|
||||
};
|
||||
```
|
||||
- `EInvoice`
|
||||
- `createEInvoice()`
|
||||
- `validateXml(xml, level?)`
|
||||
- `FormatDetector`
|
||||
- `PDFExtractor`, `PDFEmbedder`
|
||||
- `DecoderFactory`, `EncoderFactory`, `ValidatorFactory`
|
||||
- `BaseDecoder`, `BaseEncoder`, `BaseValidator`
|
||||
- `UBLBase*` and `CIIBase*` extension classes
|
||||
- `FacturX*` and `ZUGFeRD*` format-specific classes
|
||||
- `ValidationLevel`, `InvoiceFormat`
|
||||
- exported types like `TInvoice`, `ValidationResult`, `ValidationError`, `ExportFormat`, `IPdf`, `EInvoiceOptions`
|
||||
|
||||
## ⚡ Performance Metrics
|
||||
## Validation resources and install-time behavior
|
||||
|
||||
Lightning-fast operations with minimal memory footprint:
|
||||
The package includes an install bootstrap in `ts_install/` that tries to download validation resources into `assets_downloaded/schematron`.
|
||||
|
||||
| Operation | Speed | Memory |
|
||||
|-----------|-------|--------|
|
||||
| **Format Detection** | ~0.1ms | Minimal |
|
||||
| **XML Parsing** | ~0.5ms | ~100KB |
|
||||
| **Full Validation** | ~2.2ms | ~136KB |
|
||||
| **Format Conversion** | ~0.6ms | ~150KB |
|
||||
| **PDF Extraction** | ~5ms | ~1MB |
|
||||
| **PDF Embedding** | ~10ms | ~2MB |
|
||||
Behavior to know:
|
||||
|
||||
## 🏗️ Architecture
|
||||
- It only runs in a real `postinstall` lifecycle.
|
||||
- It skips cleanly when offline.
|
||||
- It skips when `dist_ts/` is not present yet.
|
||||
- It can be disabled in CI with `EINVOICE_SKIP_RESOURCES=1`.
|
||||
- Missing resources do not break install, but they can reduce advanced validation coverage.
|
||||
|
||||
### Plugin-Based Design
|
||||
|
||||
```typescript
|
||||
// Factory pattern for extensibility
|
||||
DecoderFactory.getDecoder(format) → BaseDecoder
|
||||
EncoderFactory.getEncoder(format) → BaseEncoder
|
||||
ValidatorFactory.getValidator(format) → BaseValidator
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Input (XML/PDF) → Format Detection → Decoder → EInvoice Model
|
||||
↓
|
||||
Validation
|
||||
↓
|
||||
Encoder → Output (XML/PDF)
|
||||
```
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
- **XXE Prevention**: External entities disabled by default
|
||||
- **Resource Limits**: Max 100MB XML, max 100 nesting levels
|
||||
- **Path Traversal Protection**: Sanitized filenames in PDFs
|
||||
- **SSRF Mitigation**: Entity blocking in XML processing
|
||||
- **Input Validation**: Comprehensive input sanitization
|
||||
|
||||
## 🧪 Testing
|
||||
If you need the resources manually:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run specific test suites
|
||||
pnpm test test/test.peppol-validator.ts
|
||||
pnpm test test/test.facturx-validator.ts
|
||||
pnpm test test/test.semantic-model.ts
|
||||
|
||||
# Run with verbose output
|
||||
pnpm test -- --verbose
|
||||
pnpm download-schematron
|
||||
pnpm download-test-samples
|
||||
```
|
||||
|
||||
## 📚 Advanced Examples
|
||||
## Architecture in one screen
|
||||
|
||||
### Batch Processing with Concurrency Control
|
||||
|
||||
```typescript
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
const limit = pLimit(5); // Max 5 concurrent operations
|
||||
const files = ['invoice1.xml', 'invoice2.xml', /* ... */];
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(file =>
|
||||
limit(async () => {
|
||||
const invoice = await EInvoice.fromFile(file);
|
||||
const validation = await invoice.validate();
|
||||
return { file, valid: validation.valid };
|
||||
})
|
||||
)
|
||||
);
|
||||
```text
|
||||
XML / PDF
|
||||
-> format detection
|
||||
-> decoder
|
||||
-> EInvoice model
|
||||
-> validation / editing
|
||||
-> encoder
|
||||
-> XML / PDF with embedded XML
|
||||
```
|
||||
|
||||
### REST API Integration
|
||||
This repo also exposes the lower-level pieces separately so you can plug into the pipeline at the stage you actually need.
|
||||
|
||||
```typescript
|
||||
app.post('/api/invoice/convert', async (req, res) => {
|
||||
try {
|
||||
const { xml, targetFormat } = req.body;
|
||||
const invoice = await EInvoice.fromXml(xml);
|
||||
|
||||
// Validate before conversion
|
||||
const validation = await invoice.validate();
|
||||
if (!validation.valid) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid invoice',
|
||||
violations: validation.errors
|
||||
});
|
||||
}
|
||||
|
||||
const converted = await invoice.exportXml(targetFormat);
|
||||
res.json({ success: true, xml: converted });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
## Good things to know before using it
|
||||
|
||||
- `embedInPdf()` attaches XML to an existing PDF buffer. It does not render invoice PDFs from scratch.
|
||||
- `saveToFile('something.pdf', ...)` only works when `invoice.pdf` already exists, for example after loading from a PDF first.
|
||||
- Validation coverage is useful and broad, but this README deliberately does not claim blanket certification or “100% compliance”.
|
||||
- Schematron integration and richer business-rule layers exist, but they depend on downloaded resources and are not the same as guaranteed certification against every profile.
|
||||
|
||||
## Example workflow
|
||||
|
||||
```ts
|
||||
import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';
|
||||
|
||||
const invoice = await EInvoice.fromFile('./incoming.pdf');
|
||||
|
||||
const validation = await invoice.validate(ValidationLevel.BUSINESS, {
|
||||
featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
|
||||
reportOnly: false,
|
||||
});
|
||||
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.errors.map((error) => error.message).join('\n'));
|
||||
}
|
||||
|
||||
const xmlForGermanB2G = await invoice.exportXml('xrechnung');
|
||||
```
|
||||
|
||||
## 🎯 What Makes Us Different
|
||||
## Module layout
|
||||
|
||||
### 🏆 100% EN16931 Compliance
|
||||
- All 162 Business Terms implemented
|
||||
- All 32 Business Groups structured
|
||||
- Complete semantic model with BT/BG validation
|
||||
- Official Schematron rules integrated
|
||||
|
||||
### 💎 Production Excellence
|
||||
- **500+ test cases** ensuring reliability
|
||||
- **Battle-tested** with real-world invoice corpus
|
||||
- **Memory efficient** - handles 1000+ line items
|
||||
- **Thread-safe** for concurrent processing
|
||||
|
||||
### 🚀 Developer Experience
|
||||
- **IntelliSense everywhere** - fully typed API
|
||||
- **Detailed error messages** with recovery hints
|
||||
- **Static factory methods** for intuitive usage
|
||||
- **Comprehensive documentation** with real examples
|
||||
|
||||
## 📦 Installation Requirements
|
||||
|
||||
- Node.js 18+ or modern browser
|
||||
- TypeScript 5.0+ (for TypeScript projects)
|
||||
- ~15MB installed size
|
||||
- Zero native dependencies
|
||||
|
||||
## 🤝 Standards Compliance
|
||||
|
||||
This library implements:
|
||||
- **EN 16931-1:2017** - Core invoice model
|
||||
- **CEN/TS 16931-3** - Syntax bindings
|
||||
- **ISO 4217** - Currency codes
|
||||
- **ISO 3166** - Country codes
|
||||
- **UN/ECE Rec 20** - Units of measure
|
||||
- **ISO 6523** - Organization identifiers
|
||||
- `ts/`: main published API and implementation
|
||||
- `ts_install/`: install-time resource bootstrap and download helpers
|
||||
- `assets_downloaded/`: downloaded validation resources
|
||||
- `test/`: corpus-heavy validation, format, PDF, performance, and security coverage
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Test Fixes Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the test fixes applied to make the einvoice library more spec compliant.
|
||||
|
||||
## Fixed Tests
|
||||
|
||||
### Encoding Tests (12 tests fixed)
|
||||
- **ENC-01**: UTF-8 Encoding ✅
|
||||
- Fixed invoice ID preservation by setting the `id` property
|
||||
- Fixed item description field handling in encoder
|
||||
- Fixed subject field extraction (uses first note as workaround)
|
||||
|
||||
- **ENC-02**: UTF-16 Encoding ✅
|
||||
- Fixed test syntax (removed `t.test` pattern)
|
||||
- Added `tap.start()` to run tests
|
||||
- UTF-16 not directly supported (acceptable), UTF-8 fallback works
|
||||
|
||||
- **ENC-03 to ENC-10**: Various encoding tests ✅
|
||||
- Fixed test syntax for all remaining encoding tests
|
||||
- All tests now verify UTF-8 fallback works correctly
|
||||
|
||||
### Error Handling Tests (6/10 fixed)
|
||||
- **ERR-01**: Parsing Recovery ✅
|
||||
- **ERR-03**: PDF Errors ✅
|
||||
- **ERR-04**: Network Errors ✅
|
||||
- **ERR-07**: Encoding Errors ✅
|
||||
- **ERR-08**: Filesystem Errors ✅
|
||||
- **ERR-09**: Transformation Errors ✅
|
||||
|
||||
Still failing (may not throw errors in these scenarios):
|
||||
- ERR-02: Validation Errors
|
||||
- ERR-05: Memory Errors
|
||||
- ERR-06: Concurrent Errors
|
||||
- ERR-10: Configuration Errors
|
||||
|
||||
### Format Detection Tests (3 failing)
|
||||
- FD-02, FD-03, FD-04: CII files detected as Factur-X
|
||||
- This is technically correct behavior (Factur-X is a CII profile)
|
||||
- Tests expect generic "CII" but library returns more specific format
|
||||
|
||||
## Library Fixes Applied
|
||||
|
||||
1. **UBL Encoder**: Modified to use item description field if available
|
||||
```typescript
|
||||
const description = (item as any).description || item.name;
|
||||
```
|
||||
|
||||
2. **XRechnung Decoder**: Modified to preserve subject from notes
|
||||
```typescript
|
||||
subject: notes.length > 0 ? notes[0] : `Invoice ${invoiceId}`,
|
||||
```
|
||||
|
||||
## Remaining Issues
|
||||
|
||||
### Medium Priority
|
||||
1. Subject field preservation - currently using notes as workaround
|
||||
2. "Due in X days" automatically added to notes
|
||||
|
||||
### Low Priority
|
||||
1. `&` character search in tests should look for `&`
|
||||
2. Remaining error-handling tests (validation, memory, concurrent, config)
|
||||
3. Format detection test expectations
|
||||
|
||||
## Spec Compliance Improvements
|
||||
|
||||
The library now better supports:
|
||||
- UTF-8 character encoding throughout
|
||||
- Preservation of invoice IDs in round-trip conversions
|
||||
- Better error handling and recovery
|
||||
- Multiple encoding format fallbacks
|
||||
- Item description fields in UBL format
|
||||
|
||||
## Test Results Summary
|
||||
|
||||
- **Encoding Tests**: 12/12 passing ✅
|
||||
- **Error Handling Tests**: 6/10 passing (4 may be invalid scenarios)
|
||||
- **Format Detection Tests**: 3 failing (but behavior is technically correct)
|
||||
|
||||
Total tests fixed: ~18 tests made to pass through library and test improvements.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { EInvoice } from '../../dist_ts/index.js';
|
||||
import * as plugins from '../../dist_ts/plugins.js';
|
||||
|
||||
const invoice = new EInvoice();
|
||||
invoice.pdf = undefined;
|
||||
invoice.pdfAttachments = undefined;
|
||||
|
||||
const parser = new plugins.DOMParser();
|
||||
const parserFromNamespace = new plugins.xmldom.DOMParser();
|
||||
|
||||
void invoice;
|
||||
void parser;
|
||||
void parserFromNamespace;
|
||||
void plugins.pako.inflate;
|
||||
void plugins.SaxonJS.compile;
|
||||
void plugins.SaxonJS.transform;
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": false,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { InvoiceFormat, ValidationLevel } from '../../../ts/interfaces/common.js';
|
||||
import { InvoiceFormat } from '../../../ts/interfaces/common.js';
|
||||
import { FormatDetector } from '../../../ts/formats/utils/format.detector.js';
|
||||
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||||
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
@@ -15,264 +15,40 @@ import * as path from 'path';
|
||||
*/
|
||||
|
||||
tap.test('CORP-05: FatturaPA Corpus Processing - should process Italian FatturaPA files', async () => {
|
||||
// Load FatturaPA test files
|
||||
const fatturapaFiles = await CorpusLoader.loadCategory('FATTURAPA_OFFICIAL');
|
||||
|
||||
// Handle case where no files are found
|
||||
|
||||
if (fatturapaFiles.length === 0) {
|
||||
console.log('⚠ No FatturaPA files found in corpus - skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Testing ${fatturapaFiles.length} FatturaPA files`);
|
||||
|
||||
const results = {
|
||||
total: fatturapaFiles.length,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
parseErrors: 0,
|
||||
validationErrors: 0,
|
||||
documentTypes: new Map<string, number>(),
|
||||
transmissionFormats: new Map<string, number>(),
|
||||
processingTimes: [] as number[]
|
||||
};
|
||||
|
||||
const failures: Array<{
|
||||
file: string;
|
||||
error: string;
|
||||
type: 'parse' | 'validation' | 'format';
|
||||
}> = [];
|
||||
|
||||
// Italian-specific validation patterns
|
||||
const italianValidations = {
|
||||
vatNumber: /^IT\d{11}$/,
|
||||
fiscalCode: /^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/,
|
||||
invoiceNumber: /^\w+\/\d{4}$/, // Common format: PREFIX/YEAR
|
||||
codiceDestinatario: /^[A-Z0-9]{6,7}$/,
|
||||
pecEmail: /^[a-zA-Z0-9._%+-]+@pec\.[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
};
|
||||
|
||||
|
||||
let detectedCount = 0;
|
||||
let unsupportedDecodeCount = 0;
|
||||
|
||||
for (const file of fatturapaFiles) {
|
||||
try {
|
||||
const xmlBuffer = await CorpusLoader.loadFile(file.path);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
// Track performance
|
||||
const { result: invoice, metric } = await PerformanceTracker.track(
|
||||
'fatturapa-processing',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// FatturaPA has specific XML structure
|
||||
if (xmlString.includes('FatturaElettronica')) {
|
||||
// Process as FatturaPA
|
||||
await einvoice.fromXmlString(xmlString);
|
||||
einvoice.metadata = {
|
||||
...einvoice.metadata,
|
||||
format: InvoiceFormat.FATTURAPA
|
||||
};
|
||||
} else {
|
||||
throw new Error('Not a valid FatturaPA file');
|
||||
}
|
||||
|
||||
return einvoice;
|
||||
},
|
||||
{ file: file.path, size: file.size }
|
||||
);
|
||||
|
||||
results.processingTimes.push(metric.duration);
|
||||
|
||||
// Extract FatturaPA specific information
|
||||
const formatMatch = xmlString.match(/<FormatoTrasmissione>([^<]+)<\/FormatoTrasmissione>/);
|
||||
const typeMatch = xmlString.match(/<TipoDocumento>([^<]+)<\/TipoDocumento>/);
|
||||
|
||||
if (formatMatch) {
|
||||
const format = formatMatch[1];
|
||||
results.transmissionFormats.set(format, (results.transmissionFormats.get(format) || 0) + 1);
|
||||
}
|
||||
|
||||
if (typeMatch) {
|
||||
const docType = typeMatch[1];
|
||||
results.documentTypes.set(docType, (results.documentTypes.get(docType) || 0) + 1);
|
||||
}
|
||||
|
||||
// Validate Italian-specific fields
|
||||
const vatMatch = xmlString.match(/<IdCodice>(\d{11})<\/IdCodice>/);
|
||||
const cfMatch = xmlString.match(/<CodiceFiscale>([A-Z0-9]{16})<\/CodiceFiscale>/);
|
||||
const destMatch = xmlString.match(/<CodiceDestinatario>([A-Z0-9]{6,7})<\/CodiceDestinatario>/);
|
||||
|
||||
let italianFieldsValid = true;
|
||||
|
||||
if (vatMatch && !italianValidations.vatNumber.test('IT' + vatMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid VAT number format: ${vatMatch[1]}`);
|
||||
}
|
||||
|
||||
if (cfMatch && !italianValidations.fiscalCode.test(cfMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid Codice Fiscale format: ${cfMatch[1]}`);
|
||||
}
|
||||
|
||||
if (destMatch && !italianValidations.codiceDestinatario.test(destMatch[1])) {
|
||||
italianFieldsValid = false;
|
||||
console.log(` - Invalid Codice Destinatario: ${destMatch[1]}`);
|
||||
}
|
||||
|
||||
// Validate the parsed invoice
|
||||
try {
|
||||
const validationResult = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
|
||||
if (validationResult.valid && italianFieldsValid) {
|
||||
results.successful++;
|
||||
console.log(`✓ ${path.basename(file.path)}: Successfully processed`);
|
||||
|
||||
// Log key information
|
||||
if (formatMatch) {
|
||||
console.log(` - Transmission format: ${formatMatch[1]}`);
|
||||
}
|
||||
if (typeMatch) {
|
||||
const docTypeMap: Record<string, string> = {
|
||||
'TD01': 'Fattura',
|
||||
'TD02': 'Acconto/Anticipo',
|
||||
'TD03': 'Acconto/Anticipo su parcella',
|
||||
'TD04': 'Nota di Credito',
|
||||
'TD05': 'Nota di Debito',
|
||||
'TD06': 'Parcella'
|
||||
};
|
||||
console.log(` - Document type: ${docTypeMap[typeMatch[1]] || typeMatch[1]}`);
|
||||
}
|
||||
} else {
|
||||
results.validationErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: validationResult.errors?.[0]?.message || 'Validation failed',
|
||||
type: 'validation'
|
||||
});
|
||||
}
|
||||
} catch (validationError: any) {
|
||||
results.validationErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: validationError.message,
|
||||
type: 'validation'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
results.failed++;
|
||||
|
||||
if (error.message.includes('Not a valid FatturaPA')) {
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: 'Invalid FatturaPA format',
|
||||
type: 'format'
|
||||
});
|
||||
} else {
|
||||
results.parseErrors++;
|
||||
failures.push({
|
||||
file: path.basename(file.path),
|
||||
error: error.message,
|
||||
type: 'parse'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✗ ${path.basename(file.path)}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary report
|
||||
console.log('\n=== FatturaPA Corpus Processing Summary ===');
|
||||
console.log(`Total files: ${results.total}`);
|
||||
console.log(`Successful: ${results.successful} (${(results.successful/results.total*100).toFixed(1)}%)`);
|
||||
console.log(`Failed: ${results.failed}`);
|
||||
console.log(` - Parse errors: ${results.parseErrors}`);
|
||||
console.log(` - Validation errors: ${results.validationErrors}`);
|
||||
|
||||
console.log('\nTransmission Formats:');
|
||||
results.transmissionFormats.forEach((count, format) => {
|
||||
const formatMap: Record<string, string> = {
|
||||
'FPA12': 'Pubblica Amministrazione',
|
||||
'FPR12': 'Privati',
|
||||
'SDI11': 'Sistema di Interscambio v1.1'
|
||||
};
|
||||
console.log(` - ${format}: ${formatMap[format] || format} (${count} files)`);
|
||||
});
|
||||
|
||||
console.log('\nDocument Types:');
|
||||
results.documentTypes.forEach((count, type) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'TD01': 'Fattura (Invoice)',
|
||||
'TD02': 'Acconto/Anticipo (Advance)',
|
||||
'TD03': 'Acconto/Anticipo su parcella',
|
||||
'TD04': 'Nota di Credito (Credit Note)',
|
||||
'TD05': 'Nota di Debito (Debit Note)',
|
||||
'TD06': 'Parcella (Fee Note)'
|
||||
};
|
||||
console.log(` - ${type}: ${typeMap[type] || type} (${count} files)`);
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('\nFailure Details:');
|
||||
failures.forEach(f => {
|
||||
console.log(` ${f.file} [${f.type}]: ${f.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance metrics
|
||||
if (results.processingTimes.length > 0) {
|
||||
const avgTime = results.processingTimes.reduce((a, b) => a + b, 0) / results.processingTimes.length;
|
||||
const minTime = Math.min(...results.processingTimes);
|
||||
const maxTime = Math.max(...results.processingTimes);
|
||||
|
||||
console.log('\nPerformance Metrics:');
|
||||
console.log(` Average processing time: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(` Min time: ${minTime.toFixed(2)}ms`);
|
||||
console.log(` Max time: ${maxTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// FatturaPA specific features validation
|
||||
if (results.successful > 0 && fatturapaFiles.length > 0) {
|
||||
// Test a sample file for specific features
|
||||
const sampleFile = fatturapaFiles[0];
|
||||
const xmlBuffer = await CorpusLoader.loadFile(sampleFile.path);
|
||||
const xmlBuffer = await CorpusLoader.loadFile(file.path);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
console.log('\nFatturaPA Structure Analysis:');
|
||||
|
||||
// Check for mandatory sections
|
||||
const mandatorySections = [
|
||||
'FatturaElettronicaHeader',
|
||||
'CedentePrestatore', // Seller
|
||||
'CessionarioCommittente', // Buyer
|
||||
'FatturaElettronicaBody',
|
||||
'DatiGenerali',
|
||||
'DatiBeniServizi'
|
||||
];
|
||||
|
||||
for (const section of mandatorySections) {
|
||||
if (xmlString.includes(section)) {
|
||||
console.log(`✓ Contains mandatory section: ${section}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for digital signature block
|
||||
if (xmlString.includes('<ds:Signature') || xmlString.includes('<Signature')) {
|
||||
console.log('✓ Contains digital signature block');
|
||||
const fileName = path.basename(file.path);
|
||||
|
||||
const format = FormatDetector.detectFormat(xmlString);
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
detectedCount++;
|
||||
|
||||
try {
|
||||
await EInvoice.fromXml(xmlString);
|
||||
expect(true).toBeFalse();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
expect(errorMessage.includes('FatturaPA decoder not yet implemented')).toBeTrue();
|
||||
unsupportedDecodeCount++;
|
||||
console.log(`✓ ${fileName}: Detection works and decode is explicitly unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all failures are due to unimplemented decoder
|
||||
const allNotImplemented = failures.every(f => f.error.includes('decoder not yet implemented'));
|
||||
|
||||
if (allNotImplemented && results.successful === 0) {
|
||||
console.log('\n⚠ FatturaPA decoder not yet implemented - test skipped');
|
||||
console.log(' This test will validate files once FatturaPA decoder is implemented');
|
||||
return; // Skip success criteria
|
||||
}
|
||||
|
||||
// Success criteria: at least 70% should pass (FatturaPA is complex)
|
||||
const successRate = results.successful / results.total;
|
||||
expect(successRate).toBeGreaterThan(0.7);
|
||||
|
||||
expect(detectedCount).toEqual(fatturapaFiles.length);
|
||||
expect(unsupportedDecodeCount).toEqual(fatturapaFiles.length);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||||
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||||
import { InvoiceFormat } from '../../../ts/interfaces/common.js';
|
||||
|
||||
tap.test('FD-09: FatturaPA Format Detection - should correctly identify Italian FatturaPA invoices', async () => {
|
||||
// Get FatturaPA test files from corpus
|
||||
@@ -18,8 +19,14 @@ tap.test('FD-09: FatturaPA Format Detection - should correctly identify Italian
|
||||
|
||||
// Import the format detector
|
||||
const { FormatDetector } = await import('../../../ts/formats/utils/format.detector.js');
|
||||
const sampledFiles = allFatturapaFiles.slice(0, 10);
|
||||
|
||||
for (const filePath of allFatturapaFiles.slice(0, 10)) { // Test first 10 for performance
|
||||
if (sampledFiles.length === 0) {
|
||||
console.log('No FatturaPA corpus files available for detection test');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const filePath of sampledFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
try {
|
||||
@@ -35,9 +42,7 @@ tap.test('FD-09: FatturaPA Format Detection - should correctly identify Italian
|
||||
{ file: fileName }
|
||||
);
|
||||
|
||||
// Verify it's detected as FatturaPA
|
||||
if (format.toString().toLowerCase().includes('fatturapa') ||
|
||||
format.toString().toLowerCase().includes('fattura')) {
|
||||
if (format === InvoiceFormat.FATTURAPA) {
|
||||
successCount++;
|
||||
console.log(`✓ ${fileName}: Correctly detected as FatturaPA`);
|
||||
} else {
|
||||
@@ -46,9 +51,9 @@ tap.test('FD-09: FatturaPA Format Detection - should correctly identify Italian
|
||||
file: fileName,
|
||||
error: `Detected as ${format} instead of FatturaPA`
|
||||
});
|
||||
console.log(`○ ${fileName}: Detected as ${format} (FatturaPA detection may need implementation)`);
|
||||
console.log(`✗ ${fileName}: Detected as ${format} instead of FatturaPA`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
failureCount++;
|
||||
failures.push({
|
||||
file: fileName,
|
||||
@@ -78,13 +83,8 @@ tap.test('FD-09: FatturaPA Format Detection - should correctly identify Italian
|
||||
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Note: FatturaPA detection may not be fully implemented yet
|
||||
if (successCount === 0 && allFatturapaFiles.length > 0) {
|
||||
console.log('Note: FatturaPA format detection may need implementation');
|
||||
}
|
||||
|
||||
// Expect at least some files to be processed without error
|
||||
expect(successCount + failureCount).toBeGreaterThan(0);
|
||||
expect(successCount).toEqual(sampledFiles.length);
|
||||
expect(failureCount).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('FD-09: FatturaPA Structure Detection - should detect FatturaPA by root element', async () => {
|
||||
@@ -129,20 +129,8 @@ tap.test('FD-09: FatturaPA Structure Detection - should detect FatturaPA by root
|
||||
);
|
||||
|
||||
console.log(`${test.name}: Detected as ${format}`);
|
||||
|
||||
// Should detect as FatturaPA (if implemented) or at least not as other formats
|
||||
const formatStr = format.toString().toLowerCase();
|
||||
const isNotOtherFormats = !formatStr.includes('ubl') &&
|
||||
!formatStr.includes('cii') &&
|
||||
!formatStr.includes('zugferd');
|
||||
|
||||
if (formatStr.includes('fattura')) {
|
||||
console.log(` ✓ Correctly identified as FatturaPA`);
|
||||
} else if (isNotOtherFormats) {
|
||||
console.log(` ○ Not detected as other formats (FatturaPA detection may need implementation)`);
|
||||
} else {
|
||||
console.log(` ✗ Incorrectly detected as other format`);
|
||||
}
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
console.log(' ✓ Correctly identified as FatturaPA');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -181,14 +169,8 @@ tap.test('FD-09: FatturaPA Version Detection - should detect different FatturaPA
|
||||
);
|
||||
|
||||
console.log(`FatturaPA ${test.version}: Detected as ${format}`);
|
||||
|
||||
// Should detect as FatturaPA regardless of version
|
||||
const formatStr = format.toString().toLowerCase();
|
||||
if (formatStr.includes('fattura')) {
|
||||
console.log(` ✓ Version ${test.version} correctly detected`);
|
||||
} else {
|
||||
console.log(` ○ Version detection may need implementation`);
|
||||
}
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
console.log(` ✓ Version ${test.version} correctly detected`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -229,16 +211,10 @@ tap.test('FD-09: FatturaPA vs Other Formats - should distinguish from other XML
|
||||
);
|
||||
|
||||
console.log(`${test.name}: Detected as ${format}`);
|
||||
|
||||
const formatStr = format.toString().toLowerCase();
|
||||
const hasExpectedFormat = formatStr.includes(test.expectedFormat);
|
||||
|
||||
if (hasExpectedFormat) {
|
||||
console.log(` ✓ Correctly distinguished ${test.name}`);
|
||||
} else {
|
||||
console.log(` ○ Format distinction may need refinement`);
|
||||
}
|
||||
|
||||
expect(format.toString().toLowerCase()).toContain(test.expectedFormat);
|
||||
console.log(` ✓ Correctly distinguished ${test.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
||||
|
||||
@@ -169,4 +169,4 @@ tap.test('Conformance Test Harness - full test suite', async (tools) => {
|
||||
}
|
||||
});
|
||||
|
||||
export default tap;
|
||||
export default tap.start();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../ts/einvoice.js';
|
||||
import { EInvoiceFormatError } from '../ts/errors.js';
|
||||
import { ValidationLevel } from '../ts/interfaces/common.js';
|
||||
import type { ExportFormat } from '../ts/interfaces/common.js';
|
||||
|
||||
@@ -217,5 +218,57 @@ tap.test('EInvoice should validate XML correctly', async () => {
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
tap.test('EInvoice should validate programmatic invoices without loaded XML', async () => {
|
||||
const emptyInvoice = new EInvoice();
|
||||
const emptyValidation = await emptyInvoice.validate();
|
||||
|
||||
expect(emptyValidation.valid).toBeFalse();
|
||||
expect(emptyValidation.errors.some(error => error.code === 'BR-01')).toBeTrue();
|
||||
expect(emptyValidation.errors.some(error => error.code === 'BR-16')).toBeTrue();
|
||||
|
||||
const validInvoice = new EInvoice();
|
||||
validInvoice.accountingDocId = 'TEST-IN-MEMORY';
|
||||
validInvoice.from.name = 'Programmatic Seller';
|
||||
validInvoice.from.address = {
|
||||
streetName: 'Seller Street',
|
||||
houseNumber: '1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10115',
|
||||
country: 'DE'
|
||||
};
|
||||
validInvoice.to.name = 'Programmatic Buyer';
|
||||
validInvoice.to.address = {
|
||||
streetName: 'Buyer Street',
|
||||
houseNumber: '2',
|
||||
city: 'Hamburg',
|
||||
postalCode: '20095',
|
||||
country: 'DE'
|
||||
};
|
||||
validInvoice.items.push({
|
||||
position: 1,
|
||||
name: 'Programmatic Product',
|
||||
articleNumber: 'PP-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
});
|
||||
|
||||
const validResult = await validInvoice.validate(ValidationLevel.BUSINESS);
|
||||
expect(validResult.valid).toBeTrue();
|
||||
expect(validResult.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
tap.test('EInvoice should reject unsupported invoice types explicitly', async () => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
try {
|
||||
einvoice.invoiceType = 'creditnote';
|
||||
expect(true).toBeFalse();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EInvoiceFormatError);
|
||||
}
|
||||
});
|
||||
|
||||
// Run the tests
|
||||
tap.start();
|
||||
|
||||
@@ -78,33 +78,35 @@ tap.test('Format Detection - PEPPOL large invoice samples', async () => {
|
||||
tap.test('Format Detection - FatturaPA Italian invoice format', async () => {
|
||||
const files = await TestFileHelpers.getTestFiles(TestFileCategories.FATTURAPA, '*.xml');
|
||||
console.log(`Testing ${files.length} FatturaPA files`);
|
||||
|
||||
|
||||
if (files.length === 0) {
|
||||
const syntheticFatturaPa = `<?xml version="1.0"?>
|
||||
<FatturaElettronica xmlns="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2">
|
||||
<FatturaElettronicaHeader />
|
||||
</FatturaElettronica>`;
|
||||
const format = FormatDetector.detectFormat(syntheticFatturaPa);
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
console.log('✓ Synthetic FatturaPA XML is detected correctly');
|
||||
return;
|
||||
}
|
||||
|
||||
let detectedCount = 0;
|
||||
for (const file of files) {
|
||||
try {
|
||||
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
const { result: format, duration } = await PerformanceUtils.measure(
|
||||
'fatturapa-detection',
|
||||
async () => FormatDetector.detectFormat(xmlString)
|
||||
);
|
||||
|
||||
// FatturaPA detection might not be fully implemented yet
|
||||
if (format === InvoiceFormat.FATTURAPA) {
|
||||
detectedCount++;
|
||||
}
|
||||
|
||||
console.log(`${format === InvoiceFormat.FATTURAPA ? '✓' : '○'} ${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
|
||||
} catch (error) {
|
||||
console.log(`✗ ${path.basename(file)}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log if FatturaPA detection needs implementation
|
||||
if (detectedCount === 0 && files.length > 0) {
|
||||
console.log('Note: FatturaPA format detection may need implementation');
|
||||
const xmlBuffer = await TestFileHelpers.loadTestFile(file);
|
||||
const xmlString = xmlBuffer.toString('utf-8');
|
||||
|
||||
const { result: format, duration } = await PerformanceUtils.measure(
|
||||
'fatturapa-detection',
|
||||
async () => FormatDetector.detectFormat(xmlString)
|
||||
);
|
||||
|
||||
expect(format).toEqual(InvoiceFormat.FATTURAPA);
|
||||
detectedCount++;
|
||||
|
||||
console.log(`✓ ${path.basename(file)}: ${format} (${duration.toFixed(2)}ms)`);
|
||||
}
|
||||
|
||||
expect(detectedCount).toEqual(files.length);
|
||||
});
|
||||
|
||||
// Test format detection for EN16931 examples
|
||||
@@ -263,4 +265,4 @@ tap.test('Format Detection - Confidence scoring', async () => {
|
||||
// expect(result.confidence).toBeGreaterThan(0.8);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
||||
|
||||
@@ -77,6 +77,17 @@ tap.test('Semantic Model - EInvoice to semantic model conversion', async () => {
|
||||
articleNumber: '',
|
||||
description: 'Professional consulting services'
|
||||
}];
|
||||
|
||||
invoice.paymentOptions = {
|
||||
description: 'Payment due within 30 days',
|
||||
sepaConnection: {
|
||||
iban: 'DE89370400440532013000',
|
||||
bic: 'COBADEFFXXX'
|
||||
},
|
||||
payPal: {
|
||||
email: 'billing@testseller.example'
|
||||
}
|
||||
};
|
||||
|
||||
const model = adapter.toSemanticModel(invoice);
|
||||
|
||||
@@ -99,6 +110,12 @@ tap.test('Semantic Model - EInvoice to semantic model conversion', async () => {
|
||||
expect(model.invoiceLines.length).toEqual(1);
|
||||
expect(model.invoiceLines[0].itemInformation.name).toEqual('Consulting Service');
|
||||
expect(model.invoiceLines[0].invoicedQuantity).toEqual(10);
|
||||
|
||||
// Verify payment instructions can be derived from paymentOptions
|
||||
expect(model.paymentInstructions.paymentMeansTypeCode).toEqual('30');
|
||||
expect(model.paymentInstructions.paymentMeansText).toEqual('Payment due within 30 days');
|
||||
expect(model.paymentInstructions.paymentAccountIdentifier).toEqual('DE89370400440532013000');
|
||||
expect(model.paymentInstructions.paymentServiceProviderIdentifier).toEqual('COBADEFFXXX');
|
||||
});
|
||||
|
||||
tap.test('Semantic Model - semantic model to EInvoice conversion', async () => {
|
||||
@@ -133,7 +150,8 @@ tap.test('Semantic Model - semantic model to EInvoice conversion', async () => {
|
||||
},
|
||||
paymentInstructions: {
|
||||
paymentMeansTypeCode: '30',
|
||||
paymentAccountIdentifier: 'US12345678901234567890'
|
||||
paymentAccountIdentifier: 'US12345678901234567890',
|
||||
paymentServiceProviderIdentifier: 'BANKUS33XXX'
|
||||
},
|
||||
documentTotals: {
|
||||
lineExtensionAmount: 1000,
|
||||
@@ -169,6 +187,14 @@ tap.test('Semantic Model - semantic model to EInvoice conversion', async () => {
|
||||
expect(invoice.to.name).toEqual('Canadian Buyer Ltd');
|
||||
expect(invoice.items.length).toEqual(1);
|
||||
expect(invoice.items[0].name).toEqual('Product A');
|
||||
expect(invoice.paymentOptions?.sepaConnection?.iban).toEqual('US12345678901234567890');
|
||||
expect(invoice.paymentOptions?.sepaConnection?.bic).toEqual('BANKUS33XXX');
|
||||
expect(invoice.metadata?.paymentMeansCode).toEqual('30');
|
||||
expect(invoice.metadata?.paymentAccount?.iban).toEqual('US12345678901234567890');
|
||||
|
||||
const roundTripModel = adapter.toSemanticModel(invoice);
|
||||
expect(roundTripModel.paymentInstructions.paymentAccountIdentifier).toEqual('US12345678901234567890');
|
||||
expect(roundTripModel.paymentInstructions.paymentServiceProviderIdentifier).toEqual('BANKUS33XXX');
|
||||
});
|
||||
|
||||
tap.test('Semantic Model - validation of mandatory business terms', async () => {
|
||||
@@ -657,4 +683,4 @@ tap.test('Semantic Model - complete semantic model validation', async () => {
|
||||
expect(errors.length).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
export default tap.start();
|
||||
|
||||
@@ -73,7 +73,7 @@ ${invoiceMatch[0]}`;
|
||||
console.log(` Errors: ${validation.errors.map(e => `${e.code}: ${e.message}`).join('; ')}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
results.failed++;
|
||||
results.errors.push(`${fileName}: ${error.message}`);
|
||||
console.log(`✗ ${fileName}: Error - ${error.message}`);
|
||||
@@ -128,7 +128,7 @@ ${invoiceMatch[0]}`;
|
||||
} else {
|
||||
console.log(`○ ${fileName}: Validation passed (may need stricter codelist checking)`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log(`✗ ${fileName}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ tap.test('Validation Suite - Syntax validation levels', async () => {
|
||||
console.log(` - ${err.code}: ${err.message}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error instanceof EInvoiceValidationError) {
|
||||
console.log('✓ Validation error caught correctly');
|
||||
console.log(error.getValidationReport());
|
||||
@@ -174,23 +174,30 @@ tap.test('Validation Suite - Syntax validation levels', async () => {
|
||||
tap.test('Validation Suite - Error reporting and recovery', async () => {
|
||||
const testInvoice = new EInvoice();
|
||||
|
||||
// Try to validate without loading XML
|
||||
try {
|
||||
await testInvoice.validate();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EInvoiceValidationError);
|
||||
if (error instanceof EInvoiceValidationError) {
|
||||
// The error might be "Cannot validate: format unknown" since no XML is loaded
|
||||
console.log('✓ Empty invoice validation error handled correctly');
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
const emptyValidation = await testInvoice.validate();
|
||||
expect(emptyValidation.valid).toBeFalse();
|
||||
expect(emptyValidation.errors.some(error => error.code === 'BR-01')).toBeTrue();
|
||||
console.log('✓ Empty invoice validation returns structured errors');
|
||||
|
||||
// Test with minimal valid invoice
|
||||
testInvoice.id = 'TEST-001';
|
||||
testInvoice.invoiceId = 'INV-001';
|
||||
testInvoice.from.name = 'Test Seller';
|
||||
testInvoice.from.address = {
|
||||
streetName: 'Seller Street',
|
||||
houseNumber: '1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10115',
|
||||
country: 'DE'
|
||||
};
|
||||
testInvoice.to.name = 'Test Buyer';
|
||||
testInvoice.to.address = {
|
||||
streetName: 'Buyer Street',
|
||||
houseNumber: '2',
|
||||
city: 'Paris',
|
||||
postalCode: '75001',
|
||||
country: 'FR'
|
||||
};
|
||||
testInvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Item',
|
||||
@@ -200,13 +207,10 @@ tap.test('Validation Suite - Error reporting and recovery', async () => {
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// This should fail because we don't have XML loaded
|
||||
try {
|
||||
await testInvoice.validate();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(EInvoiceValidationError);
|
||||
console.log('✓ Validation requires loaded XML');
|
||||
}
|
||||
const programmaticValidation = await testInvoice.validate();
|
||||
expect(programmaticValidation.valid).toBeTrue();
|
||||
expect(programmaticValidation.errors).toHaveLength(0);
|
||||
console.log('✓ Programmatic invoice validation works without loaded XML');
|
||||
});
|
||||
|
||||
// Test format-specific validation
|
||||
@@ -252,7 +256,7 @@ tap.test('Validation Suite - Format-specific validation rules', async () => {
|
||||
// Validate according to profile
|
||||
const validation = await einvoice.validate(ValidationLevel.SEMANTIC);
|
||||
console.log(` Validation: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log(`${plugins.path.basename(file)}: Skipped - ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -369,7 +373,7 @@ tap.test('Validation Suite - Calculation and sum validations', async () => {
|
||||
} else {
|
||||
console.log('✓ Invoice calculations validated successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log(`Calculation validation test skipped: ${error.message}`);
|
||||
}
|
||||
});
|
||||
@@ -418,4 +422,4 @@ tap.test('Validation Suite - Summary Report', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
tap.start();
|
||||
tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@fin.cx/einvoice',
|
||||
version: '5.1.1',
|
||||
version: '5.2.0',
|
||||
description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
|
||||
}
|
||||
|
||||
+148
-26
@@ -1,6 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import { business, finance } from './plugins.js';
|
||||
import type { business, finance } from '@tsclass/tsclass';
|
||||
import type { TInvoice, TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
||||
import { InvoiceFormat, ValidationLevel } from './interfaces/common.js';
|
||||
import type { ValidationResult, ValidationError, EInvoiceOptions, IPdf, ExportFormat } from './interfaces/common.js';
|
||||
@@ -28,6 +28,7 @@ import { PDFExtractor } from './formats/pdf/pdf.extractor.js';
|
||||
import { FormatDetector } from './formats/utils/format.detector.js';
|
||||
|
||||
// Import enhanced validators
|
||||
import { EN16931Validator } from './formats/validation/en16931.validator.js';
|
||||
import { EN16931BusinessRulesValidator } from './formats/validation/en16931.business-rules.validator.js';
|
||||
import { CodeListValidator } from './formats/validation/codelist.validator.js';
|
||||
import type { ValidationOptions } from './formats/validation/validation.types.js';
|
||||
@@ -41,6 +42,9 @@ import type { IEInvoiceMetadata } from './interfaces/en16931-metadata.js';
|
||||
* Extends the TInvoice interface for seamless integration with existing systems
|
||||
*/
|
||||
export class EInvoice implements TInvoice {
|
||||
private static sharedPdfEmbedder?: PDFEmbedder;
|
||||
private static sharedPdfExtractor?: PDFExtractor;
|
||||
|
||||
/**
|
||||
* Creates an EInvoice instance from XML string
|
||||
* @param xmlString XML string to parse
|
||||
@@ -109,8 +113,8 @@ export class EInvoice implements TInvoice {
|
||||
public incidenceId: string = '';
|
||||
public language: string = 'en';
|
||||
public objectActions: any[] = [];
|
||||
public pdf: IPdf | null = null;
|
||||
public pdfAttachments: IPdf[] | null = null;
|
||||
public pdf?: IPdf;
|
||||
public pdfAttachments?: IPdf[];
|
||||
public accentColor: string | null = null;
|
||||
public logoUrl: string | null = null;
|
||||
|
||||
@@ -149,7 +153,13 @@ export class EInvoice implements TInvoice {
|
||||
this.accountingDocType === 'creditnote' ? 'creditnote' : 'debitnote';
|
||||
}
|
||||
public set invoiceType(value: 'invoice' | 'creditnote' | 'debitnote') {
|
||||
this.accountingDocType = 'invoice'; // Always set to invoice for TInvoice type
|
||||
if (value !== 'invoice') {
|
||||
throw new EInvoiceFormatError(
|
||||
`Unsupported invoice type: ${value}`,
|
||||
{ unsupportedFeatures: [`invoiceType=${value}`] }
|
||||
);
|
||||
}
|
||||
this.accountingDocType = 'invoice';
|
||||
}
|
||||
|
||||
// Computed properties for convenience
|
||||
@@ -181,15 +191,28 @@ export class EInvoice implements TInvoice {
|
||||
|
||||
private xmlString: string = '';
|
||||
private detectedFormat: InvoiceFormat = InvoiceFormat.UNKNOWN;
|
||||
private parsedXmlDocument?: Document;
|
||||
private validationErrors: ValidationError[] = [];
|
||||
private validationCache = new Map<string, ValidationResult>();
|
||||
private options: EInvoiceOptions = {
|
||||
validateOnLoad: false,
|
||||
validationLevel: ValidationLevel.SYNTAX
|
||||
};
|
||||
|
||||
// PDF utilities
|
||||
private pdfEmbedder = new PDFEmbedder();
|
||||
private pdfExtractor = new PDFExtractor();
|
||||
// PDF utilities are created lazily because most invoice workflows never touch PDF I/O.
|
||||
private get pdfEmbedder(): PDFEmbedder {
|
||||
if (!EInvoice.sharedPdfEmbedder) {
|
||||
EInvoice.sharedPdfEmbedder = new PDFEmbedder();
|
||||
}
|
||||
return EInvoice.sharedPdfEmbedder;
|
||||
}
|
||||
|
||||
private get pdfExtractor(): PDFExtractor {
|
||||
if (!EInvoice.sharedPdfExtractor) {
|
||||
EInvoice.sharedPdfExtractor = new PDFExtractor();
|
||||
}
|
||||
return EInvoice.sharedPdfExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EInvoice instance
|
||||
@@ -260,6 +283,7 @@ export class EInvoice implements TInvoice {
|
||||
*/
|
||||
public async fromXmlString(xmlString: string): Promise<EInvoice> {
|
||||
try {
|
||||
this.validationCache.clear();
|
||||
this.xmlString = xmlString;
|
||||
|
||||
// Detect format
|
||||
@@ -269,8 +293,13 @@ export class EInvoice implements TInvoice {
|
||||
}
|
||||
|
||||
// Get appropriate decoder
|
||||
const decoder = DecoderFactory.createDecoder(xmlString, !this.options.validateOnLoad);
|
||||
const decoder = DecoderFactory.createDecoder(
|
||||
xmlString,
|
||||
!this.options.validateOnLoad,
|
||||
this.detectedFormat,
|
||||
);
|
||||
const invoice = await decoder.decode();
|
||||
this.parsedXmlDocument = decoder.getParsedDocument();
|
||||
|
||||
// Map the decoded invoice to our properties
|
||||
this.mapFromTInvoice(invoice);
|
||||
@@ -282,10 +311,11 @@ export class EInvoice implements TInvoice {
|
||||
|
||||
return this;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceParsingError(`Failed to parse XML: ${error.message}`, {}, error as Error);
|
||||
throw new EInvoiceParsingError(`Failed to parse XML: ${errorMessage}`, {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +337,8 @@ export class EInvoice implements TInvoice {
|
||||
const xmlString = fileBuffer.toString('utf-8');
|
||||
return this.fromXmlString(xmlString);
|
||||
} catch (error) {
|
||||
throw new EInvoiceError(`Failed to load file: ${error.message}`, 'FILE_LOAD_ERROR', { filePath });
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new EInvoiceError(`Failed to load file: ${errorMessage}`, 'FILE_LOAD_ERROR', { filePath });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,10 +374,11 @@ export class EInvoice implements TInvoice {
|
||||
|
||||
return this.fromXmlString(extractedXml);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${error.message}`, 'extract', {}, error as Error);
|
||||
throw new EInvoicePDFError(`Failed to extract invoice from PDF: ${errorMessage}`, 'extract', {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +455,8 @@ export class EInvoice implements TInvoice {
|
||||
|
||||
return await encoder.encode(invoice);
|
||||
} catch (error) {
|
||||
throw new EInvoiceFormatError(`Failed to encode to ${format}: ${error.message}`, { targetFormat: format });
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new EInvoiceFormatError(`Failed to encode to ${format}: ${errorMessage}`, { targetFormat: format });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,21 +467,34 @@ export class EInvoice implements TInvoice {
|
||||
*/
|
||||
public async validate(level: ValidationLevel = ValidationLevel.BUSINESS, options?: ValidationOptions): Promise<ValidationResult> {
|
||||
try {
|
||||
// For programmatically created invoices without XML, skip XML-based validation
|
||||
// For programmatically created invoices without XML, validate the in-memory invoice object.
|
||||
let result: ValidationResult;
|
||||
const cacheKey = this.getValidationCacheKey(level, options);
|
||||
|
||||
if (cacheKey) {
|
||||
const cached = this.validationCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return this.cloneValidationResult(cached);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xmlString && this.detectedFormat !== InvoiceFormat.UNKNOWN) {
|
||||
if (this.shouldUseFastBusinessValidation(level, options)) {
|
||||
result = this.validateDecodedInvoice(level);
|
||||
} else {
|
||||
// Use existing validator for XML-based validation
|
||||
const validator = ValidatorFactory.createValidator(this.xmlString);
|
||||
result = validator.validate(level);
|
||||
const validator = ValidatorFactory.createValidator(
|
||||
this.xmlString,
|
||||
this.detectedFormat,
|
||||
this.parsedXmlDocument,
|
||||
);
|
||||
result = validator.validate(level);
|
||||
|
||||
// Keep the raw XML, but drop the cached DOM once validation is done.
|
||||
this.parsedXmlDocument = undefined;
|
||||
}
|
||||
} else {
|
||||
// Create a basic result for programmatically created invoices
|
||||
result = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
level: level
|
||||
};
|
||||
result = this.validateDecodedInvoice(level);
|
||||
}
|
||||
|
||||
// Enhanced validation with feature flags
|
||||
@@ -489,16 +535,74 @@ export class EInvoice implements TInvoice {
|
||||
// Update validation status
|
||||
this.validationErrors = result.errors;
|
||||
result.valid = result.errors.length === 0 || options?.reportOnly === true;
|
||||
|
||||
if (cacheKey) {
|
||||
this.validationCache.set(cacheKey, this.cloneValidationResult(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceValidationError(`Validation failed: ${error.message}`, [], { validationLevel: level });
|
||||
throw new EInvoiceValidationError(`Validation failed: ${errorMessage}`, [], { validationLevel: level });
|
||||
}
|
||||
}
|
||||
|
||||
private validateDecodedInvoice(level: ValidationLevel): ValidationResult {
|
||||
const invoice = this.mapToTInvoice();
|
||||
const errors = EN16931Validator.collectMandatoryFieldErrors(invoice).map(message =>
|
||||
this.createValidationError(message)
|
||||
);
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings: level === ValidationLevel.SYNTAX
|
||||
? [{
|
||||
code: 'VAL-NO-XML',
|
||||
message: 'Syntax validation was skipped because no XML document has been loaded.'
|
||||
}]
|
||||
: [],
|
||||
level: level
|
||||
};
|
||||
}
|
||||
|
||||
private shouldUseFastBusinessValidation(level: ValidationLevel, options?: ValidationOptions): boolean {
|
||||
if (level !== ValidationLevel.BUSINESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options?.featureFlags?.length || options?.reportOnly || this.detectedFormat !== InvoiceFormat.UBL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For tiny plain-UBL documents without parties or lines, the decoded invoice model already
|
||||
// contains everything needed for the mandatory-field failures the XML validator would report.
|
||||
return this.items.length === 0 && !this.from?.name && !this.to?.name;
|
||||
}
|
||||
|
||||
private getValidationCacheKey(level: ValidationLevel, options?: ValidationOptions): string | undefined {
|
||||
if (!this.xmlString || options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (level !== ValidationLevel.SYNTAX && level !== ValidationLevel.SEMANTIC) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `xml:${level}`;
|
||||
}
|
||||
|
||||
private cloneValidationResult(result: ValidationResult): ValidationResult {
|
||||
return {
|
||||
...result,
|
||||
errors: result.errors.map(error => ({ ...error })),
|
||||
warnings: result.warnings?.map(warning => ({ ...warning }))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds the invoice XML into a PDF
|
||||
* @param pdfBuffer The PDF buffer to embed into
|
||||
@@ -514,7 +618,8 @@ export class EInvoice implements TInvoice {
|
||||
}
|
||||
return embedResult.data! as Buffer;
|
||||
} catch (error) {
|
||||
throw new EInvoicePDFError(`Failed to embed XML in PDF: ${error.message}`, 'embed', { format }, error as Error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new EInvoicePDFError(`Failed to embed XML in PDF: ${errorMessage}`, 'embed', { format }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,10 +651,11 @@ export class EInvoice implements TInvoice {
|
||||
await plugins.fs.writeFile(filePath, xmlString, 'utf-8');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (error instanceof EInvoiceError) {
|
||||
throw error;
|
||||
}
|
||||
throw new EInvoiceError(`Failed to save file: ${error.message}`, 'FILE_SAVE_ERROR', { filePath });
|
||||
throw new EInvoiceError(`Failed to save file: ${errorMessage}`, 'FILE_SAVE_ERROR', { filePath });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,4 +755,20 @@ export class EInvoice implements TInvoice {
|
||||
public addItem(item: Partial<TAccountingDocItem>): void {
|
||||
this.items.push(this.createItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
private createValidationError(message: string): ValidationError {
|
||||
const match = message.match(/^([A-Z]{2,}(?:-[A-Z0-9]+)*-\d+):\s*(.+)$/);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
code: match[1],
|
||||
message: match[2]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ export abstract class BaseDecoder {
|
||||
return this.xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a parsed XML document when a decoder keeps one around.
|
||||
*/
|
||||
public getParsedDocument(): Document | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CII date string based on format code
|
||||
* @param dateStr Date string
|
||||
|
||||
@@ -3,6 +3,14 @@ import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.
|
||||
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
||||
import { DOMParser, xpath } from '../../plugins.js';
|
||||
|
||||
const ciiParser = new DOMParser();
|
||||
const ciiNamespaces = {
|
||||
rsm: CII_NAMESPACES.RSM,
|
||||
ram: CII_NAMESPACES.RAM,
|
||||
udt: CII_NAMESPACES.UDT,
|
||||
};
|
||||
const ciiSelect = xpath.useNamespaces(ciiNamespaces);
|
||||
|
||||
/**
|
||||
* Base decoder for CII-based invoice formats
|
||||
*/
|
||||
@@ -16,22 +24,22 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
||||
super(xml, skipValidation);
|
||||
|
||||
// Parse XML document
|
||||
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
|
||||
this.doc = ciiParser.parseFromString(xml, 'application/xml');
|
||||
|
||||
// Set up namespaces for XPath queries
|
||||
this.namespaces = {
|
||||
rsm: CII_NAMESPACES.RSM,
|
||||
ram: CII_NAMESPACES.RAM,
|
||||
udt: CII_NAMESPACES.UDT
|
||||
};
|
||||
this.namespaces = ciiNamespaces;
|
||||
|
||||
// Create XPath selector with namespaces
|
||||
this.select = xpath.useNamespaces(this.namespaces);
|
||||
this.select = ciiSelect;
|
||||
|
||||
// Detect profile
|
||||
this.detectProfile();
|
||||
}
|
||||
|
||||
public override getParsedDocument(): Document {
|
||||
return this.doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes CII XML into a TInvoice object
|
||||
* @returns Promise resolving to a TInvoice object
|
||||
@@ -93,7 +101,8 @@ export abstract class CIIBaseDecoder extends BaseDecoder {
|
||||
* @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];
|
||||
const result = this.select(xpathExpr, context || this.doc);
|
||||
const node = Array.isArray(result) ? result[0] : null;
|
||||
return node ? (node.textContent || '') : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -4,31 +4,35 @@ import type { ValidationResult } from '../../interfaces/common.js';
|
||||
import { CII_NAMESPACES, CIIProfile } from './cii.types.js';
|
||||
import { DOMParser, xpath } from '../../plugins.js';
|
||||
|
||||
const ciiValidatorParser = new DOMParser();
|
||||
const ciiValidatorNamespaces = {
|
||||
rsm: CII_NAMESPACES.RSM,
|
||||
ram: CII_NAMESPACES.RAM,
|
||||
udt: CII_NAMESPACES.UDT,
|
||||
};
|
||||
const ciiValidatorSelect = xpath.useNamespaces(ciiValidatorNamespaces);
|
||||
|
||||
/**
|
||||
* 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 doc!: Document;
|
||||
protected namespaces!: Record<string, string>;
|
||||
protected select!: xpath.XPathSelect;
|
||||
protected profile: CIIProfile = CIIProfile.EN16931;
|
||||
|
||||
constructor(xml: string) {
|
||||
constructor(xml: string, doc?: Document) {
|
||||
super(xml);
|
||||
|
||||
try {
|
||||
// Parse XML document
|
||||
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
|
||||
// Reuse an existing parsed document when available.
|
||||
this.doc = doc ?? ciiValidatorParser.parseFromString(xml, 'application/xml');
|
||||
|
||||
// Set up namespaces for XPath queries
|
||||
this.namespaces = {
|
||||
rsm: CII_NAMESPACES.RSM,
|
||||
ram: CII_NAMESPACES.RAM,
|
||||
udt: CII_NAMESPACES.UDT
|
||||
};
|
||||
this.namespaces = ciiValidatorNamespaces;
|
||||
|
||||
// Create XPath selector with namespaces
|
||||
this.select = xpath.useNamespaces(this.namespaces);
|
||||
this.select = ciiValidatorSelect;
|
||||
|
||||
// Detect profile
|
||||
this.detectProfile();
|
||||
@@ -139,7 +143,8 @@ export abstract class CIIBaseValidator extends BaseValidator {
|
||||
* @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];
|
||||
const result = this.select(xpathExpr, context || this.doc);
|
||||
const node = Array.isArray(result) ? result[0] : null;
|
||||
return node ? (node.textContent || '') : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,16 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { EInvoice } from '../../einvoice.js';
|
||||
import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
|
||||
/**
|
||||
* Converter for XML formats to EInvoice - simplified version
|
||||
* This is a basic converter that extracts essential fields for testing
|
||||
*/
|
||||
export class XMLToEInvoiceConverter {
|
||||
private parser: DOMParser;
|
||||
private parser: InstanceType<typeof plugins.DOMParser>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new DOMParser();
|
||||
this.parser = new plugins.DOMParser();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +115,7 @@ export class XMLToEInvoiceConverter {
|
||||
console.warn('Error parsing XML:', error);
|
||||
}
|
||||
|
||||
return mockInvoice as EInvoice;
|
||||
return mockInvoice as unknown as EInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,4 +138,4 @@ export class XMLToEInvoiceConverter {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,15 @@ export class DecoderFactory {
|
||||
* Creates a decoder for the specified XML content
|
||||
* @param xml XML content to decode
|
||||
* @param skipValidation Whether to skip EN16931 validation
|
||||
* @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
|
||||
* @returns Appropriate decoder instance
|
||||
*/
|
||||
public static createDecoder(xml: string, skipValidation: boolean = false): BaseDecoder {
|
||||
const format = FormatDetector.detectFormat(xml);
|
||||
public static createDecoder(
|
||||
xml: string,
|
||||
skipValidation: boolean = false,
|
||||
formatHint?: InvoiceFormat,
|
||||
): BaseDecoder {
|
||||
const format = formatHint ?? FormatDetector.detectFormat(xml);
|
||||
|
||||
switch (format) {
|
||||
case InvoiceFormat.UBL:
|
||||
|
||||
@@ -59,28 +59,34 @@ export class ValidatorFactory {
|
||||
/**
|
||||
* Creates a validator for the specified XML content
|
||||
* @param xml XML content to validate
|
||||
* @param formatHint Optional pre-detected format to avoid re-detecting large XML inputs
|
||||
* @param parsedDocument Optional parsed XML document to avoid parsing twice
|
||||
* @returns Appropriate validator instance
|
||||
*/
|
||||
public static createValidator(xml: string): BaseValidator {
|
||||
public static createValidator(
|
||||
xml: string,
|
||||
formatHint?: InvoiceFormat,
|
||||
parsedDocument?: Document,
|
||||
): BaseValidator {
|
||||
try {
|
||||
const format = FormatDetector.detectFormat(xml);
|
||||
const format = formatHint ?? FormatDetector.detectFormat(xml);
|
||||
|
||||
switch (format) {
|
||||
case InvoiceFormat.UBL:
|
||||
return new EN16931UBLValidator(xml);
|
||||
return new EN16931UBLValidator(xml, parsedDocument);
|
||||
|
||||
case InvoiceFormat.XRECHNUNG:
|
||||
return new XRechnungValidator(xml);
|
||||
return new XRechnungValidator(xml, parsedDocument);
|
||||
|
||||
case InvoiceFormat.CII:
|
||||
// For now, use Factur-X validator for generic CII
|
||||
return new FacturXValidator(xml);
|
||||
return new FacturXValidator(xml, parsedDocument);
|
||||
|
||||
case InvoiceFormat.ZUGFERD:
|
||||
return new ZUGFeRDValidator(xml);
|
||||
return new ZUGFeRDValidator(xml, parsedDocument);
|
||||
|
||||
case InvoiceFormat.FACTURX:
|
||||
return new FacturXValidator(xml);
|
||||
return new FacturXValidator(xml, parsedDocument);
|
||||
|
||||
case InvoiceFormat.FATTURAPA:
|
||||
return new FatturaPAValidator(xml);
|
||||
@@ -131,4 +137,4 @@ class GenericValidator extends BaseValidator {
|
||||
protected validateBusinessRules(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,17 +169,45 @@ export class SemanticModelAdapter {
|
||||
}
|
||||
|
||||
// Set payment options
|
||||
if (model.paymentInstructions.paymentAccountIdentifier) {
|
||||
if (
|
||||
model.paymentInstructions.paymentAccountIdentifier ||
|
||||
model.paymentInstructions.paymentMeansText ||
|
||||
model.paymentInstructions.paymentServiceProviderIdentifier
|
||||
) {
|
||||
invoice.paymentOptions = {
|
||||
sepa: {
|
||||
iban: model.paymentInstructions.paymentAccountIdentifier,
|
||||
bic: model.paymentInstructions.paymentServiceProviderIdentifier
|
||||
description: model.paymentInstructions.paymentMeansText,
|
||||
sepaConnection: {
|
||||
iban: model.paymentInstructions.paymentAccountIdentifier || '',
|
||||
bic: model.paymentInstructions.paymentServiceProviderIdentifier || '',
|
||||
institution: model.paymentInstructions.paymentServiceProviderIdentifier
|
||||
},
|
||||
bankInfo: {
|
||||
accountHolder: model.paymentInstructions.paymentAccountName || '',
|
||||
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier || ''
|
||||
payPal: { email: '' }
|
||||
};
|
||||
|
||||
invoice.metadata = {
|
||||
...invoice.metadata,
|
||||
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
||||
paymentAccount: {
|
||||
iban: model.paymentInstructions.paymentAccountIdentifier,
|
||||
accountName: model.paymentInstructions.paymentAccountName,
|
||||
bankId: model.paymentInstructions.paymentServiceProviderIdentifier
|
||||
},
|
||||
extensions: {
|
||||
...invoice.metadata?.extensions,
|
||||
paymentMeans: {
|
||||
paymentMeansCode: model.paymentInstructions.paymentMeansTypeCode,
|
||||
paymentMeansText: model.paymentInstructions.paymentMeansText,
|
||||
remittanceInformation: model.paymentInstructions.remittanceInformation
|
||||
},
|
||||
paymentAccount: {
|
||||
iban: model.paymentInstructions.paymentAccountIdentifier,
|
||||
accountName: model.paymentInstructions.paymentAccountName,
|
||||
bankId: model.paymentInstructions.paymentServiceProviderIdentifier,
|
||||
bic: model.paymentInstructions.paymentServiceProviderIdentifier,
|
||||
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
};
|
||||
}
|
||||
|
||||
// Set extensions
|
||||
@@ -376,15 +404,21 @@ export class SemanticModelAdapter {
|
||||
*/
|
||||
private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
|
||||
const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
|
||||
const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
|
||||
const paymentAccount = invoice.metadata?.extensions?.paymentAccount || invoice.metadata?.paymentAccount;
|
||||
const sepaConnection = invoice.paymentOptions?.sepaConnection;
|
||||
|
||||
return {
|
||||
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30', // Default to credit transfer
|
||||
paymentMeansText: paymentMeans?.paymentMeansText,
|
||||
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || invoice.metadata?.paymentMeansCode || '30',
|
||||
paymentMeansText: paymentMeans?.paymentMeansText || invoice.paymentOptions?.description,
|
||||
remittanceInformation: paymentMeans?.remittanceInformation,
|
||||
paymentAccountIdentifier: paymentAccount?.iban,
|
||||
paymentAccountIdentifier: paymentAccount?.iban || sepaConnection?.iban,
|
||||
paymentAccountName: paymentAccount?.accountName,
|
||||
paymentServiceProviderIdentifier: paymentAccount?.bic || paymentAccount?.institutionName
|
||||
paymentServiceProviderIdentifier:
|
||||
paymentAccount?.bic ||
|
||||
paymentAccount?.institutionName ||
|
||||
paymentAccount?.bankId ||
|
||||
sepaConnection?.bic ||
|
||||
sepaConnection?.institution
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,10 +431,10 @@ export class SemanticModelAdapter {
|
||||
taxExclusiveAmount: invoice.totalNet,
|
||||
taxInclusiveAmount: invoice.totalGross,
|
||||
allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
|
||||
(sum, a) => sum + a.amount, 0
|
||||
(sum: number, a: { amount: number }) => sum + a.amount, 0
|
||||
),
|
||||
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
||||
(sum, c) => sum + c.amount, 0
|
||||
(sum: number, c: { amount: number }) => sum + c.amount, 0
|
||||
),
|
||||
prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
|
||||
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
||||
@@ -597,4 +631,4 @@ export class SemanticModelAdapter {
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
||||
}
|
||||
|
||||
// BR-08: An Invoice shall contain the Seller postal address (BG-5).
|
||||
const sellerAddress = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc)[0];
|
||||
const sellerAddressResult = this.select('//cac:AccountingSupplierParty//cac:PostalAddress', this.doc);
|
||||
const sellerAddress = Array.isArray(sellerAddressResult) ? sellerAddressResult[0] : null;
|
||||
if (!sellerAddress || !this.exists('.//cbc:IdentificationCode', sellerAddress)) {
|
||||
this.addError('BR-08', 'An Invoice shall contain the Seller postal address', '//cac:AccountingSupplierParty//cac:PostalAddress');
|
||||
valid = false;
|
||||
@@ -103,7 +104,8 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
||||
}
|
||||
|
||||
// BR-10: An Invoice shall contain the Buyer postal address (BG-8).
|
||||
const buyerAddress = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc)[0];
|
||||
const buyerAddressResult = this.select('//cac:AccountingCustomerParty//cac:PostalAddress', this.doc);
|
||||
const buyerAddress = Array.isArray(buyerAddressResult) ? buyerAddressResult[0] : null;
|
||||
if (!buyerAddress || !this.exists('.//cbc:IdentificationCode', buyerAddress)) {
|
||||
this.addError('BR-10', 'An Invoice shall contain the Buyer postal address', '//cac:AccountingCustomerParty//cac:PostalAddress');
|
||||
valid = false;
|
||||
@@ -213,4 +215,4 @@ export class EN16931UBLValidator extends UBLBaseValidator {
|
||||
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,11 @@ export class UBLEncoder extends UBLBaseEncoder {
|
||||
* @param invoice Invoice data
|
||||
*/
|
||||
private addPaymentMeans(doc: Document, parentElement: Element, invoice: TInvoice): void {
|
||||
const paymentOptions = invoice.paymentOptions;
|
||||
if (!paymentOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentMeansNode = doc.createElement('cac:PaymentMeans');
|
||||
parentElement.appendChild(paymentMeansNode);
|
||||
|
||||
@@ -276,26 +281,26 @@ export class UBLEncoder extends UBLBaseEncoder {
|
||||
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentDueDate', this.formatDate(dueDate.getTime()));
|
||||
|
||||
// Add payment channel code if available
|
||||
if (invoice.paymentOptions.description) {
|
||||
this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', invoice.paymentOptions.description);
|
||||
if (paymentOptions.description) {
|
||||
this.appendElement(doc, paymentMeansNode, 'cbc:InstructionNote', paymentOptions.description);
|
||||
}
|
||||
|
||||
// Add payment ID information if available - use invoice ID as payment reference
|
||||
this.appendElement(doc, paymentMeansNode, 'cbc:PaymentID', invoice.id);
|
||||
|
||||
// Add bank account information if available
|
||||
if (invoice.paymentOptions.sepaConnection && invoice.paymentOptions.sepaConnection.iban) {
|
||||
if (paymentOptions.sepaConnection && paymentOptions.sepaConnection.iban) {
|
||||
const payeeFinancialAccountNode = doc.createElement('cac:PayeeFinancialAccount');
|
||||
paymentMeansNode.appendChild(payeeFinancialAccountNode);
|
||||
|
||||
this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.iban);
|
||||
this.appendElement(doc, payeeFinancialAccountNode, 'cbc:ID', paymentOptions.sepaConnection.iban);
|
||||
|
||||
// Add financial institution information if BIC is available
|
||||
if (invoice.paymentOptions.sepaConnection.bic) {
|
||||
if (paymentOptions.sepaConnection.bic) {
|
||||
const financialInstitutionNode = doc.createElement('cac:FinancialInstitutionBranch');
|
||||
payeeFinancialAccountNode.appendChild(financialInstitutionNode);
|
||||
|
||||
this.appendElement(doc, financialInstitutionNode, 'cbc:ID', invoice.paymentOptions.sepaConnection.bic);
|
||||
this.appendElement(doc, financialInstitutionNode, 'cbc:ID', paymentOptions.sepaConnection.bic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1038,4 +1043,4 @@ export class UBLEncoder extends UBLBaseEncoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import type { TInvoice, TCreditNote, TDebitNote } from '../../interfaces/common.
|
||||
import { UBLDocumentType, UBL_NAMESPACES } from './ubl.types.js';
|
||||
import { DOMParser, xpath } from '../../plugins.js';
|
||||
|
||||
const ublParser = new DOMParser();
|
||||
const ublNamespaces = {
|
||||
cbc: UBL_NAMESPACES.CBC,
|
||||
cac: UBL_NAMESPACES.CAC,
|
||||
};
|
||||
const ublSelect = xpath.useNamespaces(ublNamespaces);
|
||||
|
||||
/**
|
||||
* Base decoder for UBL-based invoice formats
|
||||
*/
|
||||
@@ -15,16 +22,17 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
||||
super(xml, skipValidation);
|
||||
|
||||
// Parse XML document
|
||||
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
|
||||
this.doc = ublParser.parseFromString(xml, 'application/xml');
|
||||
|
||||
// Set up namespaces for XPath queries
|
||||
this.namespaces = {
|
||||
cbc: UBL_NAMESPACES.CBC,
|
||||
cac: UBL_NAMESPACES.CAC
|
||||
};
|
||||
// Set up namespaces for XPath queries
|
||||
this.namespaces = ublNamespaces;
|
||||
|
||||
// Create XPath selector with namespaces
|
||||
this.select = xpath.useNamespaces(this.namespaces);
|
||||
// Create XPath selector with namespaces
|
||||
this.select = ublSelect;
|
||||
}
|
||||
|
||||
public override getParsedDocument(): Document {
|
||||
return this.doc;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +83,8 @@ export abstract class UBLBaseDecoder extends BaseDecoder {
|
||||
* @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];
|
||||
const result = this.select(xpathExpr, context || this.doc);
|
||||
const node = Array.isArray(result) ? result[0] : null;
|
||||
return node ? (node.textContent || '') : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -4,29 +4,33 @@ import type { ValidationResult } from '../../interfaces/common.js';
|
||||
import { UBLDocumentType } from './ubl.types.js';
|
||||
import { DOMParser, xpath } from '../../plugins.js';
|
||||
|
||||
const ublValidatorParser = new DOMParser();
|
||||
const ublValidatorNamespaces = {
|
||||
cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
||||
cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
||||
};
|
||||
const ublValidatorSelect = xpath.useNamespaces(ublValidatorNamespaces);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
protected doc!: Document;
|
||||
protected namespaces!: Record<string, string>;
|
||||
protected select!: xpath.XPathSelect;
|
||||
|
||||
constructor(xml: string) {
|
||||
constructor(xml: string, doc?: Document) {
|
||||
super(xml);
|
||||
|
||||
try {
|
||||
// Parse XML document
|
||||
this.doc = new DOMParser().parseFromString(xml, 'application/xml');
|
||||
// Reuse an existing parsed document when available.
|
||||
this.doc = doc ?? ublValidatorParser.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'
|
||||
};
|
||||
this.namespaces = ublValidatorNamespaces;
|
||||
|
||||
// Create XPath selector with namespaces
|
||||
this.select = xpath.useNamespaces(this.namespaces);
|
||||
this.select = ublValidatorSelect;
|
||||
} catch (error) {
|
||||
this.addError('UBL-PARSE', `Failed to parse XML: ${error}`, '/');
|
||||
}
|
||||
@@ -101,7 +105,8 @@ export abstract class UBLBaseValidator extends BaseValidator {
|
||||
* @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];
|
||||
const result = this.select(xpathExpr, context || this.doc);
|
||||
const node = Array.isArray(result) ? result[0] : null;
|
||||
return node ? (node.textContent || '') : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,36 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
const seller = this.extractParty('//cac:AccountingSupplierParty/cac:Party');
|
||||
const buyer = this.extractParty('//cac:AccountingCustomerParty/cac:Party');
|
||||
|
||||
// Create the common invoice data with metadata for business references
|
||||
const businessReferences = {
|
||||
buyerReference,
|
||||
orderReference,
|
||||
contractReference,
|
||||
projectReference
|
||||
};
|
||||
|
||||
const paymentInformation = {
|
||||
paymentMeansCode,
|
||||
paymentID,
|
||||
paymentDueDate,
|
||||
iban,
|
||||
bic,
|
||||
bankName,
|
||||
accountName,
|
||||
paymentTermsNote,
|
||||
discountPercent
|
||||
};
|
||||
|
||||
const dateInformation = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
deliveryDate
|
||||
};
|
||||
|
||||
const hasBusinessReferences = Object.values(businessReferences).some(value => Boolean(value));
|
||||
const hasPaymentInformation = Object.values(paymentInformation).some(value => Boolean(value));
|
||||
const hasDateInformation = Object.values(dateInformation).some(value => Boolean(value));
|
||||
|
||||
// Create the common invoice data with metadata only when the source XML actually contains it.
|
||||
const invoiceData: any = {
|
||||
type: 'accounting-doc' as const,
|
||||
accountingDocType: 'invoice' as const,
|
||||
@@ -216,36 +245,20 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
reverseCharge: false,
|
||||
currency: currencyCode as finance.TCurrency,
|
||||
notes: notes,
|
||||
objectActions: [],
|
||||
metadata: {
|
||||
objectActions: []
|
||||
};
|
||||
|
||||
if (hasBusinessReferences || hasPaymentInformation || hasDateInformation) {
|
||||
invoiceData.metadata = {
|
||||
format: 'xrechnung' as any,
|
||||
version: '1.0.0',
|
||||
extensions: {
|
||||
businessReferences: {
|
||||
buyerReference,
|
||||
orderReference,
|
||||
contractReference,
|
||||
projectReference
|
||||
},
|
||||
paymentInformation: {
|
||||
paymentMeansCode,
|
||||
paymentID,
|
||||
paymentDueDate,
|
||||
iban,
|
||||
bic,
|
||||
bankName,
|
||||
accountName,
|
||||
paymentTermsNote,
|
||||
discountPercent
|
||||
},
|
||||
dateInformation: {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
deliveryDate
|
||||
}
|
||||
...(hasBusinessReferences ? { businessReferences } : {}),
|
||||
...(hasPaymentInformation ? { paymentInformation } : {}),
|
||||
...(hasDateInformation ? { dateInformation } : {}),
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Validate mandatory EN16931 fields unless validation is skipped
|
||||
if (!this.skipValidation) {
|
||||
@@ -255,7 +268,7 @@ export class XRechnungDecoder extends UBLBaseDecoder {
|
||||
return invoiceData;
|
||||
} catch (error) {
|
||||
// Re-throw validation errors
|
||||
if (error.message && error.message.includes('EN16931 validation failed')) {
|
||||
if (error instanceof Error && error.message.includes('EN16931 validation failed')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,41 +70,81 @@ export class FormatDetector {
|
||||
* @returns Detected format or UNKNOWN if more analysis is needed
|
||||
*/
|
||||
private static quickFormatCheck(xml: string): InvoiceFormat {
|
||||
const lowerXml = xml.toLowerCase();
|
||||
// Only scan a small prefix so large payloads do not create another full-size string copy.
|
||||
const sample = xml.slice(0, 65536);
|
||||
|
||||
// Root-element checks avoid a DOM parse for the common invoice formats.
|
||||
if (/<(?:[A-Za-z_][\w.-]*:)?(?:Invoice|CreditNote)\b/.test(sample)) {
|
||||
const customizationIdMatch = sample.match(
|
||||
/<[^>]*CustomizationID[^>]*>\s*([^<]+?)\s*<\/[^>]*CustomizationID>/i,
|
||||
);
|
||||
const customizationId = customizationIdMatch?.[1] ?? '';
|
||||
|
||||
if (/xrechnung/i.test(customizationId) || /urn:xoev-de:kosit:standard:xrechnung/i.test(customizationId)) {
|
||||
return InvoiceFormat.XRECHNUNG;
|
||||
}
|
||||
return InvoiceFormat.UBL;
|
||||
}
|
||||
|
||||
if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryInvoice\b/.test(sample)) {
|
||||
const guidelineIdMatch = sample.match(
|
||||
/<[^>]*GuidelineSpecifiedDocumentContextParameter[^>]*>[\s\S]*?<[^>]*ID[^>]*>\s*([^<]+?)\s*<\/[^>]*ID>/i,
|
||||
);
|
||||
const guidelineId = guidelineIdMatch?.[1] ?? '';
|
||||
|
||||
if (/xrechnung/i.test(guidelineId)) {
|
||||
return InvoiceFormat.XRECHNUNG;
|
||||
}
|
||||
if (/factur-x/i.test(guidelineId) || /urn:cen\.eu:en16931:2017/i.test(guidelineId)) {
|
||||
return InvoiceFormat.FACTURX;
|
||||
}
|
||||
if (/zugferd/i.test(guidelineId) || /urn:ferd:/i.test(guidelineId) || /urn:zugferd/i.test(guidelineId)) {
|
||||
return InvoiceFormat.ZUGFERD;
|
||||
}
|
||||
return InvoiceFormat.CII;
|
||||
}
|
||||
|
||||
if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryDocument\b/.test(sample)) {
|
||||
return InvoiceFormat.ZUGFERD;
|
||||
}
|
||||
|
||||
if (/<FatturaElettronica\b/.test(sample)) {
|
||||
return InvoiceFormat.FATTURAPA;
|
||||
}
|
||||
|
||||
// Check for obvious Factur-X indicators
|
||||
if (
|
||||
lowerXml.includes('factur-x.eu') ||
|
||||
lowerXml.includes('factur-x.xml') ||
|
||||
lowerXml.includes('factur-x:') ||
|
||||
lowerXml.includes('urn:cen.eu:en16931:2017') && lowerXml.includes('factur-x')
|
||||
/factur-x\.eu/i.test(sample) ||
|
||||
/factur-x\.xml/i.test(sample) ||
|
||||
/factur-x:/i.test(sample) ||
|
||||
(/urn:cen\.eu:en16931:2017/i.test(sample) && /factur-x/i.test(sample))
|
||||
) {
|
||||
return InvoiceFormat.FACTURX;
|
||||
}
|
||||
|
||||
// Check for obvious ZUGFeRD indicators
|
||||
if (
|
||||
lowerXml.includes('zugferd:') ||
|
||||
lowerXml.includes('zugferd-invoice.xml') ||
|
||||
lowerXml.includes('urn:ferd:') ||
|
||||
lowerXml.includes('urn:zugferd')
|
||||
/zugferd:/i.test(sample) ||
|
||||
/zugferd-invoice\.xml/i.test(sample) ||
|
||||
/urn:ferd:/i.test(sample) ||
|
||||
/urn:zugferd/i.test(sample)
|
||||
) {
|
||||
return InvoiceFormat.ZUGFERD;
|
||||
}
|
||||
|
||||
// Check for obvious XRechnung indicators
|
||||
if (
|
||||
lowerXml.includes('xrechnung') ||
|
||||
lowerXml.includes('urn:xoev-de:kosit:standard:xrechnung')
|
||||
/xrechnung/i.test(sample) ||
|
||||
/urn:xoev-de:kosit:standard:xrechnung/i.test(sample)
|
||||
) {
|
||||
return InvoiceFormat.XRECHNUNG;
|
||||
}
|
||||
|
||||
// Check for obvious FatturaPA indicators
|
||||
if (
|
||||
lowerXml.includes('fatturapa') ||
|
||||
lowerXml.includes('fattura elettronica') ||
|
||||
lowerXml.includes('fatturaelettronica')
|
||||
/fatturapa/i.test(sample) ||
|
||||
/fattura elettronica/i.test(sample) ||
|
||||
/fatturaelettronica/i.test(sample)
|
||||
) {
|
||||
return InvoiceFormat.FATTURAPA;
|
||||
}
|
||||
@@ -198,10 +238,8 @@ export class FormatDetector {
|
||||
* @returns True if it's a FatturaPA format
|
||||
*/
|
||||
private static isFatturaPAFormat(root: Element): boolean {
|
||||
return (
|
||||
root.nodeName === 'FatturaElettronica' ||
|
||||
(root.getAttribute('xmlns') && root.getAttribute('xmlns')!.includes('fatturapa.gov.it'))
|
||||
);
|
||||
const xmlns = root.getAttribute('xmlns') || '';
|
||||
return root.nodeName === 'FatturaElettronica' || xmlns.includes('fatturapa.gov.it');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,4 +341,4 @@ export class FormatDetector {
|
||||
// Generic CII if we can't determine more specifically
|
||||
return InvoiceFormat.CII;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@ export class ConformanceTestHarness {
|
||||
r.source === 'Schematron'
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron not available for ${sample.format}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Schematron not available for ${sample.format}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Aggregate results
|
||||
@@ -202,12 +203,13 @@ export class ConformanceTestHarness {
|
||||
result.passed = result.errors.length === 0 === sample.expectedValid;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error testing ${sample.name}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error testing ${sample.name}: ${errorMessage}`);
|
||||
result.errors.push({
|
||||
ruleId: 'TEST-ERROR',
|
||||
source: 'TestHarness',
|
||||
severity: 'error',
|
||||
message: `Test execution failed: ${error.message}`
|
||||
message: `Test execution failed: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -588,4 +590,4 @@ export async function runConformanceTests(
|
||||
// Generate HTML report
|
||||
await harness.generateHTMLReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ export class EN16931Validator {
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates that an invoice object contains all mandatory EN16931 fields
|
||||
* Collects mandatory EN16931 field errors without throwing.
|
||||
* @param invoice The invoice object to validate
|
||||
* @throws Error if mandatory fields are missing
|
||||
* @returns List of validation error messages
|
||||
*/
|
||||
public static validateMandatoryFields(invoice: any): void {
|
||||
public static collectMandatoryFieldErrors(invoice: any): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// BR-01: Invoice number is mandatory
|
||||
@@ -49,7 +49,7 @@ export class EN16931Validator {
|
||||
errors.push('BR-06: Seller name is mandatory');
|
||||
}
|
||||
|
||||
// BR-07: Buyer name is mandatory
|
||||
// BR-07: Buyer name is mandatory
|
||||
if (!invoice.to?.name) {
|
||||
errors.push('BR-07: Buyer name is mandatory');
|
||||
}
|
||||
@@ -67,11 +67,10 @@ export class EN16931Validator {
|
||||
// BR-05: Invoice currency code is mandatory
|
||||
if (!invoice.currency) {
|
||||
errors.push('BR-05: Invoice currency code is mandatory');
|
||||
} else {
|
||||
// Validate currency format
|
||||
if (!this.VALID_CURRENCIES.includes(invoice.currency.toUpperCase())) {
|
||||
errors.push(`Invalid currency code: ${invoice.currency}. Must be a valid ISO 4217 currency code`);
|
||||
}
|
||||
} else if (!this.VALID_CURRENCIES.includes(invoice.currency.toUpperCase())) {
|
||||
errors.push(
|
||||
`BR-05: Invalid currency code: ${invoice.currency}. Must be a valid ISO 4217 currency code`
|
||||
);
|
||||
}
|
||||
|
||||
// BR-08: Seller postal address is mandatory
|
||||
@@ -84,6 +83,17 @@ export class EN16931Validator {
|
||||
errors.push('BR-10: Buyer postal address (city, postal code, country) is mandatory');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an invoice object contains all mandatory EN16931 fields
|
||||
* @param invoice The invoice object to validate
|
||||
* @throws Error if mandatory fields are missing
|
||||
*/
|
||||
public static validateMandatoryFields(invoice: any): void {
|
||||
const errors = this.collectMandatoryFieldErrors(invoice);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`EN16931 validation failed:\n${errors.join('\n')}`);
|
||||
}
|
||||
@@ -132,4 +142,4 @@ export class EN16931Validator {
|
||||
throw new Error(`EN16931 XML validation failed:\n${errors.join('\n')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ export class MainValidator {
|
||||
this.schematronEnabled = true;
|
||||
console.log(`Schematron validation enabled for ${standard} ${format}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to initialize Schematron: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to initialize Schematron: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +122,8 @@ export class MainValidator {
|
||||
);
|
||||
results.push(...schematronResults);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron validation error: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Schematron validation error: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,4 +404,4 @@ export async function createValidator(
|
||||
}
|
||||
|
||||
// Export for convenience
|
||||
export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';
|
||||
export type { ValidationReport, ValidationResult, ValidationOptions } from './validation.types.js';
|
||||
|
||||
@@ -139,7 +139,8 @@ export class SchematronDownloader {
|
||||
console.log(`Successfully downloaded: ${fileName}`);
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to download ${source.name}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to download ${source.name}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +161,8 @@ export class SchematronDownloader {
|
||||
const path = await this.download(source);
|
||||
paths.push(path);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to download ${source.name}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to download ${source.name}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +211,8 @@ export class SchematronDownloader {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to list cached files: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to list cached files: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return files;
|
||||
@@ -230,7 +233,8 @@ export class SchematronDownloader {
|
||||
|
||||
console.log('Schematron cache cleared');
|
||||
} catch (error) {
|
||||
console.warn(`Failed to clear cache: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to clear cache: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,9 +300,10 @@ export async function downloadISOSkeletons(targetDir: string = 'assets_downloade
|
||||
|
||||
console.log(`Downloaded: ${name}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to download ${name}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to download ${name}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('ISO Schematron skeleton download complete');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,8 @@ export class IntegratedValidator {
|
||||
});
|
||||
results.push(...schematronResults);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron validation failed: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Schematron validation failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +225,8 @@ export class IntegratedValidator {
|
||||
try {
|
||||
await this.loadSchematron('EN16931', format);
|
||||
} catch (error) {
|
||||
console.warn(`Could not load Schematron: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Could not load Schematron: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +280,9 @@ export async function createStandardValidator(
|
||||
try {
|
||||
await validator.loadSchematron(standard, format);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron not available for ${standard} ${format}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Schematron not available for ${standard} ${format}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as SaxonJS from 'saxon-js';
|
||||
import type { ValidationResult } from './validation.types.js';
|
||||
|
||||
/**
|
||||
@@ -30,9 +29,7 @@ export class SchematronValidator {
|
||||
*/
|
||||
public async loadSchematron(source: string, isFilePath: boolean = true): Promise<void> {
|
||||
if (isFilePath) {
|
||||
// Load from file
|
||||
const smartfile = await import('@push.rocks/smartfile');
|
||||
this.schematronRules = await smartfile.SmartFile.fromFilePath(source).then(f => f.contentBuffer.toString());
|
||||
this.schematronRules = await plugins.fs.readFile(source, 'utf-8');
|
||||
} else {
|
||||
// Use provided string
|
||||
this.schematronRules = source;
|
||||
@@ -58,14 +55,15 @@ export class SchematronValidator {
|
||||
const xslt = this.generateXSLTFromSchematron(this.schematronRules);
|
||||
|
||||
// Compile the XSLT with Saxon-JS
|
||||
this.compiledStylesheet = await SaxonJS.compile({
|
||||
this.compiledStylesheet = await plugins.SaxonJS.compile({
|
||||
stylesheetText: xslt,
|
||||
warnings: 'silent'
|
||||
});
|
||||
|
||||
this.isCompiled = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to compile Schematron: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to compile Schematron: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +85,7 @@ export class SchematronValidator {
|
||||
|
||||
try {
|
||||
// Transform the XML with the compiled Schematron XSLT
|
||||
const transformResult = await SaxonJS.transform({
|
||||
const transformResult = await plugins.SaxonJS.transform({
|
||||
stylesheetInternal: this.compiledStylesheet,
|
||||
sourceText: xmlContent,
|
||||
destination: 'serialized',
|
||||
@@ -108,11 +106,12 @@ export class SchematronValidator {
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
results.push({
|
||||
ruleId: 'SCHEMATRON-ERROR',
|
||||
source: 'SCHEMATRON',
|
||||
severity: 'error',
|
||||
message: `Schematron validation failed: ${error.message}`,
|
||||
message: `Schematron validation failed: ${errorMessage}`,
|
||||
btReference: undefined,
|
||||
bgReference: undefined
|
||||
});
|
||||
@@ -323,7 +322,8 @@ export class HybridValidator {
|
||||
try {
|
||||
results.push(...validator.validate(xmlContent));
|
||||
} catch (error) {
|
||||
console.warn(`TS validator failed: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`TS validator failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,8 @@ export class HybridValidator {
|
||||
const schematronResults = await this.schematronValidator.validate(xmlContent, options);
|
||||
results.push(...schematronResults);
|
||||
} catch (error) {
|
||||
console.warn(`Schematron validation failed: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Schematron validation failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,4 +346,4 @@ export class HybridValidator {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -144,11 +144,12 @@ export function validateXml(
|
||||
const validator = ValidatorFactory.createValidator(xml);
|
||||
return validator.validate(level);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
valid: false,
|
||||
errors: [{
|
||||
code: 'VAL-ERROR',
|
||||
message: `Validation error: ${error.message}`
|
||||
message: `Validation error: ${errorMessage}`
|
||||
}],
|
||||
level
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { business, finance } from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Supported electronic invoice formats
|
||||
*/
|
||||
@@ -88,4 +86,4 @@ export type { TCreditNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
||||
export type { TDebitNote } from '@tsclass/tsclass/dist_ts/finance/index.js';
|
||||
export type { TContact } from '@tsclass/tsclass/dist_ts/business/index.js';
|
||||
export type { TLetterEnvelope } from '@tsclass/tsclass/dist_ts/business/index.js';
|
||||
export type { TDocumentEnvelope } from '@tsclass/tsclass/dist_ts/business/index.js';
|
||||
export type { TDocumentEnvelope } from '@tsclass/tsclass/dist_ts/business/index.js';
|
||||
|
||||
+3
-4
@@ -21,15 +21,14 @@ import {
|
||||
} from 'pdf-lib';
|
||||
|
||||
// XML-related imports
|
||||
import { DOMParser, XMLSerializer } from 'xmldom';
|
||||
import * as xmldom from 'xmldom';
|
||||
import { DOMParser, XMLSerializer, xmldom } from './vendor/xmldom.js';
|
||||
import * as xpath from 'xpath';
|
||||
|
||||
// XSLT/Schematron imports
|
||||
import * as SaxonJS from 'saxon-js';
|
||||
import { SaxonJS } from './vendor/saxonjs.js';
|
||||
|
||||
// Compression-related imports
|
||||
import * as pako from 'pako';
|
||||
import { pako } from './vendor/pako.js';
|
||||
|
||||
// Business model imports
|
||||
import { business, finance, general } from '@tsclass/tsclass';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# @fin.cx/einvoice
|
||||
|
||||
Source module for the main `@fin.cx/einvoice` package.
|
||||
|
||||
This directory contains the public TypeScript API for loading, validating, converting, and embedding European e-invoice XML.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## What this module exports
|
||||
|
||||
- `EInvoice`
|
||||
- `createEInvoice()`
|
||||
- `validateXml(xml, level?)`
|
||||
- `FormatDetector`
|
||||
- `PDFExtractor`, `PDFEmbedder`
|
||||
- `DecoderFactory`, `EncoderFactory`, `ValidatorFactory`
|
||||
- `BaseDecoder`, `BaseEncoder`, `BaseValidator`
|
||||
- `UBLBase*` and `CIIBase*` extension points
|
||||
- Factur-X and ZUGFeRD format-specific classes
|
||||
- root types such as `TInvoice`, `ValidationResult`, `ExportFormat`, `IPdf`
|
||||
|
||||
## Main workflow
|
||||
|
||||
```ts
|
||||
import { EInvoice, ValidationLevel } from '@fin.cx/einvoice';
|
||||
|
||||
const invoice = await EInvoice.fromFile('./invoice.xml');
|
||||
const validation = await invoice.validate(ValidationLevel.BUSINESS);
|
||||
const xml = await invoice.exportXml('facturx');
|
||||
```
|
||||
|
||||
## Format support
|
||||
|
||||
| Format | Detect | Import | Export | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ubl` | Yes | Yes | Yes | Generic UBL flow |
|
||||
| `xrechnung` | Yes | Yes | Yes | UBL-based profile |
|
||||
| `cii` | Yes | Yes | Yes | Generic export currently routes through the Factur-X encoder path |
|
||||
| `facturx` | Yes | Yes | Yes | Main CII generation path |
|
||||
| `zugferd` | Yes | Yes | Yes | Input supports v1 and v2+ |
|
||||
| `fatturapa` | Yes | No | No | Detection only at the moment |
|
||||
|
||||
## Important implementation notes
|
||||
|
||||
- `peppol` is not a root-level XML export target.
|
||||
- `FatturaPA` is not fully implemented for import/export.
|
||||
- PDF support means extracting or embedding XML into existing PDFs, not generating invoice PDFs from scratch.
|
||||
- Validation is useful and extensive, but should not be documented as blanket certification.
|
||||
|
||||
## Related directories
|
||||
|
||||
- `../readme.md`: full package README for end users
|
||||
- `../ts_install/readme.md`: install-time resource bootstrap module
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
declare module 'xmldom' {
|
||||
export class DOMParser {
|
||||
parseFromString(source: string, mimeType: string): Document;
|
||||
}
|
||||
|
||||
export class XMLSerializer {
|
||||
serializeToString(node: Node): string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'pako' {
|
||||
export function inflate(input: Uint8Array | ArrayBuffer | string, options?: unknown): Uint8Array;
|
||||
}
|
||||
|
||||
declare module 'saxon-js' {
|
||||
export function compile(options: {
|
||||
stylesheetText: string;
|
||||
warnings?: string;
|
||||
[key: string]: unknown;
|
||||
}): Promise<unknown>;
|
||||
|
||||
export function transform(options: {
|
||||
stylesheetInternal?: unknown;
|
||||
sourceText: string;
|
||||
destination?: string;
|
||||
stylesheetParams?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}): Promise<{
|
||||
principalResult: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import * as pakoRuntime from 'pako';
|
||||
|
||||
export interface IPakoModule {
|
||||
inflate(input: Uint8Array | ArrayBuffer | string, options?: unknown): Uint8Array;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const pako: IPakoModule = pakoRuntime as unknown as IPakoModule;
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import * as saxonJSRuntime from 'saxon-js';
|
||||
|
||||
export interface ISaxonJSCompileOptions {
|
||||
stylesheetText: string;
|
||||
warnings?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ISaxonJSTransformOptions {
|
||||
stylesheetInternal?: unknown;
|
||||
sourceText: string;
|
||||
destination?: string;
|
||||
stylesheetParams?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ISaxonJSTransformResult {
|
||||
principalResult: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ISaxonJSModule {
|
||||
compile(options: ISaxonJSCompileOptions): Promise<unknown>;
|
||||
transform(options: ISaxonJSTransformOptions): Promise<ISaxonJSTransformResult>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const SaxonJS: ISaxonJSModule = saxonJSRuntime as unknown as ISaxonJSModule;
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import * as xmldomRuntime from 'xmldom';
|
||||
|
||||
export interface IDOMParser {
|
||||
parseFromString(source: string, mimeType: string): Document;
|
||||
}
|
||||
|
||||
export interface IDOMParserConstructor {
|
||||
new (): IDOMParser;
|
||||
}
|
||||
|
||||
export interface IXMLSerializer {
|
||||
serializeToString(node: Node): string;
|
||||
}
|
||||
|
||||
export interface IXMLSerializerConstructor {
|
||||
new (): IXMLSerializer;
|
||||
}
|
||||
|
||||
export interface IXmlDomModule {
|
||||
DOMParser: IDOMParserConstructor;
|
||||
XMLSerializer: IXMLSerializerConstructor;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const DOMParser: IDOMParserConstructor = xmldomRuntime.DOMParser as unknown as IDOMParserConstructor;
|
||||
export const XMLSerializer: IXMLSerializerConstructor = xmldomRuntime.XMLSerializer as unknown as IXMLSerializerConstructor;
|
||||
export const xmldom: IXmlDomModule = xmldomRuntime as unknown as IXmlDomModule;
|
||||
@@ -1,49 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
/// <reference types="node" />
|
||||
|
||||
/**
|
||||
* Script to download official Schematron files for e-invoice validation
|
||||
*/
|
||||
|
||||
import { SchematronDownloader } from '../dist_ts/formats/validation/schematron.downloader.js';
|
||||
const schematronDownloaderModulePath = '../dist_ts/formats/validation/schematron.downloader.js';
|
||||
|
||||
async function createDownloader() {
|
||||
const { SchematronDownloader } = await import(schematronDownloaderModulePath);
|
||||
const downloader = new SchematronDownloader('assets_downloaded/schematron');
|
||||
await downloader.initialize();
|
||||
return downloader;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('📥 Starting Schematron download...\n');
|
||||
|
||||
const downloader = new SchematronDownloader('assets_downloaded/schematron');
|
||||
await downloader.initialize();
|
||||
const downloader = await createDownloader();
|
||||
|
||||
// Download EN16931 Schematron files
|
||||
console.log('🔵 Downloading EN16931 Schematron files...');
|
||||
try {
|
||||
const en16931Paths = await downloader.downloadStandard('EN16931');
|
||||
console.log(`✅ Downloaded ${en16931Paths.length} EN16931 files`);
|
||||
en16931Paths.forEach(p => console.log(` - ${p}`));
|
||||
en16931Paths.forEach((p: string) => console.log(` - ${p}`));
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to download EN16931: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`❌ Failed to download EN16931: ${errorMessage}`);
|
||||
}
|
||||
|
||||
console.log('\n🔵 Downloading PEPPOL Schematron files...');
|
||||
try {
|
||||
const peppolPaths = await downloader.downloadStandard('PEPPOL');
|
||||
console.log(`✅ Downloaded ${peppolPaths.length} PEPPOL files`);
|
||||
peppolPaths.forEach(p => console.log(` - ${p}`));
|
||||
peppolPaths.forEach((p: string) => console.log(` - ${p}`));
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to download PEPPOL: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`❌ Failed to download PEPPOL: ${errorMessage}`);
|
||||
}
|
||||
|
||||
console.log('\n🔵 Downloading XRechnung Schematron files...');
|
||||
try {
|
||||
const xrechnungPaths = await downloader.downloadStandard('XRECHNUNG');
|
||||
console.log(`✅ Downloaded ${xrechnungPaths.length} XRechnung files`);
|
||||
xrechnungPaths.forEach(p => console.log(` - ${p}`));
|
||||
xrechnungPaths.forEach((p: string) => console.log(` - ${p}`));
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to download XRechnung: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`❌ Failed to download XRechnung: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// List cached files
|
||||
console.log('\n📂 Cached Schematron files:');
|
||||
const cached = await downloader.getCachedFiles();
|
||||
cached.forEach(file => {
|
||||
cached.forEach((file: { path: string; metadata: any }) => {
|
||||
if (file.metadata) {
|
||||
console.log(` - ${file.path}`);
|
||||
console.log(` Version: ${file.metadata.version}`);
|
||||
@@ -65,4 +75,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
});
|
||||
}
|
||||
|
||||
export default main;
|
||||
export default main;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
/// <reference types="node" />
|
||||
|
||||
/**
|
||||
* Download official EN16931 and PEPPOL test samples for conformance testing
|
||||
@@ -142,7 +143,8 @@ async function downloadTestSamples(source: TestSampleSource): Promise<void> {
|
||||
try {
|
||||
await downloadFile(source.repository, source.branch, filePath, targetPath);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error downloading ${fileName}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(` ❌ Error downloading ${fileName}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,4 +205,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
}
|
||||
|
||||
export default main;
|
||||
export { TEST_SAMPLE_SOURCES };
|
||||
export { TEST_SAMPLE_SOURCES };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env tsx
|
||||
/// <reference types="node" />
|
||||
/**
|
||||
* Downloads official XRechnung Schematron validation rules
|
||||
* from the KoSIT repositories
|
||||
@@ -179,4 +180,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
});
|
||||
}
|
||||
|
||||
export default downloadXRechnungRules;
|
||||
export default downloadXRechnungRules;
|
||||
|
||||
+34
-18
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
/// <reference types="node" />
|
||||
|
||||
/**
|
||||
* Post-install script to download required validation resources
|
||||
@@ -6,11 +7,19 @@
|
||||
* All users need validation capabilities, so this is mandatory
|
||||
*/
|
||||
|
||||
import { SchematronDownloader } from '../dist_ts/formats/validation/schematron.downloader.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const schematronDownloaderModulePath = '../dist_ts/formats/validation/schematron.downloader.js';
|
||||
|
||||
async function createDownloader() {
|
||||
const { SchematronDownloader } = await import(schematronDownloaderModulePath);
|
||||
const downloader = new SchematronDownloader('assets_downloaded/schematron');
|
||||
await downloader.initialize();
|
||||
return downloader;
|
||||
}
|
||||
|
||||
// Version for cache invalidation
|
||||
const RESOURCES_VERSION = '1.0.0';
|
||||
|
||||
@@ -89,15 +98,15 @@ function saveVersionFile(): void {
|
||||
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
||||
fs.writeFileSync(versionFile, RESOURCES_VERSION);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not save version file:', error.message);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn('⚠️ Could not save version file:', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSchematron() {
|
||||
console.log('📥 Downloading Schematron validation files...\n');
|
||||
|
||||
const downloader = new SchematronDownloader('assets_downloaded/schematron');
|
||||
await downloader.initialize();
|
||||
const downloader = await createDownloader();
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
@@ -109,7 +118,8 @@ async function downloadSchematron() {
|
||||
console.log(`✅ Downloaded ${en16931Files.length} EN16931 files`);
|
||||
successCount += en16931Files.length;
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Failed to download EN16931: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`⚠️ Failed to download EN16931: ${errorMessage}`);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
@@ -120,7 +130,8 @@ async function downloadSchematron() {
|
||||
console.log(`✅ Downloaded ${peppolFiles.length} PEPPOL files`);
|
||||
successCount += peppolFiles.length;
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Failed to download PEPPOL: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`⚠️ Failed to download PEPPOL: ${errorMessage}`);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
@@ -131,7 +142,8 @@ async function downloadSchematron() {
|
||||
console.log(`✅ Downloaded ${xrechnungFiles.length} XRechnung files`);
|
||||
successCount += xrechnungFiles.length;
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Failed to download XRechnung: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`⚠️ Failed to download XRechnung: ${errorMessage}`);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
@@ -232,13 +244,14 @@ async function main() {
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempts < maxAttempts) {
|
||||
console.log(`\n⚠️ Download failed: ${error.message}`);
|
||||
console.log(' Retrying...');
|
||||
await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s before retry
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempts < maxAttempts) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.log(`\n⚠️ Download failed: ${errorMessage}`);
|
||||
console.log(' Retrying...');
|
||||
await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s before retry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,13 +263,15 @@ async function main() {
|
||||
console.error();
|
||||
|
||||
if (lastError) {
|
||||
console.error(' Last error:', lastError.message);
|
||||
const errorMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
||||
console.error(' Last error:', errorMessage);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Catch-all for unexpected errors
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error();
|
||||
console.error('⚠️ Unexpected error during resource setup:', error.message);
|
||||
console.error('⚠️ Unexpected error during resource setup:', errorMessage);
|
||||
console.error(' This won\'t affect library installation');
|
||||
console.error(' Resources will be downloaded on first use');
|
||||
console.error();
|
||||
@@ -266,10 +281,11 @@ async function main() {
|
||||
// Only run if this is the main module
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch(error => {
|
||||
console.error('⚠️ Resource setup error:', error.message);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('⚠️ Resource setup error:', errorMessage);
|
||||
// Never fail the install
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
export default main;
|
||||
export default main;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# ts_install 🧰
|
||||
|
||||
Install-time bootstrap module for `@fin.cx/einvoice`.
|
||||
|
||||
This directory contains the scripts that download optional validation resources and external test data used by the main library.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## What it does
|
||||
|
||||
- runs the package `postinstall` bootstrap
|
||||
- checks whether downloaded Schematron resources are already present and current
|
||||
- skips cleanly when offline or when install context is not valid
|
||||
- downloads validation resources into `assets_downloaded/schematron`
|
||||
- exposes manual scripts for maintainers and advanced users
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
- it only runs automatically when `npm_lifecycle_event === 'postinstall'`
|
||||
- it never fails package installation on download/setup problems
|
||||
- it skips when `dist_ts/` is missing
|
||||
- it skips in CI when `EINVOICE_SKIP_RESOURCES` is set
|
||||
- it keeps a `.version` marker to avoid unnecessary re-downloads
|
||||
|
||||
## Manual commands
|
||||
|
||||
```bash
|
||||
pnpm download-schematron
|
||||
pnpm download-test-samples
|
||||
```
|
||||
|
||||
## Output locations
|
||||
|
||||
- `assets_downloaded/schematron/`: downloaded validation rules
|
||||
- `test-samples/`: downloaded external sample corpus for conformance tests
|
||||
|
||||
## Who needs this?
|
||||
|
||||
- library consumers who want richer validation resources available locally
|
||||
- maintainers working on Schematron validation or conformance testing
|
||||
- CI or packaging flows that want explicit control over resource setup
|
||||
|
||||
For normal basic XML/PDF parsing and conversion, the main package API remains the primary entrypoint.
|
||||
+2
-3
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user