#!/usr/bin/env tsx /** * Downloads official XRechnung Schematron validation rules * from the KoSIT repositories */ import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; const XRECHNUNG_VERSION = '3.0.2'; // Latest version as of 2025 const VALIDATOR_VERSION = '2025-07-31'; // Next release date const REPOS = { schematron: { url: 'https://github.com/itplr-kosit/xrechnung-schematron/archive/refs/tags/release-3.0.2.zip', dir: 'xrechnung-schematron' }, validator: { url: 'https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/release-2024-07-31/validator-configuration-xrechnung_3.0.1_2024-07-31.zip', dir: 'xrechnung-validator' } }; const ASSETS_DIR = path.join(process.cwd(), 'assets', 'schematron', 'xrechnung'); async function downloadFile(url: string, destination: string): Promise { console.log(`Downloading ${url}...`); try { // Use curl to download the file execSync(`curl -L -o "${destination}" "${url}"`, { stdio: 'inherit' }); console.log(`Downloaded to ${destination}`); } catch (error) { console.error(`Failed to download ${url}:`, error); throw error; } } async function extractZip(zipFile: string, destination: string): Promise { console.log(`Extracting ${zipFile}...`); try { // Create destination directory if it doesn't exist fs.mkdirSync(destination, { recursive: true }); // Extract using unzip execSync(`unzip -o "${zipFile}" -d "${destination}"`, { stdio: 'inherit' }); console.log(`Extracted to ${destination}`); } catch (error) { console.error(`Failed to extract ${zipFile}:`, error); throw error; } } async function downloadXRechnungRules(): Promise { console.log('Starting XRechnung Schematron rules download...\n'); // Create assets directory fs.mkdirSync(ASSETS_DIR, { recursive: true }); const tempDir = path.join(ASSETS_DIR, 'temp'); fs.mkdirSync(tempDir, { recursive: true }); // Download and extract Schematron rules console.log('1. Downloading XRechnung Schematron rules...'); const schematronZip = path.join(tempDir, 'xrechnung-schematron.zip'); await downloadFile(REPOS.schematron.url, schematronZip); const schematronDir = path.join(ASSETS_DIR, REPOS.schematron.dir); await extractZip(schematronZip, schematronDir); // Find the actual Schematron files const schematronExtractedDir = path.join(schematronDir, `xrechnung-schematron-release-${XRECHNUNG_VERSION}`); const schematronValidationDir = path.join(schematronExtractedDir, 'validation', 'schematron'); if (fs.existsSync(schematronValidationDir)) { console.log('\nFound Schematron validation files:'); // List UBL Schematron files const ublDir = path.join(schematronValidationDir, 'ubl-inv'); if (fs.existsSync(ublDir)) { const ublFiles = fs.readdirSync(ublDir).filter(f => f.endsWith('.sch') || f.endsWith('.xsl')); console.log(' UBL Invoice Schematron:', ublFiles.join(', ')); } // List CII Schematron files const ciiDir = path.join(schematronValidationDir, 'cii'); if (fs.existsSync(ciiDir)) { const ciiFiles = fs.readdirSync(ciiDir).filter(f => f.endsWith('.sch') || f.endsWith('.xsl')); console.log(' CII Schematron:', ciiFiles.join(', ')); } // Copy to final location const finalUblDir = path.join(ASSETS_DIR, 'ubl'); const finalCiiDir = path.join(ASSETS_DIR, 'cii'); fs.mkdirSync(finalUblDir, { recursive: true }); fs.mkdirSync(finalCiiDir, { recursive: true }); // Copy UBL files if (fs.existsSync(ublDir)) { const ublFiles = fs.readdirSync(ublDir); for (const file of ublFiles) { if (file.endsWith('.sch') || file.endsWith('.xsl')) { fs.copyFileSync( path.join(ublDir, file), path.join(finalUblDir, file) ); } } console.log(`\nCopied UBL Schematron files to ${finalUblDir}`); } // Copy CII files if (fs.existsSync(ciiDir)) { const ciiFiles = fs.readdirSync(ciiDir); for (const file of ciiFiles) { if (file.endsWith('.sch') || file.endsWith('.xsl')) { fs.copyFileSync( path.join(ciiDir, file), path.join(finalCiiDir, file) ); } } console.log(`Copied CII Schematron files to ${finalCiiDir}`); } } // Download validator configuration (contains additional rules and scenarios) console.log('\n2. Downloading XRechnung validator configuration...'); const validatorZip = path.join(tempDir, 'xrechnung-validator.zip'); await downloadFile(REPOS.validator.url, validatorZip); const validatorDir = path.join(ASSETS_DIR, REPOS.validator.dir); await extractZip(validatorZip, validatorDir); // Create metadata file const metadata = { version: XRECHNUNG_VERSION, validatorVersion: VALIDATOR_VERSION, downloadDate: new Date().toISOString(), sources: { schematron: REPOS.schematron.url, validator: REPOS.validator.url }, files: { ubl: fs.existsSync(path.join(ASSETS_DIR, 'ubl')) ? fs.readdirSync(path.join(ASSETS_DIR, 'ubl')).filter(f => f.endsWith('.sch')) : [], cii: fs.existsSync(path.join(ASSETS_DIR, 'cii')) ? fs.readdirSync(path.join(ASSETS_DIR, 'cii')).filter(f => f.endsWith('.sch')) : [] } }; fs.writeFileSync( path.join(ASSETS_DIR, 'metadata.json'), JSON.stringify(metadata, null, 2) ); // Clean up temp directory console.log('\n3. Cleaning up...'); fs.rmSync(tempDir, { recursive: true, force: true }); console.log('\nāœ… XRechnung Schematron rules downloaded successfully!'); console.log(`šŸ“ Files are located in: ${ASSETS_DIR}`); console.log('\nNext steps:'); console.log('1. Run Saxon-JS to compile .sch files to SEF format'); console.log('2. Integrate with SchematronValidator'); console.log('3. Add XRechnung-specific TypeScript validators'); } // Run if executed directly if (import.meta.url === `file://${process.argv[1]}`) { downloadXRechnungRules().catch(error => { console.error('Failed to download XRechnung rules:', error); process.exit(1); }); } export default downloadXRechnungRules;