Files
skr/ts/skr.export.ts
Juergen Kunz 73b46f7857
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
feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
2025-08-12 12:37:01 +00:00

443 lines
14 KiB
TypeScript

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;
}
}