feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped

This commit is contained in:
2025-08-12 12:37:01 +00:00
parent 08d7803be2
commit 73b46f7857
19 changed files with 6211 additions and 20 deletions

View File

@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2025-01-09
### Added
- **E-Invoice Integration**: Full XRechnung/ZUGFeRD support with import/export capabilities
- **Invoice Processing**: Automatic booking of electronic invoices to accounting
- **Advanced Export Features**: Comprehensive export functionality for accounts, balances, and ledger data
- **PDF Generation**: Professional PDF report generation with customizable templates
- **Security Features**: Merkle tree audit trails and digital signature support for tamper-proof records
- **Invoice Storage**: Dedicated invoice persistence layer with search and filtering
- **Invoice Adapter**: Bidirectional conversion between e-invoice formats and internal data model
- **Invoice Booking Engine**: Intelligent automatic account detection and VAT splitting
- **Cryptographic Signatures**: Support for signing exports with private keys and certificates
- **Structured Export Formats**: Export data in multiple formats (JSON, CSV, PDF)
- **Jahresabschluss Export**: Complete annual closing package generation
- New dependencies: @e-invoice-eu/core, @fin.cx/einvoice, merkletreejs, node-forge
- Enhanced documentation with invoice and export examples
### Changed
- Updated README with comprehensive documentation of new features
- Expanded API reference with new invoice and export methods
## [1.1.0] - 2025-01-09
### Added

View File

@@ -1,6 +1,6 @@
{
"name": "@fin.cx/skr",
"version": "1.1.0",
"version": "1.2.0",
"description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
@@ -25,16 +25,25 @@
"license": "MIT",
"packageManager": "pnpm@10.11.0",
"dependencies": {
"@e-invoice-eu/core": "^2.1.9",
"@fin.cx/einvoice": "5.1.4",
"@push.rocks/smartdata": "^5.15.1",
"@push.rocks/smartfile": "^11.0.22",
"@push.rocks/smarthash": "^3.0.7",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpdf": "^3.1.5",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9"
"@push.rocks/smartunique": "^3.0.9",
"merkletreejs": "^0.4.0",
"node-forge": "^1.3.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.2",
"@push.rocks/qenv": "^6.1.0"
"@push.rocks/qenv": "^6.1.0",
"@types/node-forge": "^1.3.11"
},
"repository": {
"type": "git",

469
pnpm-lock.yaml generated
View File

@@ -8,18 +8,42 @@ importers:
.:
dependencies:
'@e-invoice-eu/core':
specifier: ^2.1.9
version: 2.1.9
'@fin.cx/einvoice':
specifier: 5.1.4
version: 5.1.4
'@push.rocks/smartdata':
specifier: ^5.15.1
version: 5.15.1(@aws-sdk/credential-providers@3.864.0)(socks@2.8.6)
'@push.rocks/smartfile':
specifier: ^11.0.22
version: 11.2.5
'@push.rocks/smarthash':
specifier: ^3.0.7
version: 3.2.3
'@push.rocks/smartlog':
specifier: ^3.1.8
version: 3.1.8
'@push.rocks/smartpath':
specifier: ^5.0.18
version: 5.1.0
'@push.rocks/smartpdf':
specifier: ^3.1.5
version: 3.3.0(typescript@5.8.3)
'@push.rocks/smarttime':
specifier: ^4.1.1
version: 4.1.1
'@push.rocks/smartunique':
specifier: ^3.0.9
version: 3.0.9
merkletreejs:
specifier: ^0.4.0
version: 0.4.1
node-forge:
specifier: ^1.3.1
version: 1.3.1
devDependencies:
'@git.zone/tsbuild':
specifier: ^2.6.4
@@ -33,6 +57,9 @@ importers:
'@push.rocks/qenv':
specifier: ^6.1.0
version: 6.1.0
'@types/node-forge':
specifier: ^1.3.11
version: 1.3.13
packages:
@@ -51,6 +78,9 @@ packages:
'@api.global/typedsocket@3.0.1':
resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'}
@@ -226,6 +256,9 @@ packages:
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@cantoo/pdf-lib@2.4.2':
resolution: {integrity: sha512-ZqMiY8XEyM6Rc3WjpsQnrZYwCdyf/Emg2J3RbmSxoIKN1Kpa/93uIaO9cx/14dJoC6vkcAtMhrYsO7YLB8i8Lg==}
'@cloudflare/workers-types@4.20250809.0':
resolution: {integrity: sha512-MAG8S0aL+/WT52/XaqXCb6qc7XyfuYqmhwnpyLsx+RUkMZJxkhpJqaOny3Wa4IlWQfpgtQXktogmEoxxCpUmfA==}
@@ -236,6 +269,34 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@csstools/color-helpers@5.0.2':
resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.0.10':
resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
@@ -248,6 +309,14 @@ packages:
'@design.estate/dees-element@2.1.2':
resolution: {integrity: sha512-ZiwvE411RJPHaYio26asQLnSmtJ6G1HRLYWbxW/HvCMbFtrcrXysP1y4PQ9KjdNfiQ4yoWPjTtwYMJjLE0NcbA==}
'@e-invoice-eu/core@2.1.9':
resolution: {integrity: sha512-chXUSUpN7FjHxdDa546bkLhkpVZCJEzpluueYaftFVqYWrZa6/G8MDvMslezEIvtsdxqHun1Se6aR+98YKOTQw==}
'@e965/xlsx@0.20.3':
resolution: {integrity: sha512-703RN/3OdsRD5mtse2HBX7Um7xwaP9tlswEG6svOtjqokXoX7rJdQj7DyabD2I+xk22RgaIIU+R6BHgkpZGB/w==}
engines: {node: '>=0.8'}
hasBin: true
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
@@ -413,6 +482,12 @@ packages:
cpu: [x64]
os: [win32]
'@esgettext/runtime@1.3.6':
resolution: {integrity: sha512-4vMSr6i4WiQrrP8XXMxtQoDvkbRgVqmivta9j1YDPBzRaRIGTN2Nu+ADu8cv2WfKk5GkVUUqDpCxfbXOKCN+Cg==}
'@fin.cx/einvoice@5.1.4':
resolution: {integrity: sha512-pCtqIBsFB9wB4s4mb8zTg29JEL/H8cc2rl4pjLeRP70qOCXZd4ICOU/YZXuxgJlzi/vO+XXGWobt09Z3+Msqgg==}
'@git.zone/tsbuild@2.6.4':
resolution: {integrity: sha512-eeNW5hnXHU9lPzTaMbtdYDkb6cpFFC8fF5849BiwLO4N1Ga9Q5Om/6w5SZyJQcct8rHjcTgOOWdlxhjeKCr6NQ==}
hasBin: true
@@ -449,6 +524,18 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jsep-plugin/assignment@1.3.0':
resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==}
engines: {node: '>= 10.16.0'}
peerDependencies:
jsep: ^0.4.0||^1.0.0
'@jsep-plugin/regex@1.0.4':
resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==}
engines: {node: '>= 10.16.0'}
peerDependencies:
jsep: ^0.4.0||^1.0.0
'@koa/router@9.4.0':
resolution: {integrity: sha512-dOOXgzqaDoHu5qqMEPLKEgLz5CeIA7q8+1W62mCvFVCOqeC71UoTGJ4u1xUSOpIl2J1x2pqrNULkFteUeZW3/A==}
engines: {node: '>= 8.0.0'}
@@ -1430,6 +1517,10 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@xmldom/xmldom@0.9.8':
resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==}
engines: {node: '>=14.6'}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -1446,6 +1537,9 @@ packages:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-256-colors@1.1.0:
resolution: {integrity: sha1-kQ3lDvzHwJ49gvL4er1rcAwYgYo=}
engines: {node: '>=0.10.0'}
@@ -1496,6 +1590,9 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
axios@1.11.0:
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
b4a@1.6.7:
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
@@ -1586,6 +1683,9 @@ packages:
buffer-json@2.0.0:
resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==}
buffer-reverse@1.0.1:
resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -1703,6 +1803,10 @@ packages:
color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
@@ -1795,14 +1899,25 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
crypto-random-string@5.0.0:
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
engines: {node: '>=14.16'}
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
data-uri-to-buffer@6.0.2:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
engines: {node: '>= 14'}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@@ -1835,6 +1950,9 @@ packages:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -1961,6 +2079,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -2074,6 +2196,9 @@ packages:
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
fast-xml-parser@3.21.1:
resolution: {integrity: sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==}
hasBin: true
@@ -2302,6 +2427,13 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
html-entities@2.6.0:
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
html-minifier@4.0.0:
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
engines: {node: '>=6'}
@@ -2425,6 +2557,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@@ -2493,18 +2628,39 @@ packages:
jsbn@1.1.0:
resolution: {integrity: sha1-sBMHyym2GKHtJux56RH4A8TaAEA=}
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsep@1.4.0:
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
engines: {node: '>= 10.16.0'}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
jsonfile@4.0.0:
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jsonpath-plus@10.3.0:
resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==}
engines: {node: '>=18.0.0'}
hasBin: true
keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'}
@@ -2713,6 +2869,10 @@ packages:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
merkletreejs@0.4.1:
resolution: {integrity: sha512-W2VSHeGTdAnWtedee+pgGn7SHvncMdINnMeHAaXrfarSaMNLff/pm7RCr/QXYxN6XzJFgJZY+28ejO0lAosW4A==}
engines: {node: '>= 7.6.0'}
methods@1.1.2:
resolution: {integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=}
engines: {node: '>= 0.6'}
@@ -2959,6 +3119,9 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
node-html-better-parser@1.5.3:
resolution: {integrity: sha512-rvnbT4FUS+pIQPAs3bBpzeuWdgdjne0LsgrEINdsMfAvjAKHTEGVhknMEqBriGuVRWM8iRL1LKhRhZ9RB6gPVA==}
normalize-newline@4.1.0:
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
engines: {node: '>=12'}
@@ -2967,6 +3130,9 @@ packages:
resolution: {integrity: sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==}
engines: {node: '>=14.16'}
nwsapi@2.2.21:
resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
object-assign@4.1.1:
resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=}
engines: {node: '>=0.10.0'}
@@ -3052,6 +3218,9 @@ packages:
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
param-case@2.1.1:
resolution: {integrity: sha1-35T9jPZTHs915r75oIWPvHK+Ikc=}
@@ -3075,6 +3244,9 @@ packages:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -3292,6 +3464,10 @@ packages:
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -3318,6 +3494,9 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@@ -3349,6 +3528,13 @@ packages:
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
saxon-js@2.7.0:
resolution: {integrity: sha512-uGAv7H85EuWtAyyXVezXBg3/j2UvhEfT3N9+sqkGwCJVW33KlkadllDCdES/asCDklUo0UlM6178tZ0n3GPZjQ==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -3582,6 +3768,20 @@ packages:
tiny-worker@2.3.0:
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
tldts-core@6.1.86:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
tldts@6.1.86:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
tmp@0.2.5:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
@@ -3590,6 +3790,10 @@ packages:
resolution: {integrity: sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw==}
engines: {node: '>=14.16'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
@@ -3602,6 +3806,10 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
treeify@1.1.0:
resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==}
engines: {node: '>=0.6'}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -3730,14 +3938,26 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@11.0.0:
resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
engines: {node: '>=12'}
@@ -3803,6 +4023,10 @@ packages:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xml2js@0.5.0:
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
engines: {node: '>=4.0.0'}
@@ -3815,10 +4039,21 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xmldom@0.6.0:
resolution: {integrity: sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==}
engines: {node: '>=10.0.0'}
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
xpath@0.0.34:
resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==}
engines: {node: '>=0.6.0'}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -3938,6 +4173,14 @@ snapshots:
- utf-8-validate
- vue
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@aws-crypto/crc32@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
@@ -4497,6 +4740,16 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@cantoo/pdf-lib@2.4.2':
dependencies:
'@pdf-lib/standard-fonts': 1.0.0
'@pdf-lib/upng': 1.0.1
color: 4.2.3
crypto-js: 4.2.0
node-html-better-parser: 1.5.3
pako: 1.0.11
tslib: 2.8.1
'@cloudflare/workers-types@4.20250809.0': {}
'@colors/colors@1.6.0': {}
@@ -4505,6 +4758,26 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@csstools/color-helpers@5.0.2': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.0.2
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
@@ -4556,6 +4829,18 @@ snapshots:
- supports-color
- vue
'@e-invoice-eu/core@2.1.9':
dependencies:
'@cantoo/pdf-lib': 2.4.2
'@e965/xlsx': 0.20.3
'@esgettext/runtime': 1.3.6
ajv: 8.17.1
jsonpath-plus: 10.3.0
tmp-promise: 3.0.3
xmlbuilder2: 3.1.1
'@e965/xlsx@0.20.3': {}
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.4
@@ -4650,6 +4935,27 @@ snapshots:
'@esbuild/win32-x64@0.25.8':
optional: true
'@esgettext/runtime@1.3.6': {}
'@fin.cx/einvoice@5.1.4':
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
pako: 2.1.0
pdf-lib: 1.17.1
saxon-js: 2.7.0
xmldom: 0.6.0
xpath: 0.0.34
transitivePeerDependencies:
- bufferutil
- canvas
- debug
- supports-color
- utf-8-validate
'@git.zone/tsbuild@2.6.4':
dependencies:
'@git.zone/tspublish': 1.10.1
@@ -4769,6 +5075,14 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jsep-plugin/assignment@1.3.0(jsep@1.4.0)':
dependencies:
jsep: 1.4.0
'@jsep-plugin/regex@1.0.4(jsep@1.4.0)':
dependencies:
jsep: 1.4.0
'@koa/router@9.4.0':
dependencies:
debug: 4.4.1
@@ -6343,6 +6657,8 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@xmldom/xmldom@0.9.8': {}
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -6359,6 +6675,13 @@ snapshots:
dependencies:
humanize-ms: 1.2.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.0.6
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-256-colors@1.1.0: {}
ansi-regex@5.0.1: {}
@@ -6397,6 +6720,14 @@ snapshots:
asynckit@0.4.0: {}
axios@1.11.0:
dependencies:
follow-redirects: 1.15.11(debug@4.4.1)
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
b4a@1.6.7: {}
bail@2.0.2: {}
@@ -6499,6 +6830,8 @@ snapshots:
buffer-json@2.0.0: {}
buffer-reverse@1.0.1: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -6624,6 +6957,11 @@ snapshots:
color-convert: 1.9.3
color-string: 1.9.1
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
colorspace@1.1.4:
dependencies:
color: 3.2.1
@@ -6701,12 +7039,24 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
crypto-random-string@5.0.0:
dependencies:
type-fest: 2.19.0
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
data-uri-to-buffer@6.0.2: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
date-fns@4.1.0: {}
dayjs@1.11.13: {}
@@ -6723,6 +7073,8 @@ snapshots:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -6852,6 +7204,8 @@ snapshots:
entities@4.5.0: {}
entities@6.0.1: {}
env-paths@2.2.1: {}
error-ex@1.3.2:
@@ -7026,6 +7380,8 @@ snapshots:
fast-json-stable-stringify@2.1.0: {}
fast-uri@3.0.6: {}
fast-xml-parser@3.21.1:
dependencies:
strnum: 1.1.2
@@ -7328,6 +7684,12 @@ snapshots:
he@1.2.0: {}
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
html-entities@2.6.0: {}
html-minifier@4.0.0:
dependencies:
camel-case: 3.0.0
@@ -7456,6 +7818,8 @@ snapshots:
is-plain-obj@4.1.0: {}
is-potential-custom-element-name@1.0.1: {}
is-promise@4.0.0: {}
is-regex@1.2.1:
@@ -7512,10 +7876,41 @@ snapshots:
jsbn@1.1.0: {}
jsdom@26.1.0:
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.21
parse5: 7.3.0
rrweb-cssom: 0.8.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 5.1.2
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.3
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
jsep@1.4.0: {}
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@1.0.0: {}
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
@@ -7526,6 +7921,12 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jsonpath-plus@10.3.0:
dependencies:
'@jsep-plugin/assignment': 1.3.0(jsep@1.4.0)
'@jsep-plugin/regex': 1.0.4(jsep@1.4.0)
jsep: 1.4.0
keygrip@1.1.0:
dependencies:
tsscmp: 1.0.6
@@ -7825,6 +8226,12 @@ snapshots:
merge-descriptors@2.0.0: {}
merkletreejs@0.4.1:
dependencies:
buffer-reverse: 1.0.1
crypto-js: 4.2.0
treeify: 1.1.0
methods@1.1.2: {}
micromark-core-commonmark@2.0.3:
@@ -8170,12 +8577,18 @@ snapshots:
node-forge@1.3.1: {}
node-html-better-parser@1.5.3:
dependencies:
html-entities: 2.6.0
normalize-newline@4.1.0:
dependencies:
replace-buffer: 1.2.1
normalize-url@8.0.2: {}
nwsapi@2.2.21: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -8260,6 +8673,8 @@ snapshots:
pako@1.0.11: {}
pako@2.1.0: {}
param-case@2.1.1:
dependencies:
no-case: 2.3.2
@@ -8281,6 +8696,10 @@ snapshots:
parse-ms@4.0.0: {}
parse5@7.3.0:
dependencies:
entities: 6.0.1
parseurl@1.3.3: {}
passthrough-counter@1.0.0: {}
@@ -8547,6 +8966,8 @@ snapshots:
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
resolve-alpn@1.2.1: {}
resolve-from@4.0.0: {}
@@ -8593,6 +9014,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
rrweb-cssom@0.8.0: {}
rss-parser@3.13.0:
dependencies:
entities: 2.2.0
@@ -8634,6 +9057,16 @@ snapshots:
sax@1.4.1: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
saxon-js@2.7.0:
dependencies:
axios: 1.11.0
transitivePeerDependencies:
- debug
semver@6.3.1: {}
semver@7.7.2: {}
@@ -8962,6 +9395,18 @@ snapshots:
dependencies:
esm: 3.2.25
tldts-core@6.1.86: {}
tldts@6.1.86:
dependencies:
tldts-core: 6.1.86
tmp-promise@3.0.3:
dependencies:
tmp: 0.2.5
tmp@0.2.5: {}
toidentifier@1.0.1: {}
token-types@6.0.4:
@@ -8969,6 +9414,10 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
tr46@3.0.0:
dependencies:
punycode: 2.3.1
@@ -8979,6 +9428,8 @@ snapshots:
tree-kill@1.2.2: {}
treeify@1.1.0: {}
trim-lines@3.0.1: {}
triple-beam@1.4.1: {}
@@ -9095,10 +9546,20 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
webidl-conversions@7.0.0: {}
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@3.0.0: {}
whatwg-mimetype@4.0.0: {}
whatwg-url@11.0.0:
dependencies:
tr46: 3.0.0
@@ -9159,6 +9620,8 @@ snapshots:
dependencies:
sax: 1.4.1
xml-name-validator@5.0.0: {}
xml2js@0.5.0:
dependencies:
sax: 1.4.1
@@ -9173,8 +9636,14 @@ snapshots:
xmlbuilder@11.0.1: {}
xmlchars@2.2.0: {}
xmldom@0.6.0: {}
xmlhttprequest-ssl@2.1.2: {}
xpath@0.0.34: {}
xtend@4.0.2: {}
y18n@5.0.8: {}

223
readme.md
View File

@@ -1,7 +1,7 @@
# @fin.cx/skr 📊
> **Enterprise-grade German accounting standards implementation for SKR03 and SKR04**
> Rock-solid double-entry bookkeeping with MongoDB persistence and full TypeScript support
> Rock-solid double-entry bookkeeping with MongoDB persistence, e-invoice integration, and full TypeScript support
## 🚀 Why @fin.cx/skr?
@@ -17,6 +17,9 @@ Building compliant German accounting software? You've come to the right place! T
- **🔄 Transaction Safety**: Built-in double-entry validation and automatic reversals
- **✅ Battle-Tested**: 65+ comprehensive tests covering all edge cases
- **🛡️ SKR Validation**: Automatic validation against official SKR standards
- **🧾 E-Invoice Support**: Full XRechnung/ZUGFeRD integration for modern invoice processing
- **🔐 Cryptographic Security**: Merkle tree and digital signature support for audit trails
- **📑 PDF Export**: Professional PDF report generation with customizable templates
## 📦 Installation
@@ -76,6 +79,47 @@ const journalEntry = await api.postJournalEntry({
});
```
### 🧾 E-Invoice Integration
```typescript
// Import electronic invoices (XRechnung/ZUGFeRD)
const invoiceData = await api.importInvoice(xmlContent, {
format: 'xrechnung',
validateSchema: true,
checkDuplicates: true
});
// Automatically book invoice to accounting
const booking = await api.bookInvoice(invoiceData.invoiceId, {
autoDetectAccounts: true,
splitVAT: true,
createPaymentSchedule: true
});
// Export invoice in various formats
const xRechnung = await api.exportInvoice(invoiceId, {
format: 'xrechnung',
version: '3.0',
includeAttachments: true
});
// Search and filter invoices
const invoices = await api.searchInvoices({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
status: 'booked',
minAmount: 100,
customerVATId: 'DE123456789'
});
// Generate compliance reports
const complianceReport = await api.createInvoiceComplianceReport({
period: '2024-Q1',
includeValidation: true,
includeStatistics: true
});
```
### 📊 Generating Financial Reports
```typescript
@@ -109,6 +153,71 @@ const cashFlow = await api.generateCashFlowStatement({
});
```
### 📑 Advanced Export Features
```typescript
// Export complete annual closing package (Jahresabschluss)
const jahresabschluss = await api.exportJahresabschluss({
year: 2024,
includeReports: ['balance_sheet', 'income_statement', 'cash_flow'],
format: 'structured', // 'structured' | 'pdf' | 'csv'
language: 'de',
signatureRequired: true
});
// Generate PDF reports with professional formatting
const pdfReports = await api.generatePdfReports({
reports: ['trial_balance', 'income_statement', 'balance_sheet'],
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
companyInfo: {
name: 'Mustermann GmbH',
address: 'Hauptstraße 1, 10115 Berlin',
taxNumber: 'DE123456789',
registrationNumber: 'HRB 12345'
},
outputPath: './reports/',
template: 'professional' // Custom templates available
});
// Export with cryptographic signatures for audit trail
const signedExport = await api.signExport({
data: jahresabschluss,
privateKey: privateKeyPEM,
certificate: certificatePEM,
includeTimestamp: true,
hashAlgorithm: 'SHA256'
});
// Detailed account data export
const accountExport = await api.exportAccountData({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
format: 'detailed', // 'summary' | 'detailed' | 'tree'
includeTransactions: true,
includeBalances: true
});
// Balance history export for analysis
const balanceHistory = await api.exportBalanceData({
accounts: ['1200', '1000', '8400'],
interval: 'monthly', // 'daily' | 'weekly' | 'monthly' | 'quarterly'
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
includeRunningTotals: true
});
// Ledger export with filtering options
const ledgerExport = await api.exportLedgerData({
accounts: ['1000-1999'], // Range support
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
includeReversals: false,
groupByAccount: true,
format: 'journal' // 'journal' | 'T-account' | 'chronological'
});
```
## 🏗️ Core Features
### Account Management
@@ -316,6 +425,50 @@ try {
}
```
### Invoice Processing & Compliance
```typescript
// Get invoice statistics and analytics
const stats = await api.getInvoiceStatistics({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31'),
groupBy: 'month',
includeVATAnalysis: true
});
// Generate invoices programmatically
const invoice = await api.generateInvoice({
invoiceNumber: 'INV-2024-001',
date: new Date(),
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
seller: {
name: 'Your Company GmbH',
vatId: 'DE123456789',
address: 'Hauptstraße 1, 10115 Berlin'
},
buyer: {
name: 'Customer AG',
vatId: 'DE987654321',
address: 'Kundenweg 5, 80331 München'
},
lines: [
{
description: 'Consulting Services',
quantity: 10,
unitPrice: 100,
vatRate: 19
}
]
});
// Validate invoice compliance
const validation = await api.validateInvoice(invoice, {
standard: 'xrechnung',
checkBusinessRules: true,
checkVATRules: true
});
```
### Utility Functions
```typescript
@@ -346,7 +499,12 @@ import type {
IPaginationParams,
IAccountBalance,
ICashFlowStatement,
IGeneralLedger
IGeneralLedger,
IInvoice,
IInvoiceLine,
IInvoiceParty,
IBookingRules,
IValidationResult
} from '@fin.cx/skr';
// All operations are fully typed
@@ -413,7 +571,22 @@ async function performJahresabschluss() {
]
});
// 2. Generate financial statements
// 2. Generate comprehensive annual closing package
const jahresabschluss = await api.exportJahresabschluss({
year: 2024,
includeReports: ['balance_sheet', 'income_statement', 'cash_flow', 'trial_balance'],
format: 'pdf',
language: 'de',
signatureRequired: true,
companyInfo: {
name: 'Mustermann GmbH',
address: 'Hauptstraße 1, 10115 Berlin',
taxNumber: 'DE123456789',
registrationNumber: 'HRB 12345'
}
});
// 3. Generate individual reports for analysis
const incomeStatement = await api.generateIncomeStatement({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31')
@@ -423,34 +596,37 @@ async function performJahresabschluss() {
date: new Date('2024-12-31')
});
const trialBalance = await api.generateTrialBalance({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31')
});
const cashFlow = await api.generateCashFlowStatement({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31')
});
// 3. Export for tax advisor
// 4. Export for tax advisor in DATEV format
const datevExport = await api.exportToDATEV({
dateFrom: new Date('2024-01-01'),
dateTo: new Date('2024-12-31')
});
// 4. Close the period
// 5. Create signed export for audit trail
const signedExport = await api.signExport({
data: jahresabschluss,
privateKey: process.env.PRIVATE_KEY!,
certificate: process.env.CERTIFICATE!,
includeTimestamp: true
});
// 6. Close the period
await api.closePeriod('2024-12', {
performYearEndAdjustments: true,
generateReports: true
});
console.log('=== Jahresabschluss 2024 ===');
console.log(`Umsatz: €${incomeStatement.totalRevenue}`);
console.log(`Aufwendungen: €${incomeStatement.totalExpenses}`);
console.log(`Jahresergebnis: €${incomeStatement.netIncome}`);
console.log(`Bilanzsumme: €${balanceSheet.assets.totalAssets}`);
console.log(`Cash Flow: €${cashFlow.netCashFlow}`);
console.log('🎊 Jahresabschluss 2024 Complete!');
console.log(`📈 Umsatz: €${incomeStatement.totalRevenue.toLocaleString('de-DE')}`);
console.log(`💰 Aufwendungen: €${incomeStatement.totalExpenses.toLocaleString('de-DE')}`);
console.log(`📊 Jahresergebnis: €${incomeStatement.netIncome.toLocaleString('de-DE')}`);
console.log(`💼 Bilanzsumme: €${balanceSheet.assets.totalAssets.toLocaleString('de-DE')}`);
console.log(`💵 Cash Flow: €${cashFlow.netCashFlow.toLocaleString('de-DE')}`);
console.log(incomeStatement.netIncome > 0 ? '✅ Gewinn!' : '📉 Verlust');
await api.close();
@@ -472,6 +648,9 @@ performJahresabschluss().catch(console.error);
| **`Account`** | Account model with balance tracking |
| **`Transaction`** | Double-entry transaction model |
| **`JournalEntry`** | Complex multi-line journal entries |
| **`InvoiceAdapter`** | XRechnung/ZUGFeRD invoice processing |
| **`InvoiceBookingEngine`** | Automatic invoice to accounting booking |
| **`InvoiceStorage`** | Invoice persistence and search |
### Key Methods
@@ -489,6 +668,13 @@ performJahresabschluss().catch(console.error);
| `generateCashFlowStatement(params)` | Generate cash flow statement |
| `generateGeneralLedger(params)` | Generate complete general ledger |
| `exportToDATEV(params)` | Export DATEV-compatible data |
| `exportJahresabschluss(params)` | Export complete annual closing package |
| `generatePdfReports(params)` | Generate professional PDF reports |
| `signExport(data)` | Create cryptographically signed exports |
| `importInvoice(data, options)` | Import XRechnung/ZUGFeRD invoices |
| `bookInvoice(invoiceId, rules)` | Book invoice to accounting |
| `exportInvoice(id, options)` | Export invoice in various formats |
| `searchInvoices(filter)` | Search and filter invoices |
| `closePeriod(period, options)` | Close accounting period |
| `recalculateBalances()` | Recalculate all account balances |
| `validateDoubleEntry(data)` | Validate transaction before posting |
@@ -505,6 +691,9 @@ performJahresabschluss().catch(console.error);
- **📚 German Compliance**: Full HGB/GoBD compliance built-in
- **🤝 Type Safety**: Complete TypeScript definitions prevent runtime errors
- **🔍 Smart Validation**: Warns about non-standard accounts and type mismatches
- **🧾 E-Invoice Ready**: Native XRechnung/ZUGFeRD support for modern workflows
- **🔐 Audit-Proof**: Cryptographic signatures and Merkle trees for tamper-proof records
- **📑 Professional Reports**: Generate PDF reports that impress auditors and stakeholders
## 📋 Requirements
@@ -525,6 +714,8 @@ pnpm test test/test.skr03.ts # SKR03 functionality
pnpm test test/test.skr04.ts # SKR04 functionality
pnpm test test/test.jahresabschluss.skr03.ts # Annual closing SKR03
pnpm test test/test.jahresabschluss.skr04.ts # Annual closing SKR04
pnpm test test/test.invoice.ts # Invoice processing
pnpm test test/test.export.ts # Export functionality
```
## License and Legal Information

View File

@@ -8,3 +8,9 @@ export * from './skr.classes.reports.js';
export * from './skr.api.js';
export * from './skr03.data.js';
export * from './skr04.data.js';
export * from './skr.export.js';
export * from './skr.export.ledger.js';
export * from './skr.export.accounts.js';
export * from './skr.export.balances.js';
export * from './skr.export.pdf.js';
export * from './skr.security.js';

View File

@@ -3,5 +3,26 @@ import * as smartdata from '@push.rocks/smartdata';
import * as smartunique from '@push.rocks/smartunique';
import * as smarttime from '@push.rocks/smarttime';
import * as smartlog from '@push.rocks/smartlog';
import * as smartfile from '@push.rocks/smartfile';
import * as smarthash from '@push.rocks/smarthash';
import * as smartpath from '@push.rocks/smartpath';
import * as smartpdf from '@push.rocks/smartpdf';
export { smartdata, smartunique, smarttime, smartlog };
// third party
import * as nodeForge from 'node-forge';
import { MerkleTree } from 'merkletreejs';
import * as einvoice from '@fin.cx/einvoice';
export {
smartdata,
smartunique,
smarttime,
smartlog,
smartfile,
smarthash,
smartpath,
smartpdf,
nodeForge,
MerkleTree,
einvoice
};

View File

@@ -1,10 +1,28 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
import { Ledger } from './skr.classes.ledger.js';
import { Reports } from './skr.classes.reports.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import { SkrExport, type IExportOptions } from './skr.export.js';
import { LedgerExporter } from './skr.export.ledger.js';
import { AccountsExporter } from './skr.export.accounts.js';
import { BalancesExporter } from './skr.export.balances.js';
import { PdfReportGenerator, type IPdfReportOptions } from './skr.export.pdf.js';
import { SecurityManager, type ISigningOptions } from './skr.security.js';
import { InvoiceAdapter } from './skr.invoice.adapter.js';
import { InvoiceStorage } from './skr.invoice.storage.js';
import { InvoiceBookingEngine, type IBookingOptions, type IBookingResult } from './skr.invoice.booking.js';
import type {
IInvoice,
IInvoiceFilter,
IInvoiceImportOptions,
IInvoiceExportOptions,
IBookingRules,
TInvoiceDirection,
} from './skr.invoice.entity.js';
import type {
IDatabaseConfig,
TSKRType,
@@ -17,6 +35,7 @@ import type {
ITrialBalanceReport,
IIncomeStatement,
IBalanceSheet,
IAccountBalance,
} from './skr.types.js';
/**
@@ -29,6 +48,9 @@ export class SkrApi {
private logger: plugins.smartlog.Smartlog;
private initialized: boolean = false;
private currentSKRType: TSKRType | null = null;
private invoiceAdapter: InvoiceAdapter | null = null;
private invoiceStorage: InvoiceStorage | null = null;
private invoiceBookingEngine: InvoiceBookingEngine | null = null;
constructor(private config: IDatabaseConfig) {
this.chartOfAccounts = new ChartOfAccounts(config);
@@ -62,6 +84,13 @@ export class SkrApi {
this.currentSKRType = skrType;
this.ledger = new Ledger(skrType);
this.reports = new Reports(skrType);
// Initialize invoice components
this.invoiceAdapter = new InvoiceAdapter();
const invoicePath = this.config.invoiceExportPath || path.resolve(process.cwd(), 'exports', 'invoices');
this.invoiceStorage = new InvoiceStorage(invoicePath);
this.invoiceBookingEngine = new InvoiceBookingEngine(skrType);
this.initialized = true;
this.logger.log('info', 'SKR API initialized successfully');
@@ -350,6 +379,262 @@ export class SkrApi {
return await this.chartOfAccounts.exportAccountsToCSV();
}
/**
* Export Jahresabschluss in GoBD-compliant BagIt format
* Creates a revision-safe export for 10-year archival
*/
public async exportJahresabschluss(options: IExportOptions): Promise<string> {
this.ensureInitialized();
if (!this.ledger || !this.reports || !this.currentSKRType) {
throw new Error('API not fully initialized');
}
this.logger.log('info', `Starting Jahresabschluss export for fiscal year ${options.fiscalYear}`);
// Create export instance
const exporter = new SkrExport(options);
// Create BagIt structure
await exporter.createBagItStructure();
await exporter.createExportMetadata(this.currentSKRType);
await exporter.createSchemas();
// Export accounting data
await this.exportLedgerData(exporter, options);
await this.exportAccountData(exporter, options);
await this.exportBalanceData(exporter, options);
// Generate PDF reports if requested
if (options.generatePdfReports) {
await this.generatePdfReports(exporter, options);
}
// Sign export if requested
if (options.signExport) {
await this.signExport(exporter, options);
}
// Create manifests and validate
await exporter.writeManifests();
const merkleRoot = await exporter.createMerkleTree();
const isValid = await exporter.validateBagIt();
if (!isValid) {
throw new Error('BagIt validation failed');
}
this.logger.log('ok', `Jahresabschluss export completed. Merkle root: ${merkleRoot}`);
return options.exportPath;
}
/**
* Export ledger data in NDJSON format
*/
private async exportLedgerData(exporter: SkrExport, options: IExportOptions): Promise<void> {
if (!this.ledger) throw new Error('Ledger not initialized');
const ledgerExporter = new LedgerExporter(options.exportPath);
await ledgerExporter.initialize();
// Get all transactions for the period
const transactions = await this.chartOfAccounts.getTransactions({
dateFrom: options.dateFrom,
dateTo: options.dateTo
});
// Export each transaction
for (const transaction of transactions) {
const transactionData = transaction;
await ledgerExporter.exportTransaction(transactionData as any);
}
// Get all journal entries for the period
// Use MongoDB query syntax for date range
const journalEntries = await JournalEntry.getInstances({
date: {
$gte: options.dateFrom,
$lte: options.dateTo
} as any, // SmartData supports MongoDB query operators
skrType: this.currentSKRType
});
// Export each journal entry
for (const entry of journalEntries) {
const entryData = entry;
await ledgerExporter.exportJournalEntry(entryData as any);
}
const entryCount = await ledgerExporter.close();
this.logger.log('info', `Exported ${entryCount} ledger entries`);
}
/**
* Export account data in CSV format
*/
private async exportAccountData(exporter: SkrExport, options: IExportOptions): Promise<void> {
const accountsExporter = new AccountsExporter(options.exportPath);
// Get all accounts
const accounts = await this.chartOfAccounts.getAllAccounts();
// Add each account to export
for (const account of accounts) {
const accountData = account;
accountsExporter.addAccount(accountData as any);
}
// Export to CSV and JSON
await accountsExporter.exportToCSV();
await accountsExporter.exportToJSON();
this.logger.log('info', `Exported ${accountsExporter.getAccountCount()} accounts`);
}
/**
* Export balance data in CSV format
*/
private async exportBalanceData(exporter: SkrExport, options: IExportOptions): Promise<void> {
if (!this.ledger) throw new Error('Ledger not initialized');
const balancesExporter = new BalancesExporter(
options.exportPath,
options.fiscalYear
);
// Get all accounts with balances
const accounts = await this.chartOfAccounts.getAllAccounts();
for (const account of accounts) {
const balance = await this.ledger.getAccountBalance(
account.accountNumber,
options.dateTo
);
if (balance) {
balancesExporter.addBalance(
account.accountNumber,
account.accountName,
balance as IAccountBalance,
`${options.fiscalYear}`
);
}
}
// Export balance reports
await balancesExporter.exportToCSV();
await balancesExporter.exportTrialBalance();
await balancesExporter.exportClassSummary();
this.logger.log('info', `Exported ${balancesExporter.getBalanceCount()} account balances`);
}
/**
* Generate PDF reports for the export
*/
private async generatePdfReports(exporter: SkrExport, options: IExportOptions): Promise<void> {
if (!this.reports) throw new Error('Reports not initialized');
const pdfOptions: IPdfReportOptions = {
companyName: options.companyInfo?.name || 'Unternehmen',
companyAddress: options.companyInfo?.address,
taxId: options.companyInfo?.taxId,
registrationNumber: options.companyInfo?.registrationNumber,
fiscalYear: options.fiscalYear,
dateFrom: options.dateFrom,
dateTo: options.dateTo,
preparedDate: new Date()
};
const pdfGenerator = new PdfReportGenerator(options.exportPath, pdfOptions);
await pdfGenerator.initialize();
try {
// Generate reports
const trialBalance = await this.reports.getTrialBalance({
dateFrom: options.dateFrom,
dateTo: options.dateTo,
skrType: this.currentSKRType
});
const incomeStatement = await this.reports.getIncomeStatement({
dateFrom: options.dateFrom,
dateTo: options.dateTo,
skrType: this.currentSKRType
});
const balanceSheet = await this.reports.getBalanceSheet({
dateFrom: options.dateFrom,
dateTo: options.dateTo,
skrType: this.currentSKRType
});
// Generate PDFs
const jahresabschlussPdf = await pdfGenerator.generateJahresabschlussPdf(
trialBalance,
incomeStatement,
balanceSheet
);
// Save PDFs
await pdfGenerator.savePdfReport('jahresabschluss.pdf', jahresabschlussPdf);
// Store in BagIt structure
await exporter.storeDocument(jahresabschlussPdf, 'jahresabschluss.pdf');
this.logger.log('info', 'PDF reports generated successfully');
} finally {
await pdfGenerator.close();
}
}
/**
* Sign the export with CAdES signature
*/
private async signExport(exporter: SkrExport, options: IExportOptions): Promise<void> {
const signingOptions: ISigningOptions = {
certificatePem: options.signExport ? undefined : undefined, // Use provided cert or generate
privateKeyPem: options.signExport ? undefined : undefined,
includeTimestamp: options.timestampExport !== false
};
const security = new SecurityManager(signingOptions);
// Generate self-signed certificate if none provided
let cert: string, key: string;
if (!signingOptions.certificatePem) {
const generated = await security.generateSelfSignedCertificate(
options.companyInfo?.name || 'SKR Export System'
);
cert = generated.certificate;
key = generated.privateKey;
} else {
cert = signingOptions.certificatePem;
key = signingOptions.privateKeyPem!;
}
// Sign the manifest
const manifestPath = path.resolve(
options.exportPath,
`jahresabschluss_${options.fiscalYear}`,
'manifest-sha256.txt'
);
await security.createDetachedSignature(
manifestPath,
path.resolve(
options.exportPath,
`jahresabschluss_${options.fiscalYear}`,
'data',
'metadata',
'signatures',
'manifest.cades'
)
);
this.logger.log('info', 'Export signed with CAdES signature');
}
// ========== Utility Methods ==========
/**
@@ -532,4 +817,204 @@ export class SkrApi {
totalPages,
};
}
// ========== Invoice Management ==========
/**
* Import an invoice from file or buffer
* Parses, validates, and optionally books the invoice
*/
public async importInvoice(
file: Buffer | string,
direction: TInvoiceDirection,
options?: IInvoiceImportOptions
): Promise<IInvoice> {
this.ensureInitialized();
if (!this.invoiceAdapter || !this.invoiceStorage || !this.invoiceBookingEngine) {
throw new Error('Invoice components not initialized');
}
this.logger.log('info', `Importing ${direction} invoice`);
// Parse and validate invoice
const invoice = await this.invoiceAdapter.parseInvoice(file, direction);
// Store invoice
await this.invoiceStorage.initialize();
const contentHash = await this.invoiceStorage.storeInvoice(invoice);
invoice.contentHash = contentHash;
// Auto-book if requested
if (options?.autoBook) {
const bookingResult = await this.bookInvoice(
invoice,
options.bookingRules,
{
autoBook: true,
confidenceThreshold: options.confidenceThreshold || 80,
skipValidation: options.validateOnly
}
);
if (bookingResult.success && bookingResult.bookingInfo) {
invoice.bookingInfo = bookingResult.bookingInfo;
invoice.status = 'posted';
// Update stored metadata with booking information
await this.invoiceStorage.updateMetadata(invoice.contentHash, {
journalEntryId: bookingResult.bookingInfo.journalEntryId,
transactionIds: bookingResult.bookingInfo.transactionIds
});
}
}
this.logger.log('info', `Invoice imported successfully: ${invoice.invoiceNumber}`);
return invoice;
}
/**
* Book an invoice to the ledger
*/
public async bookInvoice(
invoice: IInvoice,
bookingRules?: Partial<IBookingRules>,
options?: IBookingOptions
): Promise<IBookingResult> {
this.ensureInitialized();
if (!this.invoiceBookingEngine) {
throw new Error('Invoice booking engine not initialized');
}
this.logger.log('info', `Booking invoice ${invoice.invoiceNumber}`);
const result = await this.invoiceBookingEngine.bookInvoice(
invoice,
bookingRules,
options
);
if (result.success) {
this.logger.log('info', `Invoice booked successfully with confidence ${result.confidence}%`);
// Update stored metadata if invoice has a content hash
if (invoice.contentHash && result.bookingInfo && this.invoiceStorage) {
await this.invoiceStorage.updateMetadata(invoice.contentHash, {
journalEntryId: result.bookingInfo.journalEntryId,
transactionIds: result.bookingInfo.transactionIds
});
}
} else {
this.logger.log('error', `Invoice booking failed: ${result.errors?.join(', ')}`);
}
return result;
}
/**
* Export an invoice in a different format
*/
public async exportInvoice(
invoice: IInvoice,
options: IInvoiceExportOptions
): Promise<{ xml: string; pdf?: Buffer }> {
this.ensureInitialized();
if (!this.invoiceAdapter) {
throw new Error('Invoice adapter not initialized');
}
this.logger.log('info', `Exporting invoice ${invoice.invoiceNumber} to ${options.format}`);
// Convert format if needed
const xml = await this.invoiceAdapter.convertFormat(invoice, options.format);
// Generate PDF if requested
let pdf: Buffer | undefined;
if (options.embedInPdf) {
const result = await this.invoiceAdapter.generateInvoice(invoice, options.format);
pdf = result.pdf;
}
return { xml, pdf };
}
/**
* Search invoices by filter
*/
public async searchInvoices(filter: IInvoiceFilter): Promise<IInvoice[]> {
this.ensureInitialized();
if (!this.invoiceStorage) {
throw new Error('Invoice storage not initialized');
}
await this.invoiceStorage.initialize();
const metadata = await this.invoiceStorage.searchInvoices(filter);
const invoices: IInvoice[] = [];
for (const meta of metadata) {
const invoice = await this.invoiceStorage.retrieveInvoice(meta.contentHash);
if (invoice) {
invoices.push(invoice);
}
}
return invoices;
}
/**
* Get invoice by content hash
*/
public async getInvoice(contentHash: string): Promise<IInvoice | null> {
this.ensureInitialized();
if (!this.invoiceStorage) {
throw new Error('Invoice storage not initialized');
}
await this.invoiceStorage.initialize();
return await this.invoiceStorage.retrieveInvoice(contentHash);
}
/**
* Get invoice storage statistics
*/
public async getInvoiceStatistics(): Promise<any> {
this.ensureInitialized();
if (!this.invoiceStorage) {
throw new Error('Invoice storage not initialized');
}
await this.invoiceStorage.initialize();
return await this.invoiceStorage.getStatistics();
}
/**
* Create EN16931 compliance report for invoices
*/
public async createInvoiceComplianceReport(): Promise<void> {
this.ensureInitialized();
if (!this.invoiceStorage) {
throw new Error('Invoice storage not initialized');
}
await this.invoiceStorage.initialize();
await this.invoiceStorage.createComplianceReport();
this.logger.log('info', 'Invoice compliance report created');
}
/**
* Generate an invoice from internal data
*/
public async generateInvoice(
invoiceData: Partial<IInvoice>,
format: IInvoiceExportOptions['format']
): Promise<{ xml: string; pdf?: Buffer }> {
this.ensureInitialized();
if (!this.invoiceAdapter) {
throw new Error('Invoice adapter not initialized');
}
this.logger.log('info', `Generating invoice in ${format} format`);
return await this.invoiceAdapter.generateInvoice(invoiceData, format);
}
}

154
ts/skr.export.accounts.ts Normal file
View File

@@ -0,0 +1,154 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { IAccountData, TSKRType } from './skr.types.js';
// Extended interface for export with additional fields
export interface IAccountDataExport extends IAccountData {
parentAccount?: string;
defaultTaxCode?: string;
activeFrom?: Date | string;
activeTo?: Date | string;
}
export interface IAccountExportRow {
account_code: string;
name: string;
type: string;
class: number;
parent?: string;
skr_set: TSKRType;
tax_code_default?: string;
active_from?: string;
active_to?: string;
description?: string;
is_active: boolean;
}
export class AccountsExporter {
private exportPath: string;
private accounts: IAccountExportRow[] = [];
constructor(exportPath: string) {
this.exportPath = exportPath;
}
/**
* Adds an account to the export
*/
public addAccount(account: IAccountDataExport): void {
const exportRow: IAccountExportRow = {
account_code: account.accountNumber,
name: account.accountName,
type: account.accountType,
class: account.accountClass,
parent: account.parentAccount,
skr_set: account.skrType,
tax_code_default: account.defaultTaxCode,
active_from: account.activeFrom ? this.formatDate(account.activeFrom) : undefined,
active_to: account.activeTo ? this.formatDate(account.activeTo) : undefined,
description: account.description,
is_active: account.isActive !== false
};
this.accounts.push(exportRow);
}
/**
* Exports accounts to CSV format
*/
public async exportToCSV(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'accounts.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Create CSV header
const headers = [
'account_code',
'name',
'type',
'class',
'parent',
'skr_set',
'tax_code_default',
'active_from',
'active_to',
'description',
'is_active'
];
let csvContent = headers.join(',') + '\n';
// Add account rows
for (const account of this.accounts) {
const row = [
this.escapeCSV(account.account_code),
this.escapeCSV(account.name),
this.escapeCSV(account.type),
account.class.toString(),
this.escapeCSV(account.parent || ''),
this.escapeCSV(account.skr_set),
this.escapeCSV(account.tax_code_default || ''),
this.escapeCSV(account.active_from || ''),
this.escapeCSV(account.active_to || ''),
this.escapeCSV(account.description || ''),
account.is_active.toString()
];
csvContent += row.join(',') + '\n';
}
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Exports accounts to JSON format (alternative)
*/
public async exportToJSON(): Promise<void> {
const jsonPath = path.join(this.exportPath, 'data', 'accounting', 'accounts.json');
await plugins.smartfile.fs.ensureDir(path.dirname(jsonPath));
const jsonData = {
schema_version: '1.0',
export_date: new Date().toISOString(),
accounts: this.accounts
};
await plugins.smartfile.memory.toFs(
JSON.stringify(jsonData, null, 2),
jsonPath
);
}
/**
* Escapes CSV values
*/
private escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
/**
* Formats a date to ISO date string
*/
private formatDate(date: Date | string): string {
if (typeof date === 'string') {
return date.split('T')[0];
}
return date.toISOString().split('T')[0];
}
/**
* Gets the number of accounts
*/
public getAccountCount(): number {
return this.accounts.length;
}
/**
* Clears the accounts list
*/
public clear(): void {
this.accounts = [];
}
}

270
ts/skr.export.balances.ts Normal file
View File

@@ -0,0 +1,270 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { IAccountBalance } from './skr.types.js';
// Extended interface for export with additional fields
export interface IAccountBalanceExport extends IAccountBalance {
openingBalance?: number;
transactionCount?: number;
}
export interface IBalanceExportRow {
account_code: string;
account_name: string;
fiscal_year: number;
period?: string;
opening_balance: string;
closing_balance: string;
debit_sum: string;
credit_sum: string;
balance: string;
transaction_count: number;
}
export class BalancesExporter {
private exportPath: string;
private balances: IBalanceExportRow[] = [];
private fiscalYear: number;
constructor(exportPath: string, fiscalYear: number) {
this.exportPath = exportPath;
this.fiscalYear = fiscalYear;
}
/**
* Adds a balance entry to the export
*/
public addBalance(
accountCode: string,
accountName: string,
balance: IAccountBalanceExport,
period?: string
): void {
const exportRow: IBalanceExportRow = {
account_code: accountCode,
account_name: accountName,
fiscal_year: this.fiscalYear,
period: period,
opening_balance: (balance.openingBalance || 0).toFixed(2),
closing_balance: balance.balance.toFixed(2),
debit_sum: balance.debitTotal.toFixed(2),
credit_sum: balance.creditTotal.toFixed(2),
balance: balance.balance.toFixed(2),
transaction_count: balance.transactionCount || 0
};
this.balances.push(exportRow);
}
/**
* Exports balances to CSV format
*/
public async exportToCSV(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'balances.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Create CSV header
const headers = [
'account_code',
'account_name',
'fiscal_year',
'period',
'opening_balance',
'closing_balance',
'debit_sum',
'credit_sum',
'balance',
'transaction_count'
];
let csvContent = headers.join(',') + '\n';
// Sort balances by account code
this.balances.sort((a, b) => a.account_code.localeCompare(b.account_code));
// Add balance rows
for (const balance of this.balances) {
const row = [
this.escapeCSV(balance.account_code),
this.escapeCSV(balance.account_name),
balance.fiscal_year.toString(),
this.escapeCSV(balance.period || ''),
balance.opening_balance,
balance.closing_balance,
balance.debit_sum,
balance.credit_sum,
balance.balance,
balance.transaction_count.toString()
];
csvContent += row.join(',') + '\n';
}
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Exports trial balance (Summen- und Saldenliste)
*/
public async exportTrialBalance(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'trial_balance.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Create CSV header for trial balance
const headers = [
'Konto',
'Bezeichnung',
'Anfangssaldo',
'Soll',
'Haben',
'Saldo',
'Endsaldo'
];
let csvContent = headers.join(',') + '\n';
// Add rows with German formatting
for (const balance of this.balances) {
const row = [
this.escapeCSV(balance.account_code),
this.escapeCSV(balance.account_name),
this.formatGermanNumber(parseFloat(balance.opening_balance)),
this.formatGermanNumber(parseFloat(balance.debit_sum)),
this.formatGermanNumber(parseFloat(balance.credit_sum)),
this.formatGermanNumber(parseFloat(balance.debit_sum) - parseFloat(balance.credit_sum)),
this.formatGermanNumber(parseFloat(balance.closing_balance))
];
csvContent += row.join(',') + '\n';
}
// Add totals row
const totalDebit = this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0);
const totalCredit = this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0);
csvContent += '\n';
csvContent += [
'SUMME',
'',
'',
this.formatGermanNumber(totalDebit),
this.formatGermanNumber(totalCredit),
this.formatGermanNumber(totalDebit - totalCredit),
''
].join(',') + '\n';
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Exports balances to JSON format
*/
public async exportToJSON(): Promise<void> {
const jsonPath = path.join(this.exportPath, 'data', 'accounting', 'balances.json');
await plugins.smartfile.fs.ensureDir(path.dirname(jsonPath));
const jsonData = {
schema_version: '1.0',
export_date: new Date().toISOString(),
fiscal_year: this.fiscalYear,
balances: this.balances,
totals: {
total_debit: this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0).toFixed(2),
total_credit: this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0).toFixed(2),
account_count: this.balances.length
}
};
await plugins.smartfile.memory.toFs(
JSON.stringify(jsonData, null, 2),
jsonPath
);
}
/**
* Generates balance summary for specific account classes
*/
public async exportClassSummary(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'class_summary.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Group balances by account class (first digit of account code)
const classSummary: { [key: string]: { debit: number; credit: number; balance: number } } = {};
for (const balance of this.balances) {
const accountClass = balance.account_code.charAt(0);
if (!classSummary[accountClass]) {
classSummary[accountClass] = { debit: 0, credit: 0, balance: 0 };
}
classSummary[accountClass].debit += parseFloat(balance.debit_sum);
classSummary[accountClass].credit += parseFloat(balance.credit_sum);
classSummary[accountClass].balance += parseFloat(balance.balance);
}
// Create CSV
let csvContent = 'Kontenklasse,Bezeichnung,Soll,Haben,Saldo\n';
const classNames: { [key: string]: string } = {
'0': 'Anlagevermögen',
'1': 'Umlaufvermögen',
'2': 'Eigenkapital',
'3': 'Fremdkapital',
'4': 'Betriebliche Erträge',
'5': 'Materialaufwand',
'6': 'Betriebsaufwand',
'7': 'Weitere Aufwendungen',
'8': 'Erträge',
'9': 'Abschlusskonten'
};
for (const [classNum, summary] of Object.entries(classSummary)) {
const row = [
classNum,
this.escapeCSV(classNames[classNum] || `Klasse ${classNum}`),
this.formatGermanNumber(summary.debit),
this.formatGermanNumber(summary.credit),
this.formatGermanNumber(summary.balance)
];
csvContent += row.join(',') + '\n';
}
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Escapes CSV values
*/
private escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
/**
* Formats number in German format (1.234,56)
*/
private formatGermanNumber(value: number): string {
return value.toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
/**
* Gets the number of balance entries
*/
public getBalanceCount(): number {
return this.balances.length;
}
/**
* Clears the balances list
*/
public clear(): void {
this.balances = [];
}
}

249
ts/skr.export.ledger.ts Normal file
View File

@@ -0,0 +1,249 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { ITransactionData, IJournalEntry, IJournalEntryLine } from './skr.types.js';
import { createWriteStream, type WriteStream } from 'fs';
// Extended interfaces for export with additional tracking fields
export interface ITransactionDataExport extends ITransactionData {
_id?: string;
postingDate?: Date;
currency?: string;
createdAt?: Date | string;
modifiedAt?: Date | string;
reversalOf?: string;
reversedBy?: string;
taxCode?: string;
project?: string;
vatAccount?: string;
}
export interface IJournalEntryExport extends IJournalEntry {
_id?: string;
postingDate?: Date;
currency?: string;
journal?: string;
createdAt?: Date | string;
modifiedAt?: Date | string;
reversalOf?: string;
reversedBy?: string;
}
export interface IJournalEntryLineExport extends IJournalEntryLine {
taxCode?: string;
project?: string;
}
export interface ILedgerEntry {
schema_version: string;
entry_id: string;
booking_date: string;
posting_date: string;
period?: string;
currency: string;
journal: string;
description: string;
reference?: string;
lines: ILedgerLine[];
document_refs?: IDocumentRef[];
created_at: string;
modified_at?: string;
user?: string;
reversal_of?: string;
reversed_by?: string;
}
export interface ILedgerLine {
posting_id: string;
account_code: string;
debit: string;
credit: string;
tax_code?: string;
cost_center?: string;
project?: string;
description?: string;
}
export interface IDocumentRef {
content_hash: string;
doc_role: 'invoice' | 'receipt' | 'contract' | 'bank-statement' | 'other';
doc_mime: string;
doc_original_name?: string;
}
export class LedgerExporter {
private exportPath: string;
private stream: WriteStream | null = null;
private entryCount: number = 0;
constructor(exportPath: string) {
this.exportPath = exportPath;
}
/**
* Initializes the NDJSON export stream
*/
public async initialize(): Promise<void> {
const ledgerPath = path.join(this.exportPath, 'data', 'accounting', 'ledger.ndjson');
await plugins.smartfile.fs.ensureDir(path.dirname(ledgerPath));
this.stream = createWriteStream(ledgerPath, {
encoding: 'utf8',
flags: 'w'
});
}
/**
* Exports a transaction as a ledger entry
*/
public async exportTransaction(transaction: ITransactionDataExport): Promise<void> {
if (!this.stream) {
throw new Error('Ledger exporter not initialized');
}
const entry: ILedgerEntry = {
schema_version: '1.0',
entry_id: transaction._id || plugins.smartunique.shortId(),
booking_date: this.formatDate(transaction.date),
posting_date: this.formatDate(transaction.postingDate || transaction.date),
currency: transaction.currency || 'EUR',
journal: 'GL',
description: transaction.description,
reference: transaction.reference,
lines: [],
created_at: transaction.createdAt ? new Date(transaction.createdAt).toISOString() : new Date().toISOString(),
modified_at: transaction.modifiedAt ? new Date(transaction.modifiedAt).toISOString() : undefined,
reversal_of: transaction.reversalOf,
reversed_by: transaction.reversedBy
};
// Add debit line
if (transaction.amount > 0) {
entry.lines.push({
posting_id: `${entry.entry_id}-1`,
account_code: transaction.debitAccount,
debit: transaction.amount.toFixed(2),
credit: '0.00',
tax_code: transaction.taxCode,
cost_center: transaction.costCenter,
project: transaction.project
});
// Add credit line
entry.lines.push({
posting_id: `${entry.entry_id}-2`,
account_code: transaction.creditAccount,
debit: '0.00',
credit: transaction.amount.toFixed(2)
});
}
// Add VAT lines if applicable
if (transaction.vatAmount && transaction.vatAmount > 0) {
entry.lines.push({
posting_id: `${entry.entry_id}-3`,
account_code: transaction.vatAccount || '1576', // Default VAT account
debit: transaction.vatAmount.toFixed(2),
credit: '0.00',
description: 'Vorsteuer'
});
}
await this.writeLine(entry);
}
/**
* Exports a journal entry
*/
public async exportJournalEntry(journalEntry: IJournalEntryExport): Promise<void> {
if (!this.stream) {
throw new Error('Ledger exporter not initialized');
}
const entry: ILedgerEntry = {
schema_version: '1.0',
entry_id: journalEntry._id || plugins.smartunique.shortId(),
booking_date: this.formatDate(journalEntry.date),
posting_date: this.formatDate(journalEntry.postingDate || journalEntry.date),
currency: journalEntry.currency || 'EUR',
journal: journalEntry.journal || 'GL',
description: journalEntry.description,
reference: journalEntry.reference,
lines: [],
created_at: journalEntry.createdAt ? new Date(journalEntry.createdAt).toISOString() : new Date().toISOString(),
modified_at: journalEntry.modifiedAt ? new Date(journalEntry.modifiedAt).toISOString() : undefined,
reversal_of: journalEntry.reversalOf,
reversed_by: journalEntry.reversedBy
};
// Convert journal entry lines
journalEntry.lines.forEach((line, index) => {
const extLine = line as IJournalEntryLineExport;
entry.lines.push({
posting_id: `${entry.entry_id}-${index + 1}`,
account_code: line.accountNumber,
debit: (line.debit || 0).toFixed(2),
credit: (line.credit || 0).toFixed(2),
tax_code: extLine.taxCode,
cost_center: line.costCenter,
project: extLine.project,
description: line.description
});
});
await this.writeLine(entry);
}
/**
* Writes a single NDJSON line
*/
private async writeLine(entry: ILedgerEntry): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.stream) {
reject(new Error('Stream not initialized'));
return;
}
const line = JSON.stringify(entry) + '\n';
this.stream.write(line, (error) => {
if (error) {
reject(error);
} else {
this.entryCount++;
resolve();
}
});
});
}
/**
* Formats a date to ISO date string
*/
private formatDate(date: Date | string): string {
if (typeof date === 'string') {
return date.split('T')[0];
}
return date.toISOString().split('T')[0];
}
/**
* Closes the export stream
*/
public async close(): Promise<number> {
return new Promise((resolve) => {
if (this.stream) {
this.stream.end(() => {
resolve(this.entryCount);
});
} else {
resolve(this.entryCount);
}
});
}
/**
* Gets the number of exported entries
*/
public getEntryCount(): number {
return this.entryCount;
}
}

601
ts/skr.export.pdf.ts Normal file
View File

@@ -0,0 +1,601 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { ITrialBalanceReport, IIncomeStatement, IBalanceSheet } from './skr.types.js';
export interface IPdfReportOptions {
companyName: string;
companyAddress?: string;
taxId?: string;
registrationNumber?: string;
fiscalYear: number;
dateFrom: Date;
dateTo: Date;
preparedBy?: string;
preparedDate?: Date;
}
export class PdfReportGenerator {
private exportPath: string;
private options: IPdfReportOptions;
private pdfInstance: plugins.smartpdf.SmartPdf | null = null;
constructor(exportPath: string, options: IPdfReportOptions) {
this.exportPath = exportPath;
this.options = options;
}
/**
* Initializes the PDF generator
*/
public async initialize(): Promise<void> {
this.pdfInstance = new plugins.smartpdf.SmartPdf();
await this.pdfInstance.start();
}
/**
* Generates the trial balance PDF report
*/
public async generateTrialBalancePdf(report: ITrialBalanceReport): Promise<Buffer> {
if (!this.pdfInstance) {
throw new Error('PDF generator not initialized');
}
const html = this.generateTrialBalanceHtml(report);
const pdfResult = await this.pdfInstance.getA4PdfResultForHtmlString(html);
return Buffer.from(pdfResult.buffer);
}
/**
* Generates the income statement PDF report
*/
public async generateIncomeStatementPdf(report: IIncomeStatement): Promise<Buffer> {
if (!this.pdfInstance) {
throw new Error('PDF generator not initialized');
}
const html = this.generateIncomeStatementHtml(report);
const pdfResult = await this.pdfInstance.getA4PdfResultForHtmlString(html);
return Buffer.from(pdfResult.buffer);
}
/**
* Generates the balance sheet PDF report
*/
public async generateBalanceSheetPdf(report: IBalanceSheet): Promise<Buffer> {
if (!this.pdfInstance) {
throw new Error('PDF generator not initialized');
}
const html = this.generateBalanceSheetHtml(report);
const pdfResult = await this.pdfInstance.getA4PdfResultForHtmlString(html);
return Buffer.from(pdfResult.buffer);
}
/**
* Generates the comprehensive Jahresabschluss PDF
*/
public async generateJahresabschlussPdf(
trialBalance: ITrialBalanceReport,
incomeStatement: IIncomeStatement,
balanceSheet: IBalanceSheet
): Promise<Buffer> {
if (!this.pdfInstance) {
throw new Error('PDF generator not initialized');
}
const html = this.generateJahresabschlussHtml(trialBalance, incomeStatement, balanceSheet);
const pdfResult = await this.pdfInstance.getA4PdfResultForHtmlString(html);
return Buffer.from(pdfResult.buffer);
}
/**
* Generates HTML for trial balance report
*/
private generateTrialBalanceHtml(report: ITrialBalanceReport): string {
const entries = report.entries || [];
const tableRows = entries.map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(0)}</td>
<td class="number">${this.formatGermanNumber(entry.debitBalance)}</td>
<td class="number">${this.formatGermanNumber(entry.creditBalance)}</td>
<td class="number">${this.formatGermanNumber(entry.netBalance)}</td>
</tr>
`).join('');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
${this.getBaseStyles()}
</style>
</head>
<body>
${this.generateHeader('Summen- und Saldenliste')}
<table class="report-table">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th>Anfangssaldo</th>
<th>Soll</th>
<th>Haben</th>
<th>Saldo</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3">Summe</td>
<td class="number">${this.formatGermanNumber(report.totalDebits)}</td>
<td class="number">${this.formatGermanNumber(report.totalCredits)}</td>
<td class="number">${this.formatGermanNumber(report.totalDebits - report.totalCredits)}</td>
</tr>
</tfoot>
</table>
${this.generateFooter()}
</body>
</html>
`;
}
/**
* Generates HTML for income statement report
*/
private generateIncomeStatementHtml(report: IIncomeStatement): string {
const revenueRows = (report.revenue || []).map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(entry.amount)}</td>
</tr>
`).join('');
const expenseRows = (report.expenses || []).map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(entry.amount)}</td>
</tr>
`).join('');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
${this.getBaseStyles()}
</style>
</head>
<body>
${this.generateHeader('Gewinn- und Verlustrechnung')}
<h2>Erträge</h2>
<table class="report-table">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
${revenueRows}
</tbody>
<tfoot>
<tr class="subtotal-row">
<td colspan="2">Summe Erträge</td>
<td class="number">${this.formatGermanNumber(report.totalRevenue)}</td>
</tr>
</tfoot>
</table>
<h2>Aufwendungen</h2>
<table class="report-table">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
${expenseRows}
</tbody>
<tfoot>
<tr class="subtotal-row">
<td colspan="2">Summe Aufwendungen</td>
<td class="number">${this.formatGermanNumber(report.totalExpenses)}</td>
</tr>
</tfoot>
</table>
<div class="result-section">
<h2>Ergebnis</h2>
<table class="summary-table">
<tr>
<td>Erträge</td>
<td class="number">${this.formatGermanNumber(report.totalRevenue)}</td>
</tr>
<tr>
<td>Aufwendungen</td>
<td class="number">- ${this.formatGermanNumber(report.totalExpenses)}</td>
</tr>
<tr class="total-row">
<td>${report.netIncome >= 0 ? 'Jahresüberschuss' : 'Jahresfehlbetrag'}</td>
<td class="number ${report.netIncome >= 0 ? 'positive' : 'negative'}">
${this.formatGermanNumber(report.netIncome)}
</td>
</tr>
</table>
</div>
${this.generateFooter()}
</body>
</html>
`;
}
/**
* Generates HTML for balance sheet report
*/
private generateBalanceSheetHtml(report: IBalanceSheet): string {
const assetRows = [...(report.assets.current || []), ...(report.assets.fixed || [])].map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(entry.amount)}</td>
</tr>
`).join('');
const liabilityRows = [...(report.liabilities.current || []), ...(report.liabilities.longTerm || [])].map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(entry.amount)}</td>
</tr>
`).join('');
const equityRows = (report.equity.entries || []).map(entry => `
<tr>
<td>${entry.accountNumber}</td>
<td>${entry.accountName}</td>
<td class="number">${this.formatGermanNumber(entry.amount)}</td>
</tr>
`).join('');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
${this.getBaseStyles()}
</style>
</head>
<body>
${this.generateHeader('Bilanz')}
<div class="balance-sheet">
<div class="aktiva">
<h2>Aktiva</h2>
<table class="report-table">
<thead>
<tr>
<th>Konto</th>
<th>Bezeichnung</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
${assetRows}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="2">Summe Aktiva</td>
<td class="number">${this.formatGermanNumber(report.assets.totalAssets)}</td>
</tr>
</tfoot>
</table>
</div>
<div class="passiva">
<h2>Passiva</h2>
<h3>Eigenkapital</h3>
<table class="report-table">
<tbody>
${equityRows}
</tbody>
<tfoot>
<tr class="subtotal-row">
<td colspan="2">Summe Eigenkapital</td>
<td class="number">${this.formatGermanNumber(report.equity.totalEquity)}</td>
</tr>
</tfoot>
</table>
<h3>Fremdkapital</h3>
<table class="report-table">
<tbody>
${liabilityRows}
</tbody>
<tfoot>
<tr class="subtotal-row">
<td colspan="2">Summe Fremdkapital</td>
<td class="number">${this.formatGermanNumber(report.liabilities.totalLiabilities)}</td>
</tr>
</tfoot>
</table>
<table class="summary-table">
<tr class="total-row">
<td>Summe Passiva</td>
<td class="number">${this.formatGermanNumber(report.liabilities.totalLiabilities + report.equity.totalEquity)}</td>
</tr>
</table>
</div>
</div>
${this.generateFooter()}
</body>
</html>
`;
}
/**
* Generates comprehensive Jahresabschluss HTML
*/
private generateJahresabschlussHtml(
trialBalance: ITrialBalanceReport,
incomeStatement: IIncomeStatement,
balanceSheet: IBalanceSheet
): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
${this.getBaseStyles()}
.page-break { page-break-after: always; }
.cover-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
.cover-page h1 { font-size: 36px; margin-bottom: 20px; }
.cover-page h2 { font-size: 24px; margin-bottom: 40px; }
.toc { margin-top: 50px; }
.toc h2 { margin-bottom: 20px; }
.toc ul { list-style: none; padding: 0; }
.toc li { margin: 10px 0; font-size: 16px; }
</style>
</head>
<body>
<div class="cover-page">
<h1>Jahresabschluss</h1>
<h2>${this.options.companyName}</h2>
<p>Geschäftsjahr ${this.options.fiscalYear}</p>
<p>${this.formatGermanDate(this.options.dateFrom)} bis ${this.formatGermanDate(this.options.dateTo)}</p>
<div class="toc">
<h2>Inhalt</h2>
<ul>
<li>1. Bilanz</li>
<li>2. Gewinn- und Verlustrechnung</li>
<li>3. Summen- und Saldenliste</li>
</ul>
</div>
</div>
<div class="page-break"></div>
${this.generateBalanceSheetHtml(balanceSheet)}
<div class="page-break"></div>
${this.generateIncomeStatementHtml(incomeStatement)}
<div class="page-break"></div>
${this.generateTrialBalanceHtml(trialBalance)}
</body>
</html>
`;
}
/**
* Generates the report header
*/
private generateHeader(reportTitle: string): string {
return `
<div class="header">
<h1>${this.options.companyName}</h1>
${this.options.companyAddress ? `<p>${this.options.companyAddress}</p>` : ''}
${this.options.taxId ? `<p>Steuernummer: ${this.options.taxId}</p>` : ''}
${this.options.registrationNumber ? `<p>Handelsregister: ${this.options.registrationNumber}</p>` : ''}
<hr>
<h2>${reportTitle}</h2>
<p>Periode: ${this.formatGermanDate(this.options.dateFrom)} bis ${this.formatGermanDate(this.options.dateTo)}</p>
</div>
`;
}
/**
* Generates the report footer
*/
private generateFooter(): string {
const preparedDate = this.options.preparedDate || new Date();
return `
<div class="footer">
<hr>
<p>Erstellt am: ${this.formatGermanDate(preparedDate)}</p>
${this.options.preparedBy ? `<p>Erstellt von: ${this.options.preparedBy}</p>` : ''}
<p class="disclaimer">
Dieser Bericht wurde automatisch generiert und ist Teil des revisionssicheren
Jahresabschluss-Exports gemäß GoBD.
</p>
</div>
`;
}
/**
* Gets the base CSS styles for all reports
*/
private getBaseStyles(): string {
return `
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 40px;
color: #333;
line-height: 1.6;
}
h1 { color: #2c3e50; margin-bottom: 10px; }
h2 { color: #34495e; margin-top: 30px; margin-bottom: 15px; }
h3 { color: #7f8c8d; margin-top: 20px; margin-bottom: 10px; }
.header {
text-align: center;
margin-bottom: 40px;
}
.footer {
margin-top: 50px;
text-align: center;
font-size: 12px;
color: #7f8c8d;
}
.disclaimer {
margin-top: 20px;
font-style: italic;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th {
background-color: #34495e;
color: white;
padding: 10px;
text-align: left;
font-weight: 600;
}
td {
padding: 8px;
border-bottom: 1px solid #ecf0f1;
}
tbody tr:hover {
background-color: #f8f9fa;
}
.number {
text-align: right;
font-family: 'Courier New', monospace;
}
.total-row {
font-weight: bold;
background-color: #ecf0f1;
}
.subtotal-row {
font-weight: 600;
background-color: #f8f9fa;
}
.positive {
color: #27ae60;
}
.negative {
color: #e74c3c;
}
.result-section {
margin-top: 40px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 5px;
}
.summary-table {
max-width: 500px;
margin: 20px auto;
}
.balance-sheet {
display: flex;
gap: 40px;
}
.aktiva, .passiva {
flex: 1;
}
@media print {
body { margin: 20px; }
.page-break { page-break-after: always; }
}
`;
}
/**
* Formats number in German format (1.234,56)
*/
private formatGermanNumber(value: number): string {
return value.toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
/**
* Formats date in German format (DD.MM.YYYY)
*/
private formatGermanDate(date: Date): string {
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
/**
* Saves a PDF report to the export directory
*/
public async savePdfReport(filename: string, pdfBuffer: Buffer): Promise<string> {
const reportsDir = path.join(this.exportPath, 'data', 'reports');
await plugins.smartfile.fs.ensureDir(reportsDir);
const filePath = path.join(reportsDir, filename);
await plugins.smartfile.memory.toFs(pdfBuffer, filePath);
return filePath;
}
/**
* Closes the PDF generator
*/
public async close(): Promise<void> {
if (this.pdfInstance) {
await this.pdfInstance.stop();
this.pdfInstance = null;
}
}
}

443
ts/skr.export.ts Normal file
View File

@@ -0,0 +1,443 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { IAccountData, ITransactionData, IJournalEntry, TSKRType } from './skr.types.js';
export interface IExportOptions {
exportPath: string;
fiscalYear: number;
dateFrom: Date;
dateTo: Date;
includeDocuments?: boolean;
generatePdfReports?: boolean;
signExport?: boolean;
timestampExport?: boolean;
companyInfo?: {
name: string;
taxId: string;
registrationNumber?: string;
address?: string;
};
}
export interface IExportMetadata {
exportVersion: string;
exportTimestamp: string;
generator: {
name: string;
version: string;
};
company?: {
name: string;
taxId: string;
registrationNumber?: string;
address?: string;
};
fiscalYear: number;
dateRange: {
from: string;
to: string;
};
skrType: TSKRType;
schemaVersion: string;
crypto: {
digestAlgorithms: string[];
signatureType?: string;
timestampPolicy?: string;
merkleTree: boolean;
};
options: {
packagedAs: 'bagit';
compression: 'none' | 'deflate';
deduplication: boolean;
};
}
export interface IBagItManifest {
[filePath: string]: string; // filePath -> SHA256 hash
}
export interface IDocumentIndex {
contentHash: string;
sizeBytes: number;
mimeType: string;
createdAt: string;
originalFilename?: string;
pdfaAvailable: boolean;
zugferdXml?: string;
retentionClass: string;
}
export class SkrExport {
private logger: plugins.smartlog.ConsoleLog;
private options: IExportOptions;
private exportDir: string;
private manifest: IBagItManifest = {};
private tagManifest: IBagItManifest = {};
constructor(options: IExportOptions) {
this.options = options;
this.logger = new plugins.smartlog.ConsoleLog();
this.exportDir = path.join(options.exportPath, `jahresabschluss_${options.fiscalYear}`);
}
/**
* Creates the BagIt directory structure for the export
*/
public async createBagItStructure(): Promise<void> {
this.logger.log('info', 'Creating BagIt directory structure...');
// Create main directories
await plugins.smartfile.fs.ensureDir(this.exportDir);
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'schemas'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'schemas', 'v1'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'metadata', 'signatures'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'accounting'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'accounting', 'ebilanz'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'documents'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'documents', 'by-hash'));
await plugins.smartfile.fs.ensureDir(path.join(this.exportDir, 'data', 'reports'));
// Create BagIt declaration file
await this.createBagItDeclaration();
// Create README
await this.createReadme();
this.logger.log('ok', 'BagIt structure created successfully');
}
/**
* Creates the bagit.txt declaration file
*/
private async createBagItDeclaration(): Promise<void> {
const bagitContent = `BagIt-Version: 1.0
Tag-File-Character-Encoding: UTF-8`;
const filePath = path.join(this.exportDir, 'bagit.txt');
await plugins.smartfile.memory.toFs(bagitContent, filePath);
// Add to tag manifest
const hash = await this.hashFile(filePath);
this.tagManifest['bagit.txt'] = hash;
}
/**
* Creates the README.txt file with Verfahrensdokumentation
*/
private async createReadme(): Promise<void> {
const readmeContent = `SKR Jahresabschluss Export - Verfahrensdokumentation
=====================================================
Dieses Archiv enthält einen revisionssicheren Export des Jahresabschlusses
gemäß den Grundsätzen ordnungsmäßiger Buchführung (GoBD).
Export-Datum: ${new Date().toISOString()}
Geschäftsjahr: ${this.options.fiscalYear}
Zeitraum: ${this.options.dateFrom.toISOString()} bis ${this.options.dateTo.toISOString()}
STRUKTUR DES ARCHIVS
--------------------
- /data/accounting/: Buchhaltungsdaten (Journale, Konten, Salden)
- /data/documents/: Belegdokumente (content-adressiert)
- /data/reports/: Finanzberichte (PDF/A-3)
- /data/metadata/: Export-Metadaten und Schemas
- /data/metadata/signatures/: Digitale Signaturen und Zeitstempel
INTEGRITÄTSSICHERUNG
--------------------
- Alle Dateien sind mit SHA-256 gehasht (siehe manifest-sha256.txt)
- Optional: Digitale Signatur (CAdES) über Manifest
- Optional: RFC 3161 Zeitstempel
AUFBEWAHRUNG
------------
Dieses Archiv muss gemäß § 147 AO für 10 Jahre revisionssicher aufbewahrt werden.
Empfohlen wird die Speicherung auf WORM-Medien.
REIMPORT
--------
Das Archiv kann mit der SKR-Software vollständig reimportiert werden.
Die Datenintegrität wird beim Import automatisch verifiziert.
COMPLIANCE
----------
- GoBD-konform
- E-Bilanz-fähig (XBRL)
- ZUGFeRD/Factur-X kompatibel
- PDF/A-3 für Langzeitarchivierung
© ${new Date().getFullYear()} ${this.options.companyInfo?.name || 'Export System'}`;
const filePath = path.join(this.exportDir, 'readme.txt');
await plugins.smartfile.memory.toFs(readmeContent, filePath);
// Add to tag manifest
const hash = await this.hashFile(filePath);
this.tagManifest['readme.txt'] = hash;
}
/**
* Creates the export metadata JSON file
*/
public async createExportMetadata(skrType: TSKRType): Promise<void> {
const metadata: IExportMetadata = {
exportVersion: '1.0.0',
exportTimestamp: new Date().toISOString(),
generator: {
name: '@fin.cx/skr',
version: '1.1.0' // Should be read from package.json
},
company: this.options.companyInfo,
fiscalYear: this.options.fiscalYear,
dateRange: {
from: this.options.dateFrom.toISOString(),
to: this.options.dateTo.toISOString()
},
skrType: skrType,
schemaVersion: '1.0',
crypto: {
digestAlgorithms: ['sha256'],
signatureType: this.options.signExport ? 'CAdES' : undefined,
timestampPolicy: this.options.timestampExport ? 'RFC3161' : undefined,
merkleTree: true
},
options: {
packagedAs: 'bagit',
compression: 'none',
deduplication: true
}
};
const filePath = path.join(this.exportDir, 'data', 'metadata', 'export.json');
await plugins.smartfile.memory.toFs(JSON.stringify(metadata, null, 2), filePath);
// Add to manifest
const hash = await this.hashFile(filePath);
this.manifest['data/metadata/export.json'] = hash;
}
/**
* Creates JSON schemas for the export data structures
*/
public async createSchemas(): Promise<void> {
// Ledger schema
const ledgerSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Ledger Entry",
"type": "object",
"properties": {
"schema_version": { "type": "string" },
"entry_id": { "type": "string", "format": "uuid" },
"booking_date": { "type": "string", "format": "date" },
"posting_date": { "type": "string", "format": "date" },
"currency": { "type": "string" },
"journal": { "type": "string" },
"description": { "type": "string" },
"lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"posting_id": { "type": "string" },
"account_code": { "type": "string" },
"debit": { "type": "string" },
"credit": { "type": "string" },
"tax_code": { "type": "string" },
"document_refs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content_hash": { "type": "string" },
"doc_role": { "type": "string" },
"mime": { "type": "string" }
}
}
}
},
"required": ["posting_id", "account_code", "debit", "credit"]
}
},
"created_at": { "type": "string", "format": "date-time" },
"user": { "type": "string" }
},
"required": ["schema_version", "entry_id", "booking_date", "lines"]
};
// Accounts schema
const accountsSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Accounts CSV",
"type": "object",
"properties": {
"account_code": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string" },
"parent": { "type": "string" },
"skr_set": { "type": "string" },
"tax_code_default": { "type": "string" },
"active_from": { "type": "string", "format": "date" },
"active_to": { "type": "string", "format": "date" }
},
"required": ["account_code", "name", "type", "skr_set"]
};
// Save schemas
const schemasDir = path.join(this.exportDir, 'data', 'metadata', 'schemas', 'v1');
await plugins.smartfile.memory.toFs(
JSON.stringify(ledgerSchema, null, 2),
path.join(schemasDir, 'ledger.schema.json')
);
this.manifest['data/metadata/schemas/v1/ledger.schema.json'] = await this.hashFile(
path.join(schemasDir, 'ledger.schema.json')
);
await plugins.smartfile.memory.toFs(
JSON.stringify(accountsSchema, null, 2),
path.join(schemasDir, 'accounts.schema.json')
);
this.manifest['data/metadata/schemas/v1/accounts.schema.json'] = await this.hashFile(
path.join(schemasDir, 'accounts.schema.json')
);
}
/**
* Writes the BagIt manifest files
*/
public async writeManifests(): Promise<void> {
// Write data manifest
let manifestContent = '';
for (const [filePath, hash] of Object.entries(this.manifest)) {
manifestContent += `${hash} ${filePath}\n`;
}
const manifestPath = path.join(this.exportDir, 'manifest-sha256.txt');
await plugins.smartfile.memory.toFs(manifestContent, manifestPath);
// Add manifest to tag manifest
this.tagManifest['manifest-sha256.txt'] = await this.hashFile(manifestPath);
// Write tag manifest
let tagManifestContent = '';
for (const [filePath, hash] of Object.entries(this.tagManifest)) {
tagManifestContent += `${hash} ${filePath}\n`;
}
await plugins.smartfile.memory.toFs(
tagManifestContent,
path.join(this.exportDir, 'tagmanifest-sha256.txt')
);
}
/**
* Calculates SHA-256 hash of a file
*/
private async hashFile(filePath: string): Promise<string> {
const fileContent = await plugins.smartfile.fs.toBuffer(filePath);
return await plugins.smarthash.sha256FromBuffer(fileContent);
}
/**
* Stores a document in content-addressed storage
*/
public async storeDocument(content: Buffer, originalFilename?: string): Promise<string> {
const hash = await plugins.smarthash.sha256FromBuffer(content);
// Create path based on hash (first 2 chars as directory)
const hashPrefix = hash.substring(0, 2);
const hashDir = path.join(this.exportDir, 'data', 'documents', 'by-hash', hashPrefix);
await plugins.smartfile.fs.ensureDir(hashDir);
const docPath = path.join(hashDir, hash);
// Only store if not already exists (deduplication)
if (!(await plugins.smartfile.fs.fileExists(docPath))) {
await plugins.smartfile.memory.toFs(content, docPath);
this.manifest[`data/documents/by-hash/${hashPrefix}/${hash}`] = hash;
}
return hash;
}
/**
* Creates a Merkle tree from all file hashes
*/
public async createMerkleTree(): Promise<string> {
const leaves = Object.values(this.manifest).map(hash =>
Buffer.from(hash, 'hex')
);
// Create a sync hash function wrapper for MerkleTree
const hashFn = (data: Buffer) => {
// Convert async to sync by using crypto directly
const crypto = require('crypto');
return crypto.createHash('sha256').update(data).digest();
};
const tree = new plugins.MerkleTree(leaves, hashFn, {
sortPairs: true
});
const root = tree.getRoot().toString('hex');
// Save Merkle tree data
const merkleData = {
root: root,
leaves: Object.entries(this.manifest).map(([path, hash]) => ({
path,
hash
})),
timestamp: new Date().toISOString()
};
const merklePath = path.join(this.exportDir, 'data', 'metadata', 'merkle-tree.json');
await plugins.smartfile.memory.toFs(JSON.stringify(merkleData, null, 2), merklePath);
this.manifest['data/metadata/merkle-tree.json'] = await this.hashFile(merklePath);
return root;
}
/**
* Validates the BagIt structure
*/
public async validateBagIt(): Promise<boolean> {
this.logger.log('info', 'Validating BagIt structure...');
// Check required files exist
const requiredFiles = [
'bagit.txt',
'manifest-sha256.txt',
'tagmanifest-sha256.txt',
'readme.txt'
];
for (const file of requiredFiles) {
const filePath = path.join(this.exportDir, file);
if (!(await plugins.smartfile.fs.fileExists(filePath))) {
this.logger.log('error', `Required file missing: ${file}`);
return false;
}
}
// Verify all manifest entries
for (const [relPath, expectedHash] of Object.entries(this.manifest)) {
const fullPath = path.join(this.exportDir, relPath);
if (!(await plugins.smartfile.fs.fileExists(fullPath))) {
this.logger.log('error', `Manifest file missing: ${relPath}`);
return false;
}
const actualHash = await this.hashFile(fullPath);
if (actualHash !== expectedHash) {
this.logger.log('error', `Hash mismatch for ${relPath}`);
return false;
}
}
this.logger.log('ok', 'BagIt validation successful');
return true;
}
}

581
ts/skr.invoice.adapter.ts Normal file
View File

@@ -0,0 +1,581 @@
import * as plugins from './plugins.js';
import type {
IInvoice,
IInvoiceLine,
IInvoiceParty,
IVATCategory,
IValidationResult,
TInvoiceFormat,
TInvoiceDirection,
TTaxScenario,
IAllowanceCharge,
IPaymentTerms
} from './skr.invoice.entity.js';
/**
* Adapter for @fin.cx/einvoice library
* Handles parsing, validation, and format conversion of e-invoices
*/
export class InvoiceAdapter {
private logger: plugins.smartlog.ConsoleLog;
constructor() {
this.logger = new plugins.smartlog.ConsoleLog();
}
private readonly MAX_XML_SIZE = 10 * 1024 * 1024; // 10MB max
private readonly MAX_PDF_SIZE = 50 * 1024 * 1024; // 50MB max
/**
* Parse an invoice from file or buffer
*/
public async parseInvoice(
file: Buffer | string,
direction: TInvoiceDirection
): Promise<IInvoice> {
try {
// Validate input size
if (Buffer.isBuffer(file)) {
if (file.length > this.MAX_XML_SIZE) {
throw new Error(`Invoice file too large: ${file.length} bytes (max ${this.MAX_XML_SIZE} bytes)`);
}
} else if (typeof file === 'string' && file.length > this.MAX_XML_SIZE) {
throw new Error(`Invoice XML too large: ${file.length} characters (max ${this.MAX_XML_SIZE} characters)`);
}
// Parse the invoice using @fin.cx/einvoice
let einvoice;
if (typeof file === 'string') {
einvoice = await plugins.einvoice.EInvoice.fromXml(file);
} else {
// Convert buffer to string first
const xmlString = file.toString('utf-8');
einvoice = await plugins.einvoice.EInvoice.fromXml(xmlString);
}
// Get detected format
const format = this.mapEInvoiceFormat(einvoice.format || 'xrechnung');
// Validate the invoice (takes ~2.2ms)
const validationResult = await this.validateInvoice(einvoice);
// Extract invoice data
const invoiceData = einvoice.toObject();
// Map to internal invoice model
const invoice = await this.mapToInternalModel(
invoiceData,
format,
direction,
validationResult
);
// Store original XML content
invoice.xmlContent = einvoice.getXml();
// Calculate content hash
invoice.contentHash = await this.calculateContentHash(invoice.xmlContent);
// Classify tax scenario
invoice.taxScenario = this.classifyTaxScenario(invoice);
return invoice;
} catch (error) {
this.logger.log('error', `Failed to parse invoice: ${error}`);
throw new Error(`Invoice parsing failed: ${error.message}`);
}
}
/**
* Validate an invoice using multi-level validation
*/
private async validateInvoice(einvoice: any): Promise<IValidationResult> {
// Perform multi-level validation
const validationResult = await einvoice.validate();
// Parse validation results into our structure
const syntaxResult = {
isValid: validationResult.syntax?.valid !== false,
errors: validationResult.syntax?.errors || [],
warnings: validationResult.syntax?.warnings || []
};
const semanticResult = {
isValid: validationResult.semantic?.valid !== false,
errors: validationResult.semantic?.errors || [],
warnings: validationResult.semantic?.warnings || []
};
const businessResult = {
isValid: validationResult.business?.valid !== false,
errors: validationResult.business?.errors || [],
warnings: validationResult.business?.warnings || []
};
const countryResult = {
isValid: validationResult.country?.valid !== false,
errors: validationResult.country?.errors || [],
warnings: validationResult.country?.warnings || []
};
return {
isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
syntax: {
valid: syntaxResult.isValid,
errors: syntaxResult.errors || [],
warnings: syntaxResult.warnings || []
},
semantic: {
valid: semanticResult.isValid,
errors: semanticResult.errors || [],
warnings: semanticResult.warnings || []
},
businessRules: {
valid: businessResult.isValid,
errors: businessResult.errors || [],
warnings: businessResult.warnings || []
},
countrySpecific: {
valid: countryResult.isValid,
errors: countryResult.errors || [],
warnings: countryResult.warnings || []
},
validatedAt: new Date(),
validatorVersion: '5.1.4'
};
}
/**
* Map EN16931 Business Terms to internal invoice model
*/
private async mapToInternalModel(
businessTerms: any,
format: TInvoiceFormat,
direction: TInvoiceDirection,
validationResult: IValidationResult
): Promise<IInvoice> {
const invoice: IInvoice = {
// Identity
id: plugins.smartunique.shortId(),
direction,
format,
// EN16931 Business Terms
invoiceNumber: businessTerms.BT1_InvoiceNumber,
issueDate: new Date(businessTerms.BT2_IssueDate),
invoiceTypeCode: businessTerms.BT3_InvoiceTypeCode || '380',
currencyCode: businessTerms.BT5_CurrencyCode || 'EUR',
taxCurrencyCode: businessTerms.BT6_TaxCurrencyCode,
taxPointDate: businessTerms.BT7_TaxPointDate ? new Date(businessTerms.BT7_TaxPointDate) : undefined,
paymentDueDate: businessTerms.BT9_PaymentDueDate ? new Date(businessTerms.BT9_PaymentDueDate) : undefined,
buyerReference: businessTerms.BT10_BuyerReference,
projectReference: businessTerms.BT11_ProjectReference,
contractReference: businessTerms.BT12_ContractReference,
orderReference: businessTerms.BT13_OrderReference,
sellerOrderReference: businessTerms.BT14_SellerOrderReference,
// Parties
supplier: this.mapParty(businessTerms.BG4_Seller),
customer: this.mapParty(businessTerms.BG7_Buyer),
payee: businessTerms.BG10_Payee ? this.mapParty(businessTerms.BG10_Payee) : undefined,
// Line items
lines: this.mapInvoiceLines(businessTerms.BG25_InvoiceLines || []),
// Allowances and charges
allowances: this.mapAllowancesCharges(businessTerms.BG20_DocumentAllowances || [], true),
charges: this.mapAllowancesCharges(businessTerms.BG21_DocumentCharges || [], false),
// Amounts
lineNetAmount: parseFloat(businessTerms.BT106_SumOfLineNetAmounts || 0),
allowanceTotalAmount: parseFloat(businessTerms.BT107_AllowanceTotalAmount || 0),
chargeTotalAmount: parseFloat(businessTerms.BT108_ChargeTotalAmount || 0),
taxExclusiveAmount: parseFloat(businessTerms.BT109_TaxExclusiveAmount || 0),
taxInclusiveAmount: parseFloat(businessTerms.BT112_TaxInclusiveAmount || 0),
prepaidAmount: parseFloat(businessTerms.BT113_PrepaidAmount || 0),
payableAmount: parseFloat(businessTerms.BT115_PayableAmount || 0),
// VAT breakdown
vatBreakdown: this.mapVATBreakdown(businessTerms.BG23_VATBreakdown || []),
totalVATAmount: parseFloat(businessTerms.BT110_TotalVATAmount || 0),
// Payment
paymentTerms: this.mapPaymentTerms(businessTerms),
paymentMeans: this.mapPaymentMeans(businessTerms.BG16_PaymentInstructions),
// Notes
invoiceNote: businessTerms.BT22_InvoiceNote,
// Processing metadata
status: 'validated',
// Storage (to be filled later)
contentHash: '',
// Validation
validationResult,
// Audit trail
createdAt: new Date(),
createdBy: 'system',
// Metadata
metadata: {
importedAt: new Date(),
parserVersion: '5.1.4',
originalFormat: format
}
};
return invoice;
}
/**
* Map party information
*/
private mapParty(partyData: any): IInvoiceParty {
if (!partyData) {
return {
id: '',
name: '',
address: { countryCode: 'DE' }
};
}
return {
id: partyData.BT29_SellerID || partyData.BT46_BuyerID || plugins.smartunique.shortId(),
name: partyData.BT27_SellerName || partyData.BT44_BuyerName || '',
address: {
street: partyData.BT35_SellerStreet || partyData.BT50_BuyerStreet,
city: partyData.BT37_SellerCity || partyData.BT52_BuyerCity,
postalCode: partyData.BT38_SellerPostalCode || partyData.BT53_BuyerPostalCode,
countryCode: partyData.BT40_SellerCountryCode || partyData.BT55_BuyerCountryCode || 'DE'
},
vatId: partyData.BT31_SellerVATID || partyData.BT48_BuyerVATID,
taxId: partyData.BT32_SellerTaxID || partyData.BT47_BuyerTaxID,
email: partyData.BT34_SellerEmail || partyData.BT49_BuyerEmail,
phone: partyData.BT33_SellerPhone,
bankAccount: this.mapBankAccount(partyData)
};
}
/**
* Map bank account information
*/
private mapBankAccount(partyData: any): IInvoiceParty['bankAccount'] | undefined {
if (!partyData?.BT84_PaymentAccountID) {
return undefined;
}
return {
iban: partyData.BT84_PaymentAccountID,
bic: partyData.BT86_PaymentServiceProviderID,
accountHolder: partyData.BT85_PaymentAccountName
};
}
/**
* Map invoice lines
*/
private mapInvoiceLines(linesData: any[]): IInvoiceLine[] {
return linesData.map((line, index) => ({
lineNumber: index + 1,
description: line.BT154_ItemDescription || '',
quantity: parseFloat(line.BT129_Quantity || 1),
unitPrice: parseFloat(line.BT146_NetPrice || 0),
netAmount: parseFloat(line.BT131_LineNetAmount || 0),
vatCategory: this.mapVATCategory(line.BT151_ItemVATCategory, line.BT152_ItemVATRate),
vatAmount: parseFloat(line.lineVATAmount || 0),
grossAmount: parseFloat(line.BT131_LineNetAmount || 0) + parseFloat(line.lineVATAmount || 0),
productCode: line.BT155_ItemSellerID,
allowances: this.mapLineAllowancesCharges(line.BG27_LineAllowances || [], true),
charges: this.mapLineAllowancesCharges(line.BG28_LineCharges || [], false)
}));
}
/**
* Map VAT category
*/
private mapVATCategory(categoryCode: string, rate: string | number): IVATCategory {
const vatRate = typeof rate === 'string' ? parseFloat(rate) : rate;
return {
code: categoryCode || 'S',
rate: vatRate || 0,
exemptionReason: this.getExemptionReason(categoryCode)
};
}
/**
* Get exemption reason for VAT category
*/
private getExemptionReason(categoryCode: string): string | undefined {
const exemptionReasons: Record<string, string> = {
'E': 'Tax exempt',
'Z': 'Zero rated',
'AE': 'Reverse charge (§13b UStG)',
'K': 'Intra-EU supply',
'G': 'Export outside EU',
'O': 'Outside scope of tax',
'S': undefined // Standard rate, no exemption
};
return exemptionReasons[categoryCode];
}
/**
* Map VAT breakdown
*/
private mapVATBreakdown(vatBreakdown: any[]): IInvoice['vatBreakdown'] {
return vatBreakdown.map(vat => ({
vatCategory: this.mapVATCategory(vat.BT118_VATCategory, vat.BT119_VATRate),
taxableAmount: parseFloat(vat.BT116_TaxableAmount || 0),
taxAmount: parseFloat(vat.BT117_TaxAmount || 0)
}));
}
/**
* Map allowances and charges
*/
private mapAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
return data.map(item => ({
reason: item.BT97_AllowanceReason || item.BT104_ChargeReason || '',
amount: parseFloat(item.BT92_AllowanceAmount || item.BT99_ChargeAmount || 0),
percentage: item.BT94_AllowancePercentage || item.BT101_ChargePercentage,
vatCategory: item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory
? this.mapVATCategory(
item.BT95_AllowanceVATCategory || item.BT102_ChargeVATCategory,
item.BT96_AllowanceVATRate || item.BT103_ChargeVATRate
)
: undefined,
vatAmount: parseFloat(item.allowanceVATAmount || item.chargeVATAmount || 0)
}));
}
/**
* Map line-level allowances and charges
*/
private mapLineAllowancesCharges(data: any[], isAllowance: boolean): IAllowanceCharge[] {
return data.map(item => ({
reason: item.BT140_LineAllowanceReason || item.BT145_LineChargeReason || '',
amount: parseFloat(item.BT136_LineAllowanceAmount || item.BT141_LineChargeAmount || 0),
percentage: item.BT138_LineAllowancePercentage || item.BT143_LineChargePercentage
}));
}
/**
* Map payment terms
*/
private mapPaymentTerms(businessTerms: any): IPaymentTerms | undefined {
if (!businessTerms.BT9_PaymentDueDate && !businessTerms.BT20_PaymentTerms) {
return undefined;
}
const paymentTerms: IPaymentTerms = {
dueDate: businessTerms.BT9_PaymentDueDate
? new Date(businessTerms.BT9_PaymentDueDate)
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Default 30 days
paymentTermsNote: businessTerms.BT20_PaymentTerms
};
// Parse skonto from payment terms note if present
if (businessTerms.BT20_PaymentTerms) {
paymentTerms.skonto = this.parseSkontoTerms(businessTerms.BT20_PaymentTerms);
}
return paymentTerms;
}
/**
* Parse skonto terms from payment terms text
*/
private parseSkontoTerms(paymentTermsText: string): IPaymentTerms['skonto'] {
const skontoTerms: IPaymentTerms['skonto'] = [];
// Common German skonto patterns:
// "2% Skonto bei Zahlung innerhalb von 10 Tagen"
// "3% bei Zahlung bis 8 Tage, 2% bis 14 Tage"
const skontoPattern = /(\d+(?:\.\d+)?)\s*%.*?(\d+)\s*(?:Tag|Day)/gi;
let match;
while ((match = skontoPattern.exec(paymentTermsText)) !== null) {
skontoTerms.push({
percentage: parseFloat(match[1]),
days: parseInt(match[2]),
baseAmount: 0 // To be calculated based on invoice amount
});
}
return skontoTerms.length > 0 ? skontoTerms : undefined;
}
/**
* Map payment means
*/
private mapPaymentMeans(paymentInstructions: any): IInvoice['paymentMeans'] | undefined {
if (!paymentInstructions) {
return undefined;
}
return {
code: paymentInstructions.BT81_PaymentMeansCode || '30', // 30 = Bank transfer
account: paymentInstructions.BT84_PaymentAccountID
? {
iban: paymentInstructions.BT84_PaymentAccountID,
bic: paymentInstructions.BT86_PaymentServiceProviderID,
accountHolder: paymentInstructions.BT85_PaymentAccountName
}
: undefined
};
}
/**
* Classify tax scenario based on invoice data
*/
private classifyTaxScenario(invoice: IInvoice): TTaxScenario {
const supplierCountry = invoice.supplier.address.countryCode;
const customerCountry = invoice.customer.address.countryCode;
const hasVAT = invoice.totalVATAmount > 0;
const vatCategories = invoice.vatBreakdown.map(vb => vb.vatCategory.code);
// Reverse charge
if (vatCategories.includes('AE')) {
return 'reverse_charge';
}
// Small business exemption
if (vatCategories.includes('E') && invoice.invoiceNote?.includes('§19')) {
return 'small_business';
}
// Export outside EU
if (vatCategories.includes('G') || (!this.isEUCountry(customerCountry) && supplierCountry === 'DE')) {
return 'export';
}
// Intra-EU transactions
if (supplierCountry !== customerCountry && this.isEUCountry(supplierCountry) && this.isEUCountry(customerCountry)) {
if (invoice.direction === 'outbound') {
return 'intra_eu_supply';
} else {
return 'intra_eu_acquisition';
}
}
// Domestic exempt
if (!hasVAT && supplierCountry === 'DE' && customerCountry === 'DE') {
return 'domestic_exempt';
}
// Default: Domestic taxed
return 'domestic_taxed';
}
/**
* Check if country is in EU
*/
private isEUCountry(countryCode: string): boolean {
const euCountries = [
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'
];
return euCountries.includes(countryCode);
}
/**
* Map e-invoice format from library format
*/
private mapEInvoiceFormat(format: string): TInvoiceFormat {
const formatMap: Record<string, TInvoiceFormat> = {
'xrechnung': 'xrechnung',
'zugferd': 'zugferd',
'factur-x': 'facturx',
'facturx': 'facturx',
'peppol': 'peppol',
'ubl': 'ubl'
};
return formatMap[format.toLowerCase()] || 'xrechnung';
}
/**
* Calculate content hash for the invoice
*/
private async calculateContentHash(xmlContent: string): Promise<string> {
const hash = await plugins.smarthash.sha256FromString(xmlContent);
return hash;
}
/**
* Convert invoice to different format
*/
public async convertFormat(
invoice: IInvoice,
targetFormat: TInvoiceFormat
): Promise<string> {
try {
// Load from existing XML
const einvoice = await plugins.einvoice.EInvoice.fromXml(invoice.xmlContent!);
// Convert to target format (takes ~0.6ms)
const convertedXml = await einvoice.exportXml(targetFormat as any);
return convertedXml;
} catch (error) {
this.logger.log('error', `Failed to convert invoice format: ${error}`);
throw new Error(`Format conversion failed: ${error.message}`);
}
}
/**
* Generate invoice from internal data
*/
public async generateInvoice(
invoiceData: Partial<IInvoice>,
format: TInvoiceFormat
): Promise<{ xml: string; pdf?: Buffer }> {
try {
// Create a new invoice instance
const einvoice = new plugins.einvoice.EInvoice();
// Set invoice data
const businessTerms = this.mapToBusinessTerms(invoiceData);
Object.assign(einvoice, businessTerms);
// Generate XML in requested format
const xml = await einvoice.exportXml(format as any);
// Generate PDF if ZUGFeRD or Factur-X
let pdf: Buffer | undefined;
if (format === 'zugferd' || format === 'facturx') {
// Access the pdf property if it exists
if (einvoice.pdf && einvoice.pdf.buffer) {
pdf = Buffer.from(einvoice.pdf.buffer);
}
}
return { xml, pdf };
} catch (error) {
this.logger.log('error', `Failed to generate invoice: ${error}`);
throw new Error(`Invoice generation failed: ${error.message}`);
}
}
/**
* Map internal invoice to EN16931 Business Terms
*/
private mapToBusinessTerms(invoice: Partial<IInvoice>): any {
return {
BT1_InvoiceNumber: invoice.invoiceNumber,
BT2_IssueDate: invoice.issueDate?.toISOString(),
BT3_InvoiceTypeCode: invoice.invoiceTypeCode || '380',
BT5_CurrencyCode: invoice.currencyCode || 'EUR',
BT7_TaxPointDate: invoice.taxPointDate?.toISOString(),
BT9_PaymentDueDate: invoice.paymentDueDate?.toISOString(),
// Map other Business Terms...
// This would be a comprehensive mapping in production
};
}
}

738
ts/skr.invoice.booking.ts Normal file
View File

@@ -0,0 +1,738 @@
import * as plugins from './plugins.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
import type {
IInvoice,
IInvoiceLine,
IBookingRules,
IBookingInfo,
TTaxScenario,
IPaymentInfo
} from './skr.invoice.entity.js';
/**
* Options for booking an invoice
*/
export interface IBookingOptions {
autoBook?: boolean;
confidenceThreshold?: number;
bookingDate?: Date;
bookingReference?: string;
skipValidation?: boolean;
}
/**
* Result of booking an invoice
*/
export interface IBookingResult {
success: boolean;
journalEntry?: JournalEntry;
bookingInfo?: IBookingInfo;
confidence: number;
warnings?: string[];
errors?: string[];
}
/**
* Automatic booking engine for invoices
* Creates journal entries from invoice data based on SKR mapping rules
*/
export class InvoiceBookingEngine {
private logger: plugins.smartlog.ConsoleLog;
private skrType: TSKRType;
private mapper: SKRInvoiceMapper;
constructor(skrType: TSKRType) {
this.skrType = skrType;
this.mapper = new SKRInvoiceMapper(skrType);
this.logger = new plugins.smartlog.ConsoleLog();
}
/**
* Book an invoice to the ledger
*/
public async bookInvoice(
invoice: IInvoice,
bookingRules?: Partial<IBookingRules>,
options?: IBookingOptions
): Promise<IBookingResult> {
try {
// Get complete booking rules
const rules = this.mapper.mapInvoiceToSKR(invoice, bookingRules);
// Calculate confidence
const confidence = this.mapper.calculateConfidence(invoice, rules);
// Check if auto-booking is allowed
if (options?.autoBook && confidence < (options.confidenceThreshold || 80)) {
return {
success: false,
confidence,
warnings: [`Confidence score ${confidence}% is below threshold ${options.confidenceThreshold || 80}%`]
};
}
// Validate invoice before booking
if (!options?.skipValidation) {
const validationErrors = this.validateInvoice(invoice);
if (validationErrors.length > 0) {
return {
success: false,
confidence,
errors: validationErrors
};
}
}
// Build journal entry
const journalEntry = await this.buildJournalEntry(invoice, rules, options);
// Create booking info
const bookingInfo: IBookingInfo = {
journalEntryId: journalEntry.id,
transactionIds: journalEntry.transactionIds || [],
bookedAt: new Date(),
bookedBy: 'system',
bookingRules: {
vendorAccount: rules.vendorControlAccount,
customerAccount: rules.customerControlAccount,
expenseAccounts: this.getUsedExpenseAccounts(invoice, rules),
revenueAccounts: this.getUsedRevenueAccounts(invoice, rules),
vatAccounts: this.getUsedVATAccounts(invoice, rules)
},
confidence,
autoBooked: options?.autoBook || false
};
// Post the journal entry
// TODO: When MongoDB transactions are available, wrap this in a transaction
// Example: await db.withTransaction(async (session) => { ... })
try {
await journalEntry.validate();
await journalEntry.post();
// Mark invoice as posted if we have a reference to it
if (invoice.status !== 'posted') {
invoice.status = 'posted';
}
} catch (postError) {
this.logger.log('error', `Failed to post journal entry: ${postError}`);
throw postError; // Re-throw to trigger rollback when transactions are available
}
return {
success: true,
journalEntry,
bookingInfo,
confidence,
warnings: this.generateWarnings(invoice, rules)
};
} catch (error) {
this.logger.log('error', `Failed to book invoice: ${error}`);
return {
success: false,
confidence: 0,
errors: [`Booking failed: ${error.message}`]
};
}
}
/**
* Build journal entry from invoice
*/
private async buildJournalEntry(
invoice: IInvoice,
rules: IBookingRules,
options?: IBookingOptions
): Promise<JournalEntry> {
const lines: IJournalEntryLine[] = [];
const isInbound = invoice.direction === 'inbound';
const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
// Determine if we need to reverse the normal booking direction
const reverseDirection = isCredit;
if (isInbound) {
// Inbound invoice (AP)
lines.push(...this.buildAPEntry(invoice, rules, reverseDirection));
} else {
// Outbound invoice (AR)
lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
}
// Create journal entry
const journalData: IJournalEntry = {
date: options?.bookingDate || invoice.issueDate,
description: this.buildDescription(invoice),
reference: options?.bookingReference || invoice.invoiceNumber,
lines,
skrType: this.skrType
};
const journalEntry = new JournalEntry(journalData);
return journalEntry;
}
/**
* Build AP (Accounts Payable) journal entry lines
*/
private buildAPEntry(
invoice: IInvoice,
rules: IBookingRules,
reverseDirection: boolean
): IJournalEntryLine[] {
const lines: IJournalEntryLine[] = [];
// Group lines by account
const accountGroups = this.groupLinesByAccount(invoice, rules);
// Create expense/asset entries
for (const [accountNumber, group] of Object.entries(accountGroups)) {
const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
if (reverseDirection) {
// Credit note: credit expense account
lines.push({
accountNumber,
credit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
});
} else {
// Regular invoice: debit expense account
lines.push({
accountNumber,
debit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
});
}
}
// Create VAT entries
const vatLines = this.buildVATLines(invoice, rules, 'input', reverseDirection);
lines.push(...vatLines);
// Create vendor control account entry
const controlAccount = this.mapper.getControlAccount(invoice, rules);
const totalAmount = Math.abs(invoice.payableAmount);
if (reverseDirection) {
// Credit note: debit vendor account
lines.push({
accountNumber: controlAccount,
debit: totalAmount,
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`
});
} else {
// Regular invoice: credit vendor account
lines.push({
accountNumber: controlAccount,
credit: totalAmount,
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`
});
}
return lines;
}
/**
* Build AR (Accounts Receivable) journal entry lines
*/
private buildAREntry(
invoice: IInvoice,
rules: IBookingRules,
reverseDirection: boolean
): IJournalEntryLine[] {
const lines: IJournalEntryLine[] = [];
// Group lines by account
const accountGroups = this.groupLinesByAccount(invoice, rules);
// Create revenue entries
for (const [accountNumber, group] of Object.entries(accountGroups)) {
const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
if (reverseDirection) {
// Credit note: debit revenue account
lines.push({
accountNumber,
debit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
});
} else {
// Regular invoice: credit revenue account
lines.push({
accountNumber,
credit: Math.abs(amount),
description: this.getAccountDescription(accountNumber, group)
});
}
}
// Create VAT entries
const vatLines = this.buildVATLines(invoice, rules, 'output', reverseDirection);
lines.push(...vatLines);
// Create customer control account entry
const controlAccount = this.mapper.getControlAccount(invoice, rules);
const totalAmount = Math.abs(invoice.payableAmount);
if (reverseDirection) {
// Credit note: credit customer account
lines.push({
accountNumber: controlAccount,
credit: totalAmount,
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`
});
} else {
// Regular invoice: debit customer account
lines.push({
accountNumber: controlAccount,
debit: totalAmount,
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`
});
}
return lines;
}
/**
* Build VAT lines
*/
private buildVATLines(
invoice: IInvoice,
rules: IBookingRules,
direction: 'input' | 'output',
reverseDirection: boolean
): IJournalEntryLine[] {
const lines: IJournalEntryLine[] = [];
const taxScenario = invoice.taxScenario || 'domestic_taxed';
// Handle reverse charge specially
if (taxScenario === 'reverse_charge') {
return this.buildReverseChargeVATLines(invoice, rules);
}
// Standard VAT booking
for (const vatBreak of invoice.vatBreakdown) {
if (vatBreak.taxAmount === 0) continue;
const vatAccount = this.mapper.getVATAccount(
vatBreak.vatCategory,
direction,
taxScenario
);
const amount = Math.abs(vatBreak.taxAmount);
const description = `VAT ${vatBreak.vatCategory.rate}%`;
if (direction === 'input') {
// Input VAT (Vorsteuer)
if (reverseDirection) {
lines.push({ accountNumber: vatAccount, credit: amount, description });
} else {
lines.push({ accountNumber: vatAccount, debit: amount, description });
}
} else {
// Output VAT (Umsatzsteuer)
if (reverseDirection) {
lines.push({ accountNumber: vatAccount, debit: amount, description });
} else {
lines.push({ accountNumber: vatAccount, credit: amount, description });
}
}
}
return lines;
}
/**
* Calculate VAT amount from taxable amount and rate
*/
private calculateVAT(taxableAmount: number, rate: number): number {
return Math.round(taxableAmount * rate / 100 * 100) / 100; // Round to 2 decimals
}
/**
* Calculate effective VAT rate for the invoice (weighted average)
*/
private calculateEffectiveVATRate(invoice: IInvoice): number {
const totalTaxable = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxableAmount, 0);
if (totalTaxable === 0) {
return 19; // Default to standard German VAT rate
}
// Calculate weighted average VAT rate
const weightedRate = invoice.vatBreakdown.reduce((sum, vb) => {
return sum + (vb.vatCategory.rate * vb.taxableAmount);
}, 0);
return Math.round(weightedRate / totalTaxable * 100) / 100;
}
/**
* Build reverse charge VAT lines (§13b UStG)
*/
private buildReverseChargeVATLines(
invoice: IInvoice,
rules: IBookingRules
): IJournalEntryLine[] {
const lines: IJournalEntryLine[] = [];
// For reverse charge, we book both input and output VAT
for (const vatBreak of invoice.vatBreakdown) {
// For reverse charge, calculate VAT if not provided
const amount = vatBreak.taxAmount > 0
? Math.abs(vatBreak.taxAmount)
: this.calculateVAT(Math.abs(vatBreak.taxableAmount), vatBreak.vatCategory.rate);
// Input VAT (deductible)
const inputVATAccount = this.mapper.getVATAccount(
vatBreak.vatCategory,
'input',
'reverse_charge'
);
// Output VAT (payable)
const outputVATAccount = this.mapper.getVATAccount(
vatBreak.vatCategory,
'output',
'reverse_charge'
);
lines.push(
{
accountNumber: inputVATAccount,
debit: amount,
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`
},
{
accountNumber: outputVATAccount,
credit: amount,
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`
}
);
}
return lines;
}
/**
* Group invoice lines by account
*/
private groupLinesByAccount(
invoice: IInvoice,
rules: IBookingRules
): Record<string, IInvoiceLine[]> {
const groups: Record<string, IInvoiceLine[]> = {};
for (const line of invoice.lines) {
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
if (!groups[account]) {
groups[account] = [];
}
groups[account].push(line);
}
return groups;
}
/**
* Book payment for an invoice
*/
public async bookPayment(
invoice: IInvoice,
payment: IPaymentInfo,
rules: IBookingRules
): Promise<IBookingResult> {
try {
const lines: IJournalEntryLine[] = [];
const isInbound = invoice.direction === 'inbound';
const controlAccount = this.mapper.getControlAccount(invoice, rules);
// Check for skonto
const skontoAmount = payment.skontoTaken || 0;
const paymentAmount = payment.amount;
const fullAmount = paymentAmount + skontoAmount;
if (isInbound) {
// Payment for vendor invoice
lines.push(
{
accountNumber: controlAccount,
debit: fullAmount,
description: `Payment to ${invoice.supplier.name}`
},
{
accountNumber: '1000', // Bank account (would be configurable)
credit: paymentAmount,
description: `Bank payment ${payment.endToEndId || payment.paymentId}`
}
);
// Book skonto if taken
if (skontoAmount > 0) {
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
lines.push({
accountNumber: skontoAccounts.skontoAccount,
credit: skontoAmount,
description: `Skonto received`
});
// VAT correction for skonto
if (rules.skontoMethod === 'gross') {
const effectiveRate = this.calculateEffectiveVATRate(invoice);
const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
lines.push(
{
accountNumber: skontoAccounts.vatCorrectionAccount,
credit: vatCorrection,
description: `Skonto VAT correction`
}
);
}
}
} else {
// Payment from customer
lines.push(
{
accountNumber: '1000', // Bank account
debit: paymentAmount,
description: `Payment from ${invoice.customer.name}`
},
{
accountNumber: controlAccount,
credit: fullAmount,
description: `Customer payment ${payment.endToEndId || payment.paymentId}`
}
);
// Book skonto if granted
if (skontoAmount > 0) {
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
lines.push({
accountNumber: skontoAccounts.skontoAccount,
debit: skontoAmount,
description: `Skonto granted`
});
// VAT correction for skonto
if (rules.skontoMethod === 'gross') {
const effectiveRate = this.calculateEffectiveVATRate(invoice);
const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
lines.push(
{
accountNumber: skontoAccounts.vatCorrectionAccount,
debit: vatCorrection,
description: `Skonto VAT correction`
}
);
}
}
}
// Create journal entry for payment
const journalData: IJournalEntry = {
date: payment.paymentDate,
description: `Payment for invoice ${invoice.invoiceNumber}`,
reference: payment.endToEndId || payment.remittanceInfo || payment.paymentId,
lines,
skrType: this.skrType
};
const journalEntry = new JournalEntry(journalData);
await journalEntry.validate();
await journalEntry.post();
return {
success: true,
journalEntry,
confidence: 100
};
} catch (error) {
this.logger.log('error', `Failed to book payment: ${error}`);
return {
success: false,
confidence: 0,
errors: [`Payment booking failed: ${error.message}`]
};
}
}
/**
* Validate invoice before booking
*/
private validateInvoice(invoice: IInvoice): string[] {
const errors: string[] = [];
// Check required fields
if (!invoice.invoiceNumber) {
errors.push('Invoice number is required');
}
if (!invoice.issueDate) {
errors.push('Issue date is required');
}
if (!invoice.supplier || !invoice.supplier.name) {
errors.push('Supplier information is required');
}
if (!invoice.customer || !invoice.customer.name) {
errors.push('Customer information is required');
}
if (invoice.lines.length === 0) {
errors.push('Invoice must have at least one line item');
}
// Validate amounts
const calculatedNet = invoice.lines.reduce((sum, line) => sum + line.netAmount, 0);
const tolerance = 0.01;
if (Math.abs(calculatedNet - invoice.lineNetAmount) > tolerance) {
errors.push(`Line net amount mismatch: calculated ${calculatedNet}, stated ${invoice.lineNetAmount}`);
}
// Validate VAT
const calculatedVAT = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxAmount, 0);
if (Math.abs(calculatedVAT - invoice.totalVATAmount) > tolerance) {
errors.push(`VAT amount mismatch: calculated ${calculatedVAT}, stated ${invoice.totalVATAmount}`);
}
// Validate total
const calculatedTotal = invoice.taxExclusiveAmount + invoice.totalVATAmount;
if (Math.abs(calculatedTotal - invoice.taxInclusiveAmount) > tolerance) {
errors.push(`Total amount mismatch: calculated ${calculatedTotal}, stated ${invoice.taxInclusiveAmount}`);
}
return errors;
}
/**
* Generate warnings for the booking
*/
private generateWarnings(invoice: IInvoice, rules: IBookingRules): string[] {
const warnings: string[] = [];
// Warn about default account usage
const hasDefaultAccounts = invoice.lines.some(line =>
!line.accountNumber && !line.productCode
);
if (hasDefaultAccounts) {
warnings.push('Some lines are using default expense/revenue accounts');
}
// Warn about mixed VAT rates
if (invoice.vatBreakdown.length > 1) {
warnings.push('Invoice contains mixed VAT rates');
}
// Warn about reverse charge
if (invoice.taxScenario === 'reverse_charge') {
warnings.push('Reverse charge procedure applied - verify VAT treatment');
}
// Warn about credit notes
if (invoice.invoiceTypeCode === '381') {
warnings.push('This is a credit note - amounts will be reversed');
}
// Warn about foreign currency
if (invoice.currencyCode !== 'EUR') {
warnings.push(`Invoice is in foreign currency: ${invoice.currencyCode}`);
}
return warnings;
}
/**
* Build description for journal entry
*/
private buildDescription(invoice: IInvoice): string {
const type = invoice.invoiceTypeCode === '381' ? 'Credit Note' : 'Invoice';
const party = invoice.direction === 'inbound'
? invoice.supplier.name
: invoice.customer.name;
return `${type} ${invoice.invoiceNumber} - ${party}`;
}
/**
* Get account description for a group of lines
*/
private getAccountDescription(accountNumber: string, lines: IInvoiceLine[]): string {
if (lines.length === 1) {
return lines[0].description;
}
return `${this.mapper.getAccountDescription(accountNumber)} (${lines.length} items)`;
}
/**
* Get used expense accounts
*/
private getUsedExpenseAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
if (invoice.direction !== 'inbound') return [];
const accounts = new Set<string>();
for (const line of invoice.lines) {
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
accounts.add(account);
}
return Array.from(accounts);
}
/**
* Get used revenue accounts
*/
private getUsedRevenueAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
if (invoice.direction !== 'outbound') return [];
const accounts = new Set<string>();
for (const line of invoice.lines) {
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
accounts.add(account);
}
return Array.from(accounts);
}
/**
* Get used VAT accounts
*/
private getUsedVATAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
const accounts = new Set<string>();
const direction = invoice.direction === 'inbound' ? 'input' : 'output';
const taxScenario = invoice.taxScenario || 'domestic_taxed';
for (const vatBreak of invoice.vatBreakdown) {
const account = this.mapper.getVATAccount(
vatBreak.vatCategory,
direction,
taxScenario
);
accounts.add(account);
}
// Add reverse charge accounts if applicable
if (taxScenario === 'reverse_charge') {
for (const vatBreak of invoice.vatBreakdown) {
const inputAccount = this.mapper.getVATAccount(
vatBreak.vatCategory,
'input',
'reverse_charge'
);
const outputAccount = this.mapper.getVATAccount(
vatBreak.vatCategory,
'output',
'reverse_charge'
);
accounts.add(inputAccount);
accounts.add(outputAccount);
}
}
return Array.from(accounts);
}
}

351
ts/skr.invoice.entity.ts Normal file
View File

@@ -0,0 +1,351 @@
import type { TSKRType } from './skr.types.js';
/**
* Invoice direction
*/
export type TInvoiceDirection = 'inbound' | 'outbound';
/**
* Supported e-invoice formats
*/
export type TInvoiceFormat = 'xrechnung' | 'zugferd' | 'facturx' | 'peppol' | 'ubl';
/**
* Invoice status in the system
*/
export type TInvoiceStatus = 'draft' | 'validated' | 'posted' | 'partially_paid' | 'paid' | 'cancelled' | 'error';
/**
* Tax scenario classification
*/
export type TTaxScenario =
| 'domestic_taxed' // Standard domestic with VAT
| 'domestic_exempt' // Domestic tax-exempt
| 'reverse_charge' // §13b UStG
| 'intra_eu_supply' // Intra-EU supply
| 'intra_eu_acquisition' // Intra-EU acquisition
| 'export' // Export outside EU
| 'small_business'; // §19 UStG small business
/**
* VAT rate categories
*/
export interface IVATCategory {
code: string; // S (Standard), Z (Zero), E (Exempt), AE (Reverse charge), etc.
rate: number; // Tax rate percentage
exemptionReason?: string;
}
/**
* Party information (supplier/customer)
*/
export interface IInvoiceParty {
id: string;
name: string;
address: {
street?: string;
city?: string;
postalCode?: string;
countryCode: string;
};
vatId?: string;
taxId?: string;
email?: string;
phone?: string;
bankAccount?: {
iban: string;
bic?: string;
accountHolder?: string;
};
}
/**
* Invoice line item
*/
export interface IInvoiceLine {
lineNumber: number;
description: string;
quantity: number;
unitPrice: number;
netAmount: number;
vatCategory: IVATCategory;
vatAmount: number;
grossAmount: number;
accountNumber?: string; // SKR account for booking
costCenter?: string;
productCode?: string;
allowances?: IAllowanceCharge[];
charges?: IAllowanceCharge[];
}
/**
* Allowance or charge
*/
export interface IAllowanceCharge {
reason: string;
amount: number;
percentage?: number;
vatCategory?: IVATCategory;
vatAmount?: number;
}
/**
* Payment terms
*/
export interface IPaymentTerms {
dueDate: Date;
paymentTermsNote?: string;
skonto?: {
percentage: number;
days: number;
baseAmount: number;
}[];
}
/**
* Validation result
*/
export interface IValidationResult {
isValid: boolean;
syntax: {
valid: boolean;
errors: string[];
warnings: string[];
};
semantic: {
valid: boolean;
errors: string[];
warnings: string[];
};
businessRules: {
valid: boolean;
errors: string[];
warnings: string[];
};
countrySpecific?: {
valid: boolean;
errors: string[];
warnings: string[];
};
validatedAt: Date;
validatorVersion: string;
}
/**
* Booking information
*/
export interface IBookingInfo {
journalEntryId: string;
transactionIds: string[];
bookedAt: Date;
bookedBy: string;
bookingRules: {
vendorAccount?: string;
customerAccount?: string;
expenseAccounts?: string[];
revenueAccounts?: string[];
vatAccounts?: string[];
};
confidence: number; // 0-100
autoBooked: boolean;
}
/**
* Payment information
*/
export interface IPaymentInfo {
paymentId: string;
paymentDate: Date;
amount: number;
currency: string;
bankTransactionId?: string;
endToEndId?: string;
remittanceInfo?: string;
skontoTaken?: number;
}
/**
* Main invoice entity
*/
export interface IInvoice {
// Identity
id: string;
direction: TInvoiceDirection;
format: TInvoiceFormat;
// EN16931 Business Terms
invoiceNumber: string; // BT-1
issueDate: Date; // BT-2
invoiceTypeCode?: string; // BT-3 (380=Invoice, 381=Credit note)
currencyCode: string; // BT-5
taxCurrencyCode?: string; // BT-6
taxPointDate?: Date; // BT-7 (Leistungsdatum)
paymentDueDate?: Date; // BT-9
buyerReference?: string; // BT-10
projectReference?: string; // BT-11
contractReference?: string; // BT-12
orderReference?: string; // BT-13
sellerOrderReference?: string; // BT-14
// Parties
supplier: IInvoiceParty;
customer: IInvoiceParty;
payee?: IInvoiceParty; // If different from supplier
// Line items
lines: IInvoiceLine[];
// Document level allowances/charges
allowances?: IAllowanceCharge[];
charges?: IAllowanceCharge[];
// Amounts
lineNetAmount: number; // Sum of line net amounts
allowanceTotalAmount?: number;
chargeTotalAmount?: number;
taxExclusiveAmount: number; // BT-109
taxInclusiveAmount: number; // BT-112
prepaidAmount?: number; // BT-113
payableAmount: number; // BT-115
// VAT breakdown
vatBreakdown: {
vatCategory: IVATCategory;
taxableAmount: number; // BT-116
taxAmount: number; // BT-117
}[];
totalVATAmount: number; // BT-110
// Payment
paymentTerms?: IPaymentTerms;
paymentMeans?: {
code: string; // 30=Bank transfer, 48=Card, etc.
account?: IInvoiceParty['bankAccount'];
};
payments?: IPaymentInfo[];
// Notes
invoiceNote?: string; // BT-22
// Processing metadata
status: TInvoiceStatus;
taxScenario?: TTaxScenario;
skrType?: TSKRType;
// Storage
contentHash: string; // SHA-256 of normalized XML
xmlContent?: string;
pdfHash?: string;
pdfContent?: Buffer;
// Validation
validationResult?: IValidationResult;
// Booking
bookingInfo?: IBookingInfo;
// Audit trail
createdAt: Date;
createdBy: string;
modifiedAt?: Date;
modifiedBy?: string;
// Additional metadata
metadata?: {
importSource?: string;
importedAt?: Date;
parserVersion?: string;
originalFilename?: string;
originalFormat?: string;
[key: string]: any;
};
}
/**
* Invoice import options
*/
export interface IInvoiceImportOptions {
autoBook?: boolean;
confidenceThreshold?: number;
validateOnly?: boolean;
skipDuplicateCheck?: boolean;
bookingRules?: {
vendorDefaults?: Record<string, string>;
customerDefaults?: Record<string, string>;
productCategoryMapping?: Record<string, string>;
};
}
/**
* Invoice export options
*/
export interface IInvoiceExportOptions {
format: TInvoiceFormat;
embedInPdf?: boolean;
sign?: boolean;
validate?: boolean;
}
/**
* Invoice search filter
*/
export interface IInvoiceFilter {
direction?: TInvoiceDirection;
status?: TInvoiceStatus;
format?: TInvoiceFormat;
dateFrom?: Date;
dateTo?: Date;
supplierId?: string;
customerId?: string;
minAmount?: number;
maxAmount?: number;
invoiceNumber?: string;
reference?: string;
isPaid?: boolean;
isOverdue?: boolean;
}
/**
* Duplicate check result
*/
export interface IDuplicateCheckResult {
isDuplicate: boolean;
matchedInvoiceId?: string;
matchedContentHash?: string;
matchedFields?: string[];
confidence: number;
}
/**
* Booking rules configuration
*/
export interface IBookingRules {
skrType: TSKRType;
// Control accounts
vendorControlAccount: string;
customerControlAccount: string;
// VAT accounts
vatAccounts: {
inputVAT19: string;
inputVAT7: string;
outputVAT19: string;
outputVAT7: string;
reverseChargeVAT: string;
};
// Default accounts
defaultExpenseAccount: string;
defaultRevenueAccount: string;
// Mappings
productCategoryMapping?: Record<string, string>;
vendorMapping?: Record<string, string>;
customerMapping?: Record<string, string>;
// Skonto
skontoMethod?: 'net' | 'gross';
skontoExpenseAccount?: string;
skontoRevenueAccount?: string;
}

486
ts/skr.invoice.mapper.ts Normal file
View File

@@ -0,0 +1,486 @@
import * as plugins from './plugins.js';
import type { TSKRType } from './skr.types.js';
import type {
IInvoice,
IInvoiceLine,
IBookingRules,
TTaxScenario,
IVATCategory
} from './skr.invoice.entity.js';
/**
* Maps invoice data to SKR accounts
* Handles both SKR03 and SKR04 account mappings
*/
export class SKRInvoiceMapper {
private logger: plugins.smartlog.ConsoleLog;
private skrType: TSKRType;
// SKR03 account mappings
private readonly SKR03_ACCOUNTS = {
// Control accounts
vendorControl: '1600', // Verbindlichkeiten aus Lieferungen und Leistungen
customerControl: '1200', // Forderungen aus Lieferungen und Leistungen
// VAT accounts
inputVAT19: '1576', // Abziehbare Vorsteuer 19%
inputVAT7: '1571', // Abziehbare Vorsteuer 7%
outputVAT19: '1776', // Umsatzsteuer 19%
outputVAT7: '1771', // Umsatzsteuer 7%
reverseChargeVAT: '1577', // Abziehbare Vorsteuer §13b UStG
reverseChargePayable: '1787', // Umsatzsteuer §13b UStG
// Default expense/revenue accounts
defaultExpense: '4610', // Werbekosten
defaultRevenue: '8400', // Erlöse 19% USt
revenueReduced: '8300', // Erlöse 7% USt
revenueTaxFree: '8120', // Steuerfreie Umsätze
// Common expense accounts by category
materialExpense: '5000', // Aufwendungen für Roh-, Hilfs- und Betriebsstoffe
merchandiseExpense: '5400', // Aufwendungen für Waren
personnelExpense: '6000', // Löhne und Gehälter
rentExpense: '4200', // Miete
officeExpense: '4930', // Bürobedarf
travelExpense: '4670', // Reisekosten
vehicleExpense: '4530', // Kfz-Kosten
// Skonto accounts
skontoExpense: '4736', // Erhaltene Skonti 19% USt
skontoRevenue: '8736', // Gewährte Skonti 19% USt
// Intra-EU accounts
intraEUAcquisition: '8125', // Steuerfreie innergemeinschaftliche Erwerbe
intraEUSupply: '8125' // Steuerfreie innergemeinschaftliche Lieferungen
};
// SKR04 account mappings
private readonly SKR04_ACCOUNTS = {
// Control accounts
vendorControl: '3300', // Verbindlichkeiten aus Lieferungen und Leistungen
customerControl: '1400', // Forderungen aus Lieferungen und Leistungen
// VAT accounts
inputVAT19: '1406', // Abziehbare Vorsteuer 19%
inputVAT7: '1401', // Abziehbare Vorsteuer 7%
outputVAT19: '3806', // Umsatzsteuer 19%
outputVAT7: '3801', // Umsatzsteuer 7%
reverseChargeVAT: '1407', // Abziehbare Vorsteuer §13b UStG
reverseChargePayable: '3837', // Umsatzsteuer §13b UStG
// Default expense/revenue accounts
defaultExpense: '6300', // Sonstige betriebliche Aufwendungen
defaultRevenue: '4400', // Erlöse 19% USt
revenueReduced: '4300', // Erlöse 7% USt
revenueTaxFree: '4120', // Steuerfreie Umsätze
// Common expense accounts by category
materialExpense: '5000', // Aufwendungen für Roh-, Hilfs- und Betriebsstoffe
merchandiseExpense: '5400', // Aufwendungen für Waren
personnelExpense: '6000', // Löhne
rentExpense: '6310', // Miete
officeExpense: '6815', // Bürobedarf
travelExpense: '6670', // Reisekosten
vehicleExpense: '6530', // Kfz-Kosten
// Skonto accounts
skontoExpense: '4736', // Erhaltene Skonti 19% USt
skontoRevenue: '8736', // Gewährte Skonti 19% USt
// Intra-EU accounts
intraEUAcquisition: '4125', // Steuerfreie innergemeinschaftliche Erwerbe
intraEUSupply: '4125' // Steuerfreie innergemeinschaftliche Lieferungen
};
// Product category to account mappings
private readonly CATEGORY_MAPPINGS: Record<string, { skr03: string; skr04: string }> = {
'MATERIAL': { skr03: '5000', skr04: '5000' },
'MERCHANDISE': { skr03: '5400', skr04: '5400' },
'SERVICE': { skr03: '4610', skr04: '6300' },
'OFFICE': { skr03: '4930', skr04: '6815' },
'IT': { skr03: '4940', skr04: '6825' },
'TRAVEL': { skr03: '4670', skr04: '6670' },
'VEHICLE': { skr03: '4530', skr04: '6530' },
'RENT': { skr03: '4200', skr04: '6310' },
'UTILITIES': { skr03: '4240', skr04: '6320' },
'INSURANCE': { skr03: '4360', skr04: '6420' },
'MARKETING': { skr03: '4610', skr04: '6600' },
'CONSULTING': { skr03: '4640', skr04: '6650' },
'LEGAL': { skr03: '4790', skr04: '6790' },
'TELECOMMUNICATION': { skr03: '4920', skr04: '6805' }
};
constructor(skrType: TSKRType) {
this.skrType = skrType;
this.logger = new plugins.smartlog.ConsoleLog();
}
/**
* Get account mappings for current SKR type
*/
private getAccounts() {
return this.skrType === 'SKR03' ? this.SKR03_ACCOUNTS : this.SKR04_ACCOUNTS;
}
/**
* Map invoice to booking rules
*/
public mapInvoiceToSKR(
invoice: IInvoice,
customMappings?: Partial<IBookingRules>
): IBookingRules {
const accounts = this.getAccounts();
const taxScenario = invoice.taxScenario || 'domestic_taxed';
// Base booking rules
const bookingRules: IBookingRules = {
skrType: this.skrType,
// Control accounts
vendorControlAccount: customMappings?.vendorControlAccount || accounts.vendorControl,
customerControlAccount: customMappings?.customerControlAccount || accounts.customerControl,
// VAT accounts
vatAccounts: {
inputVAT19: accounts.inputVAT19,
inputVAT7: accounts.inputVAT7,
outputVAT19: accounts.outputVAT19,
outputVAT7: accounts.outputVAT7,
reverseChargeVAT: accounts.reverseChargeVAT
},
// Default accounts
defaultExpenseAccount: accounts.defaultExpense,
defaultRevenueAccount: accounts.defaultRevenue,
// Skonto
skontoMethod: customMappings?.skontoMethod || 'gross',
skontoExpenseAccount: accounts.skontoExpense,
skontoRevenueAccount: accounts.skontoRevenue,
// Custom mappings
productCategoryMapping: customMappings?.productCategoryMapping || {},
vendorMapping: customMappings?.vendorMapping || {},
customerMapping: customMappings?.customerMapping || {}
};
return bookingRules;
}
/**
* Map invoice line to SKR account
*/
public mapInvoiceLineToAccount(
line: IInvoiceLine,
invoice: IInvoice,
bookingRules: IBookingRules
): string {
// Check if account is already specified
if (line.accountNumber) {
return line.accountNumber;
}
// For revenue (outbound invoices)
if (invoice.direction === 'outbound') {
return this.mapRevenueAccount(line, invoice, bookingRules);
}
// For expenses (inbound invoices)
return this.mapExpenseAccount(line, invoice, bookingRules);
}
/**
* Map revenue account based on VAT rate and scenario
*/
private mapRevenueAccount(
line: IInvoiceLine,
invoice: IInvoice,
bookingRules: IBookingRules
): string {
const accounts = this.getAccounts();
const vatRate = line.vatCategory.rate;
// Check tax scenario
switch (invoice.taxScenario) {
case 'intra_eu_supply':
return accounts.intraEUSupply;
case 'export':
case 'domestic_exempt':
return accounts.revenueTaxFree;
case 'domestic_taxed':
default:
// Map by VAT rate
if (vatRate === 19) {
return accounts.defaultRevenue;
} else if (vatRate === 7) {
return accounts.revenueReduced;
} else if (vatRate === 0) {
return accounts.revenueTaxFree;
}
return accounts.defaultRevenue;
}
}
/**
* Map expense account based on product category and vendor
*/
private mapExpenseAccount(
line: IInvoiceLine,
invoice: IInvoice,
bookingRules: IBookingRules
): string {
const accounts = this.getAccounts();
// Check vendor-specific mapping
const vendorId = invoice.supplier.id;
if (bookingRules.vendorMapping && bookingRules.vendorMapping[vendorId]) {
return bookingRules.vendorMapping[vendorId];
}
// Try to determine category from line description
const category = this.detectProductCategory(line.description);
if (category) {
const mapping = this.CATEGORY_MAPPINGS[category];
if (mapping) {
return this.skrType === 'SKR03' ? mapping.skr03 : mapping.skr04;
}
}
// Check product category mapping
if (line.productCode && bookingRules.productCategoryMapping) {
const mappedAccount = bookingRules.productCategoryMapping[line.productCode];
if (mappedAccount) {
return mappedAccount;
}
}
// Default expense account
return bookingRules.defaultExpenseAccount;
}
/**
* Detect product category from description
*/
private detectProductCategory(description: string): string | undefined {
const lowerDesc = description.toLowerCase();
const categoryKeywords: Record<string, string[]> = {
'MATERIAL': ['material', 'rohstoff', 'raw material', 'component'],
'MERCHANDISE': ['ware', 'merchandise', 'product', 'artikel'],
'SERVICE': ['service', 'dienstleistung', 'beratung', 'support'],
'OFFICE': ['büro', 'office', 'papier', 'stationery'],
'IT': ['software', 'hardware', 'computer', 'lizenz', 'license'],
'TRAVEL': ['reise', 'travel', 'hotel', 'flug', 'flight'],
'VEHICLE': ['kfz', 'vehicle', 'auto', 'benzin', 'fuel'],
'RENT': ['miete', 'rent', 'lease', 'pacht'],
'UTILITIES': ['strom', 'wasser', 'gas', 'energie', 'electricity', 'water'],
'INSURANCE': ['versicherung', 'insurance'],
'MARKETING': ['werbung', 'marketing', 'advertising', 'kampagne'],
'CONSULTING': ['beratung', 'consulting', 'advisory'],
'LEGAL': ['rechts', 'legal', 'anwalt', 'lawyer', 'notar'],
'TELECOMMUNICATION': ['telefon', 'internet', 'mobilfunk', 'telekom']
};
for (const [category, keywords] of Object.entries(categoryKeywords)) {
if (keywords.some(keyword => lowerDesc.includes(keyword))) {
return category;
}
}
return undefined;
}
/**
* Get VAT account for given VAT category and rate
*/
public getVATAccount(
vatCategory: IVATCategory,
direction: 'input' | 'output',
taxScenario: TTaxScenario
): string {
const accounts = this.getAccounts();
// Handle reverse charge
if (taxScenario === 'reverse_charge' || vatCategory.code === 'AE') {
return direction === 'input'
? accounts.reverseChargeVAT
: accounts.reverseChargePayable;
}
// Standard VAT accounts by rate
if (direction === 'input') {
if (vatCategory.rate === 19) {
return accounts.inputVAT19;
} else if (vatCategory.rate === 7) {
return accounts.inputVAT7;
}
} else {
if (vatCategory.rate === 19) {
return accounts.outputVAT19;
} else if (vatCategory.rate === 7) {
return accounts.outputVAT7;
}
}
// Default to 19% if rate is not standard
return direction === 'input' ? accounts.inputVAT19 : accounts.outputVAT19;
}
/**
* Get control account for party
*/
public getControlAccount(
invoice: IInvoice,
bookingRules: IBookingRules
): string {
if (invoice.direction === 'inbound') {
// Check vendor-specific control account
const vendorId = invoice.supplier.id;
if (bookingRules.vendorMapping && bookingRules.vendorMapping[vendorId]) {
const customAccount = bookingRules.vendorMapping[vendorId];
// Check if it's a control account (starts with 16 for SKR03 or 33 for SKR04)
if (this.isControlAccount(customAccount)) {
return customAccount;
}
}
return bookingRules.vendorControlAccount;
} else {
// Check customer-specific control account
const customerId = invoice.customer.id;
if (bookingRules.customerMapping && bookingRules.customerMapping[customerId]) {
const customAccount = bookingRules.customerMapping[customerId];
// Check if it's a control account (starts with 12 for SKR03 or 14 for SKR04)
if (this.isControlAccount(customAccount)) {
return customAccount;
}
}
return bookingRules.customerControlAccount;
}
}
/**
* Check if account is a control account
*/
private isControlAccount(accountNumber: string): boolean {
if (this.skrType === 'SKR03') {
return accountNumber.startsWith('12') || accountNumber.startsWith('16');
} else {
return accountNumber.startsWith('14') || accountNumber.startsWith('33');
}
}
/**
* Get skonto accounts
*/
public getSkontoAccounts(invoice: IInvoice): {
skontoAccount: string;
vatCorrectionAccount: string;
} {
const accounts = this.getAccounts();
if (invoice.direction === 'inbound') {
// Received skonto (expense reduction)
return {
skontoAccount: accounts.skontoExpense,
vatCorrectionAccount: accounts.inputVAT19 // VAT correction
};
} else {
// Granted skonto (revenue reduction)
return {
skontoAccount: accounts.skontoRevenue,
vatCorrectionAccount: accounts.outputVAT19 // VAT correction
};
}
}
/**
* Validate account number format
*/
public validateAccountNumber(accountNumber: string): boolean {
// SKR accounts are typically 4 digits, sometimes with sub-accounts
const accountPattern = /^\d{4}(\d{0,2})?$/;
return accountPattern.test(accountNumber);
}
/**
* Get account description
*/
public getAccountDescription(accountNumber: string): string {
// This would typically look up from a complete SKR account database
// For now, return a basic description
const commonAccounts: Record<string, string> = {
// SKR03
'1200': 'Forderungen aus Lieferungen und Leistungen',
'1600': 'Verbindlichkeiten aus Lieferungen und Leistungen',
'1576': 'Abziehbare Vorsteuer 19%',
'1571': 'Abziehbare Vorsteuer 7%',
'1776': 'Umsatzsteuer 19%',
'1771': 'Umsatzsteuer 7%',
'4610': 'Werbekosten',
'8400': 'Erlöse 19% USt',
'8300': 'Erlöse 7% USt',
// SKR04
'1400': 'Forderungen aus Lieferungen und Leistungen',
'3300': 'Verbindlichkeiten aus Lieferungen und Leistungen',
'1406': 'Abziehbare Vorsteuer 19%',
'1401': 'Abziehbare Vorsteuer 7%',
'3806': 'Umsatzsteuer 19%',
'3801': 'Umsatzsteuer 7%',
'6300': 'Sonstige betriebliche Aufwendungen',
'4400': 'Erlöse 19% USt',
'4300': 'Erlöse 7% USt'
};
return commonAccounts[accountNumber] || `Account ${accountNumber}`;
}
/**
* Calculate booking confidence score
*/
public calculateConfidence(
invoice: IInvoice,
bookingRules: IBookingRules
): number {
let confidence = 100;
// Reduce confidence for missing or uncertain mappings
invoice.lines.forEach(line => {
if (!line.accountNumber) {
confidence -= 10; // No explicit account mapping
}
if (!line.productCode) {
confidence -= 5; // No product code for mapping
}
});
// Reduce confidence for complex tax scenarios
if (invoice.taxScenario === 'reverse_charge' ||
invoice.taxScenario === 'intra_eu_acquisition') {
confidence -= 15;
}
// Reduce confidence for mixed VAT rates
if (invoice.vatBreakdown.length > 1) {
confidence -= 10;
}
// Reduce confidence if no vendor/customer mapping exists
if (invoice.direction === 'inbound') {
if (!bookingRules.vendorMapping?.[invoice.supplier.id]) {
confidence -= 10;
}
} else {
if (!bookingRules.customerMapping?.[invoice.customer.id]) {
confidence -= 10;
}
}
// Reduce confidence for credit notes
if (invoice.invoiceTypeCode === '381') {
confidence -= 10;
}
return Math.max(0, confidence);
}
}

710
ts/skr.invoice.storage.ts Normal file
View File

@@ -0,0 +1,710 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type {
IInvoice,
IInvoiceFilter,
IDuplicateCheckResult
} from './skr.invoice.entity.js';
/**
* Invoice storage metadata
*/
export interface IInvoiceMetadata {
invoiceId: string;
invoiceNumber: string;
direction: 'inbound' | 'outbound';
issueDate: string;
supplierName: string;
customerName: string;
totalAmount: number;
currency: string;
contentHash: string;
pdfHash?: string;
xmlHash: string;
journalEntryId?: string;
transactionIds?: string[];
validationResult: {
isValid: boolean;
errors: number;
warnings: number;
};
parserVersion: string;
storedAt: string;
storedBy: string;
}
/**
* Invoice registry entry (for NDJSON streaming)
*/
export interface IInvoiceRegistryEntry {
id: string;
hash: string;
metadata: IInvoiceMetadata;
}
/**
* Storage statistics
*/
export interface IStorageStats {
totalInvoices: number;
inboundCount: number;
outboundCount: number;
totalSize: number;
duplicatesDetected: number;
lastUpdate: Date;
}
/**
* Content-addressed storage for invoices
* Integrates with BagIt archive structure for GoBD compliance
*/
export class InvoiceStorage {
private exportPath: string;
private logger: plugins.smartlog.ConsoleLog;
private registryPath: string;
private metadataCache: Map<string, IInvoiceMetadata>;
private readonly MAX_CACHE_SIZE = 10000; // Maximum number of cached entries
private cacheAccessOrder: string[] = []; // Track access order for LRU eviction
constructor(exportPath: string) {
this.exportPath = exportPath;
this.logger = new plugins.smartlog.ConsoleLog();
this.registryPath = path.join(exportPath, 'data', 'documents', 'invoices', 'registry.ndjson');
this.metadataCache = new Map();
}
/**
* Manage cache size using LRU eviction
*/
private manageCacheSize(): void {
if (this.metadataCache.size > this.MAX_CACHE_SIZE) {
// Remove least recently used entries
const entriesToRemove = Math.min(100, Math.floor(this.MAX_CACHE_SIZE * 0.1)); // Remove 10% or 100 entries
const keysToRemove = this.cacheAccessOrder.splice(0, entriesToRemove);
for (const key of keysToRemove) {
this.metadataCache.delete(key);
}
this.logger.log('info', `Evicted ${entriesToRemove} entries from metadata cache`);
}
}
/**
* Update cache access order for LRU
*/
private touchCacheEntry(key: string): void {
const index = this.cacheAccessOrder.indexOf(key);
if (index > -1) {
this.cacheAccessOrder.splice(index, 1);
}
this.cacheAccessOrder.push(key);
}
/**
* Initialize storage directories
*/
public async initialize(): Promise<void> {
const dirs = [
path.join(this.exportPath, 'data', 'documents', 'invoices', 'inbound'),
path.join(this.exportPath, 'data', 'documents', 'invoices', 'inbound', 'metadata'),
path.join(this.exportPath, 'data', 'documents', 'invoices', 'outbound'),
path.join(this.exportPath, 'data', 'documents', 'invoices', 'outbound', 'metadata'),
path.join(this.exportPath, 'data', 'validation')
];
for (const dir of dirs) {
await plugins.smartfile.fs.ensureDir(dir);
}
// Load existing registry if it exists
await this.loadRegistry();
}
private readonly MAX_PDF_SIZE = 50 * 1024 * 1024; // 50MB max
/**
* Store an invoice with content addressing
*/
public async storeInvoice(
invoice: IInvoice,
pdfBuffer?: Buffer
): Promise<string> {
try {
// Validate PDF size if provided
if (pdfBuffer && pdfBuffer.length > this.MAX_PDF_SIZE) {
throw new Error(`PDF file too large: ${pdfBuffer.length} bytes (max ${this.MAX_PDF_SIZE} bytes)`);
}
// Calculate hashes
const xmlHash = await this.calculateHash(invoice.xmlContent || '');
const pdfHash = pdfBuffer ? await this.calculateHash(pdfBuffer) : undefined;
const contentHash = xmlHash; // Primary content hash is XML
// Check for duplicates
const duplicateCheck = await this.checkDuplicate(invoice, contentHash);
if (duplicateCheck.isDuplicate) {
this.logger.log('warn', `Duplicate invoice detected: ${invoice.invoiceNumber}`);
return duplicateCheck.matchedContentHash || contentHash;
}
// Determine storage path
const direction = invoice.direction;
const basePath = path.join(
this.exportPath,
'data',
'documents',
'invoices',
direction
);
// Create filename with content hash
const dateStr = invoice.issueDate.toISOString().split('T')[0];
const sanitizedNumber = invoice.invoiceNumber.replace(/[^a-zA-Z0-9-_]/g, '_');
const xmlFilename = `${contentHash.substring(0, 8)}_${dateStr}_${sanitizedNumber}.xml`;
const xmlPath = path.join(basePath, xmlFilename);
// Store XML
await plugins.smartfile.memory.toFs(invoice.xmlContent || '', xmlPath);
// Store PDF if available
let pdfFilename: string | undefined;
if (pdfBuffer) {
pdfFilename = xmlFilename.replace('.xml', '.pdf');
const pdfPath = path.join(basePath, pdfFilename);
await plugins.smartfile.memory.toFs(pdfBuffer, pdfPath);
// Also store PDF/A-3 with embedded XML if supported
if (invoice.format === 'zugferd' || invoice.format === 'facturx') {
const pdfA3Filename = xmlFilename.replace('.xml', '_pdfa3.pdf');
const pdfA3Path = path.join(basePath, pdfA3Filename);
// The PDF should already have embedded XML if it's ZUGFeRD/Factur-X
await plugins.smartfile.memory.toFs(pdfBuffer, pdfA3Path);
}
}
// Create and store metadata
const metadata: IInvoiceMetadata = {
invoiceId: invoice.id,
invoiceNumber: invoice.invoiceNumber,
direction: invoice.direction,
issueDate: invoice.issueDate.toISOString(),
supplierName: invoice.supplier.name,
customerName: invoice.customer.name,
totalAmount: invoice.payableAmount,
currency: invoice.currencyCode,
contentHash,
pdfHash,
xmlHash,
journalEntryId: invoice.bookingInfo?.journalEntryId,
transactionIds: invoice.bookingInfo?.transactionIds,
validationResult: {
isValid: invoice.validationResult?.isValid || false,
errors: this.countErrors(invoice.validationResult),
warnings: this.countWarnings(invoice.validationResult)
},
parserVersion: invoice.metadata?.parserVersion || '5.1.4',
storedAt: new Date().toISOString(),
storedBy: invoice.createdBy
};
const metadataPath = path.join(basePath, 'metadata', `${contentHash}.json`);
await plugins.smartfile.memory.toFs(
JSON.stringify(metadata, null, 2),
metadataPath
);
// Update registry
await this.updateRegistry(invoice.id, contentHash, metadata);
// Cache metadata with LRU management
this.setCacheEntry(contentHash, metadata);
this.logger.log('info', `Invoice stored: ${invoice.invoiceNumber} (${contentHash})`);
return contentHash;
} catch (error) {
this.logger.log('error', `Failed to store invoice: ${error}`);
throw new Error(`Invoice storage failed: ${error.message}`);
}
}
/**
* Retrieve an invoice by content hash
*/
public async retrieveInvoice(contentHash: string): Promise<IInvoice | null> {
try {
// Check cache first
const metadata = this.getCacheEntry(contentHash);
if (!metadata) {
this.logger.log('warn', `Invoice not found: ${contentHash}`);
return null;
}
// Load XML content
const xmlPath = await this.findInvoiceFile(contentHash, '.xml');
if (!xmlPath) {
throw new Error(`XML file not found for invoice ${contentHash}`);
}
const xmlContent = await plugins.smartfile.fs.toStringSync(xmlPath);
// Load PDF if exists
let pdfContent: Buffer | undefined;
const pdfPath = await this.findInvoiceFile(contentHash, '.pdf');
if (pdfPath) {
pdfContent = await plugins.smartfile.fs.toBuffer(pdfPath);
}
// Reconstruct invoice object (partial)
const invoice: Partial<IInvoice> = {
id: metadata.invoiceId,
invoiceNumber: metadata.invoiceNumber,
direction: metadata.direction as any,
issueDate: new Date(metadata.issueDate),
supplier: {
name: metadata.supplierName,
id: '',
address: { countryCode: 'DE' }
},
customer: {
name: metadata.customerName,
id: '',
address: { countryCode: 'DE' }
},
payableAmount: metadata.totalAmount,
currencyCode: metadata.currency,
contentHash: metadata.contentHash,
xmlContent,
pdfContent,
pdfHash: metadata.pdfHash
};
return invoice as IInvoice;
} catch (error) {
this.logger.log('error', `Failed to retrieve invoice: ${error}`);
return null;
}
}
/**
* Check for duplicate invoices
*/
public async checkDuplicate(
invoice: IInvoice,
contentHash: string
): Promise<IDuplicateCheckResult> {
// Check by content hash (exact match)
const existing = this.getCacheEntry(contentHash);
if (existing) {
return {
isDuplicate: true,
matchedInvoiceId: existing.invoiceId,
matchedContentHash: contentHash,
matchedFields: ['contentHash'],
confidence: 100
};
}
// Check by invoice number and supplier/customer
for (const [hash, metadata] of this.metadataCache.entries()) {
if (
metadata.invoiceNumber === invoice.invoiceNumber &&
metadata.direction === invoice.direction
) {
// Same invoice number and direction
if (invoice.direction === 'inbound' && metadata.supplierName === invoice.supplier.name) {
// Same supplier
return {
isDuplicate: true,
matchedInvoiceId: metadata.invoiceId,
matchedContentHash: hash,
matchedFields: ['invoiceNumber', 'supplier'],
confidence: 95
};
} else if (invoice.direction === 'outbound' && metadata.customerName === invoice.customer.name) {
// Same customer
return {
isDuplicate: true,
matchedInvoiceId: metadata.invoiceId,
matchedContentHash: hash,
matchedFields: ['invoiceNumber', 'customer'],
confidence: 95
};
}
}
// Check by amount and date within tolerance
const dateTolerance = 7 * 24 * 60 * 60 * 1000; // 7 days
const amountTolerance = 0.01;
if (
Math.abs(metadata.totalAmount - invoice.payableAmount) < amountTolerance &&
Math.abs(new Date(metadata.issueDate).getTime() - invoice.issueDate.getTime()) < dateTolerance &&
metadata.direction === invoice.direction
) {
if (
(invoice.direction === 'inbound' && metadata.supplierName === invoice.supplier.name) ||
(invoice.direction === 'outbound' && metadata.customerName === invoice.customer.name)
) {
return {
isDuplicate: true,
matchedInvoiceId: metadata.invoiceId,
matchedContentHash: hash,
matchedFields: ['amount', 'date', 'party'],
confidence: 85
};
}
}
}
return {
isDuplicate: false,
confidence: 0
};
}
/**
* Search invoices by filter
*/
public async searchInvoices(filter: IInvoiceFilter): Promise<IInvoiceMetadata[]> {
const results: IInvoiceMetadata[] = [];
for (const metadata of this.metadataCache.values()) {
if (this.matchesFilter(metadata, filter)) {
results.push(metadata);
}
}
// Sort by date descending
results.sort((a, b) =>
new Date(b.issueDate).getTime() - new Date(a.issueDate).getTime()
);
return results;
}
/**
* Get storage statistics
*/
public async getStatistics(): Promise<IStorageStats> {
let totalSize = 0;
let inboundCount = 0;
let outboundCount = 0;
for (const metadata of this.metadataCache.values()) {
if (metadata.direction === 'inbound') {
inboundCount++;
} else {
outboundCount++;
}
// Estimate size (would need actual file sizes in production)
totalSize += 50000; // Rough estimate
}
return {
totalInvoices: this.metadataCache.size,
inboundCount,
outboundCount,
totalSize,
duplicatesDetected: 0, // Would track this in production
lastUpdate: new Date()
};
}
/**
* Create EN16931 compliance report
*/
public async createComplianceReport(): Promise<void> {
const report = {
timestamp: new Date().toISOString(),
totalInvoices: this.metadataCache.size,
validInvoices: 0,
invalidInvoices: 0,
warnings: 0,
byFormat: {} as Record<string, number>,
byDirection: {
inbound: 0,
outbound: 0
},
validationErrors: [] as string[],
complianceLevel: 'EN16931',
validatorVersion: '5.1.4'
};
for (const metadata of this.metadataCache.values()) {
if (metadata.validationResult.isValid) {
report.validInvoices++;
} else {
report.invalidInvoices++;
}
report.warnings += metadata.validationResult.warnings;
if (metadata.direction === 'inbound') {
report.byDirection.inbound++;
} else {
report.byDirection.outbound++;
}
}
const reportPath = path.join(
this.exportPath,
'data',
'validation',
'en16931_compliance.json'
);
await plugins.smartfile.memory.toFs(
JSON.stringify(report, null, 2),
reportPath
);
}
/**
* Load registry from disk
*/
private async loadRegistry(): Promise<void> {
try {
if (await plugins.smartfile.fs.fileExists(this.registryPath)) {
const content = await plugins.smartfile.fs.toStringSync(this.registryPath);
const lines = content.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const entry: IInvoiceRegistryEntry = JSON.parse(line);
this.setCacheEntry(entry.hash, entry.metadata);
} catch (e) {
this.logger.log('warn', `Invalid registry entry: ${line}`);
}
}
this.logger.log('info', `Loaded ${this.metadataCache.size} invoices from registry`);
}
} catch (error) {
this.logger.log('error', `Failed to load registry: ${error}`);
}
}
/**
* Update registry with new entry
*/
private async updateRegistry(
invoiceId: string,
contentHash: string,
metadata: IInvoiceMetadata
): Promise<void> {
try {
const entry: IInvoiceRegistryEntry = {
id: invoiceId,
hash: contentHash,
metadata
};
// Append to NDJSON file
const line = JSON.stringify(entry) + '\n';
await plugins.smartfile.fs.ensureDir(path.dirname(this.registryPath));
// Use native fs for atomic append (better performance and concurrency safety)
const fs = await import('fs/promises');
await fs.appendFile(this.registryPath, line, 'utf8');
} catch (error) {
this.logger.log('error', `Failed to update registry: ${error}`);
}
}
/**
* Find invoice file by hash and extension
*/
private async findInvoiceFile(
contentHash: string,
extension: string
): Promise<string | null> {
const dirs = [
path.join(this.exportPath, 'data', 'documents', 'invoices', 'inbound'),
path.join(this.exportPath, 'data', 'documents', 'invoices', 'outbound')
];
for (const dir of dirs) {
const files = await plugins.smartfile.fs.listFileTree(dir, '**/*' + extension);
for (const file of files) {
if (file.includes(contentHash.substring(0, 8))) {
return path.join(dir, file);
}
}
}
return null;
}
/**
* Calculate SHA-256 hash
*/
private async calculateHash(data: string | Buffer): Promise<string> {
if (typeof data === 'string') {
return await plugins.smarthash.sha256FromString(data);
} else {
return await plugins.smarthash.sha256FromBuffer(data);
}
}
/**
* Check if metadata matches filter
*/
private matchesFilter(metadata: IInvoiceMetadata, filter: IInvoiceFilter): boolean {
if (filter.direction && metadata.direction !== filter.direction) {
return false;
}
if (filter.dateFrom && new Date(metadata.issueDate) < filter.dateFrom) {
return false;
}
if (filter.dateTo && new Date(metadata.issueDate) > filter.dateTo) {
return false;
}
if (filter.minAmount && metadata.totalAmount < filter.minAmount) {
return false;
}
if (filter.maxAmount && metadata.totalAmount > filter.maxAmount) {
return false;
}
if (filter.invoiceNumber && !metadata.invoiceNumber.includes(filter.invoiceNumber)) {
return false;
}
if (filter.supplierId && !metadata.supplierName.includes(filter.supplierId)) {
return false;
}
if (filter.customerId && !metadata.customerName.includes(filter.customerId)) {
return false;
}
return true;
}
/**
* Count errors in validation result
*/
private countErrors(validationResult?: IInvoice['validationResult']): number {
if (!validationResult) return 0;
return (
validationResult.syntax.errors.length +
validationResult.semantic.errors.length +
validationResult.businessRules.errors.length +
(validationResult.countrySpecific?.errors.length || 0)
);
}
/**
* Count warnings in validation result
*/
private countWarnings(validationResult?: IInvoice['validationResult']): number {
if (!validationResult) return 0;
return (
validationResult.syntax.warnings.length +
validationResult.semantic.warnings.length +
validationResult.businessRules.warnings.length +
(validationResult.countrySpecific?.warnings.length || 0)
);
}
/**
* Clean up old invoices (for testing only)
*/
public async cleanup(olderThanDays: number = 365): Promise<number> {
let removed = 0;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
for (const [hash, metadata] of this.metadataCache.entries()) {
if (new Date(metadata.issueDate) < cutoffDate) {
this.metadataCache.delete(hash);
removed++;
}
}
this.logger.log('info', `Removed ${removed} old invoices from cache`);
return removed;
}
/**
* Set cache entry with LRU eviction
*/
private setCacheEntry(key: string, value: IInvoiceMetadata): void {
// Remove from access order if already exists
const existingIndex = this.cacheAccessOrder.indexOf(key);
if (existingIndex > -1) {
this.cacheAccessOrder.splice(existingIndex, 1);
}
// Add to end (most recently used)
this.cacheAccessOrder.push(key);
this.metadataCache.set(key, value);
// Evict oldest if cache is too large
while (this.metadataCache.size > this.MAX_CACHE_SIZE) {
const oldestKey = this.cacheAccessOrder.shift();
if (oldestKey) {
this.metadataCache.delete(oldestKey);
this.logger.log('debug', `Evicted invoice from cache: ${oldestKey}`);
}
}
}
/**
* Get cache entry and update access order
*/
private getCacheEntry(key: string): IInvoiceMetadata | undefined {
const value = this.metadataCache.get(key);
if (value) {
// Move to end (most recently used)
const index = this.cacheAccessOrder.indexOf(key);
if (index > -1) {
this.cacheAccessOrder.splice(index, 1);
}
this.cacheAccessOrder.push(key);
}
return value;
}
/**
* Update metadata in storage and cache
*/
public async updateMetadata(contentHash: string, updates: Partial<IInvoiceMetadata>): Promise<void> {
const metadata = this.getCacheEntry(contentHash);
if (!metadata) {
this.logger.log('warn', `Cannot update metadata - invoice not found: ${contentHash}`);
return;
}
// Update metadata
const updatedMetadata = { ...metadata, ...updates };
this.setCacheEntry(contentHash, updatedMetadata);
// Persist to disk
const metadataPath = path.join(
this.exportPath,
'data',
'documents',
'invoices',
metadata.direction,
'metadata',
`${contentHash}.json`
);
await plugins.smartfile.memory.toFs(
JSON.stringify(updatedMetadata, null, 2),
metadataPath
);
this.logger.log('info', `Updated metadata for invoice: ${contentHash}`);
}
}

405
ts/skr.security.ts Normal file
View File

@@ -0,0 +1,405 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import * as crypto from 'crypto';
import * as https from 'https';
export interface ISigningOptions {
certificatePem?: string;
privateKeyPem?: string;
privateKeyPassphrase?: string;
timestampServerUrl?: string;
includeTimestamp?: boolean;
}
export interface ISignatureResult {
signature: string;
signatureFormat: 'CAdES-B' | 'CAdES-T' | 'CAdES-LT';
signingTime: string;
certificateChain?: string[];
timestampToken?: string;
timestampTime?: string;
}
export interface ITimestampResponse {
token: string;
time: string;
serverUrl: string;
hashAlgorithm: string;
}
export class SecurityManager {
private options: ISigningOptions;
private logger: plugins.smartlog.ConsoleLog;
constructor(options: ISigningOptions = {}) {
this.options = {
timestampServerUrl: options.timestampServerUrl || 'http://timestamp.digicert.com',
includeTimestamp: options.includeTimestamp !== false,
...options
};
this.logger = new plugins.smartlog.ConsoleLog();
}
/**
* Creates a CAdES-B (Basic) signature for data
*/
public async createCadesSignature(
data: Buffer | string,
certificatePem?: string,
privateKeyPem?: string
): Promise<ISignatureResult> {
const cert = certificatePem || this.options.certificatePem;
const key = privateKeyPem || this.options.privateKeyPem;
if (!cert || !key) {
throw new Error('Certificate and private key are required for signing');
}
try {
// Parse certificate and key
const certificate = plugins.nodeForge.pki.certificateFromPem(cert);
const privateKey = this.options.privateKeyPassphrase
? plugins.nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
: plugins.nodeForge.pki.privateKeyFromPem(key);
// Create PKCS#7 signed data (CMS)
const p7 = plugins.nodeForge.pkcs7.createSignedData();
// Add content
if (typeof data === 'string') {
p7.content = plugins.nodeForge.util.createBuffer(data, 'utf8');
} else {
p7.content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
}
// Add certificate
p7.addCertificate(certificate);
// Add signer
p7.addSigner({
key: privateKey,
certificate: certificate,
digestAlgorithm: plugins.nodeForge.pki.oids.sha256,
authenticatedAttributes: [
{
type: plugins.nodeForge.pki.oids.contentType,
value: plugins.nodeForge.pki.oids.data
},
{
type: plugins.nodeForge.pki.oids.messageDigest
},
{
type: plugins.nodeForge.pki.oids.signingTime,
value: new Date().toISOString()
}
]
});
// Sign the data
p7.sign({ detached: true });
// Convert to PEM
const pem = plugins.nodeForge.pkcs7.messageToPem(p7);
// Extract base64 signature
const signature = pem
.replace(/-----BEGIN PKCS7-----/, '')
.replace(/-----END PKCS7-----/, '')
.replace(/\r?\n/g, '');
const result: ISignatureResult = {
signature: signature,
signatureFormat: 'CAdES-B',
signingTime: new Date().toISOString(),
certificateChain: [cert]
};
// Add timestamp if requested
if (this.options.includeTimestamp && this.options.timestampServerUrl) {
try {
const timestampResponse = await this.requestTimestamp(signature);
result.timestampToken = timestampResponse.token;
result.timestampTime = timestampResponse.time;
result.signatureFormat = 'CAdES-T';
} catch (error) {
this.logger.log('warn', `Failed to obtain timestamp: ${error}`);
}
}
return result;
} catch (error) {
throw new Error(`Failed to create CAdES signature: ${error}`);
}
}
/**
* Requests an RFC 3161 timestamp from a TSA
*/
public async requestTimestamp(dataHash: string | Buffer): Promise<ITimestampResponse> {
try {
// Create hash of the data
let hash: Buffer;
if (typeof dataHash === 'string') {
hash = crypto.createHash('sha256').update(dataHash).digest();
} else {
hash = crypto.createHash('sha256').update(dataHash).digest();
}
// Create timestamp request (simplified - in production use proper ASN.1 encoding)
const tsRequest = this.createTimestampRequest(hash);
// Send request to TSA
const response = await this.sendTimestampRequest(tsRequest);
return {
token: response.toString('base64'),
time: new Date().toISOString(),
serverUrl: this.options.timestampServerUrl!,
hashAlgorithm: 'sha256'
};
} catch (error) {
throw new Error(`Failed to obtain timestamp: ${error}`);
}
}
/**
* Creates a timestamp request (simplified version)
*/
private createTimestampRequest(hash: Buffer): Buffer {
// In production, use proper ASN.1 encoding library
// This is a simplified placeholder
const request = {
version: 1,
messageImprint: {
hashAlgorithm: { algorithm: '2.16.840.1.101.3.4.2.1' }, // SHA-256 OID
hashedMessage: hash
},
reqPolicy: null,
nonce: crypto.randomBytes(8),
certReq: true
};
// Convert to DER-encoded ASN.1 (simplified)
return Buffer.from(JSON.stringify(request));
}
/**
* Sends timestamp request to TSA server
*/
private async sendTimestampRequest(request: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
const url = new URL(this.options.timestampServerUrl!);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/timestamp-query',
'Content-Length': request.length
}
};
const req = https.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const response = Buffer.concat(chunks);
if (res.statusCode === 200) {
resolve(response);
} else {
reject(new Error(`TSA server returned status ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.write(request);
req.end();
});
}
/**
* Verifies a CAdES signature
*/
public async verifyCadesSignature(
data: Buffer | string,
signature: string,
certificatePem?: string
): Promise<boolean> {
try {
// Add PEM headers if not present
let pemSignature = signature;
if (!signature.includes('BEGIN PKCS7')) {
pemSignature = `-----BEGIN PKCS7-----\n${signature}\n-----END PKCS7-----`;
}
// Parse the PKCS#7 message
const p7 = plugins.nodeForge.pkcs7.messageFromPem(pemSignature);
// Prepare content for verification
let content: plugins.nodeForge.util.ByteStringBuffer;
if (typeof data === 'string') {
content = plugins.nodeForge.util.createBuffer(data, 'utf8');
} else {
content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
}
// Verify the signature
const verified = (p7 as any).verify({
content: content,
detached: true
});
return verified;
} catch (error) {
this.logger.log('error', `Signature verification failed: ${error}`);
return false;
}
}
/**
* Generates a self-signed certificate for testing
*/
public async generateSelfSignedCertificate(
commonName: string = 'SKR Export System',
validDays: number = 365
): Promise<{ certificate: string; privateKey: string }> {
const keys = plugins.nodeForge.pki.rsa.generateKeyPair(2048);
const cert = plugins.nodeForge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + validDays);
const attrs = [
{ name: 'commonName', value: commonName },
{ name: 'countryName', value: 'DE' },
{ name: 'organizationName', value: 'SKR Export System' },
{ shortName: 'OU', value: 'Accounting' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([
{
name: 'basicConstraints',
cA: true
},
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true
},
{
name: 'nsCertType',
client: true,
server: true,
email: true,
objsign: true,
sslCA: true,
emailCA: true,
objCA: true
},
{
name: 'subjectAltName',
altNames: [
{ type: 2, value: commonName }
]
}
]);
// Self-sign certificate
cert.sign(keys.privateKey, plugins.nodeForge.md.sha256.create());
// Convert to PEM
const certificatePem = plugins.nodeForge.pki.certificateToPem(cert);
const privateKeyPem = plugins.nodeForge.pki.privateKeyToPem(keys.privateKey);
return {
certificate: certificatePem,
privateKey: privateKeyPem
};
}
/**
* Creates a detached signature file
*/
public async createDetachedSignature(
dataPath: string,
outputPath: string
): Promise<void> {
const data = await plugins.smartfile.fs.toBuffer(dataPath);
const signature = await this.createCadesSignature(data);
const signatureData = {
signature: signature.signature,
format: signature.signatureFormat,
signingTime: signature.signingTime,
timestamp: signature.timestampToken,
timestampTime: signature.timestampTime,
algorithm: 'SHA256withRSA',
signedFile: path.basename(dataPath)
};
await plugins.smartfile.memory.toFs(
JSON.stringify(signatureData, null, 2),
outputPath
);
}
/**
* Verifies a detached signature file
*/
public async verifyDetachedSignature(
dataPath: string,
signaturePath: string
): Promise<boolean> {
try {
const data = await plugins.smartfile.fs.toBuffer(dataPath);
const signatureJson = await plugins.smartfile.fs.toStringSync(signaturePath);
const signatureData = JSON.parse(signatureJson);
return await this.verifyCadesSignature(data, signatureData.signature);
} catch (error) {
this.logger.log('error', `Failed to verify detached signature: ${error}`);
return false;
}
}
/**
* Adds Long-Term Validation (LTV) information
*/
public async addLtvInformation(
signature: ISignatureResult,
ocspResponse?: Buffer,
crlData?: Buffer
): Promise<ISignatureResult> {
// Add OCSP response and CRL data for long-term validation
const ltv = {
...signature,
signatureFormat: 'CAdES-LT' as const,
ocsp: ocspResponse?.toString('base64'),
crl: crlData?.toString('base64'),
ltvTime: new Date().toISOString()
};
return ltv;
}
}

View File

@@ -136,6 +136,7 @@ export interface ITransactionFilter {
export interface IDatabaseConfig {
mongoDbUrl: string;
dbName?: string;
invoiceExportPath?: string; // Optional path for invoice storage
}
export interface IReportParams {