import * as plugins from '../../plugins.js'; import * as path from 'path'; import { promises as fs } from 'fs'; /** * Schematron rule sources */ export interface SchematronSource { name: string; version: string; url: string; description: string; format: 'UBL' | 'CII' | 'BOTH'; } /** * Official Schematron sources for e-invoicing standards */ export const SCHEMATRON_SOURCES: Record = { EN16931: [ { name: 'EN16931-UBL', version: '1.3.14', url: 'https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/ubl/schematron/EN16931-UBL-validation.sch', description: 'Official EN16931 validation rules for UBL format', format: 'UBL' }, { name: 'EN16931-CII', version: '1.3.14', url: 'https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/cii/schematron/EN16931-CII-validation.sch', description: 'Official EN16931 validation rules for CII format', format: 'CII' }, { name: 'EN16931-EDIFACT', version: '1.3.14', url: 'https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/edifact/schematron/EN16931-EDIFACT-validation.sch', description: 'Official EN16931 validation rules for EDIFACT format', format: 'CII' } ], XRECHNUNG: [ { name: 'XRechnung-UBL', version: '3.0.2', url: 'https://github.com/itplr-kosit/xrechnung-schematron/raw/master/src/validation/schematron/ubl/XRechnung-UBL-validation.sch', description: 'XRechnung CIUS validation for UBL', format: 'UBL' }, { name: 'XRechnung-CII', version: '3.0.2', url: 'https://github.com/itplr-kosit/xrechnung-schematron/raw/master/src/validation/schematron/cii/XRechnung-CII-validation.sch', description: 'XRechnung CIUS validation for CII', format: 'CII' } ], PEPPOL: [ { name: 'PEPPOL-EN16931-UBL', version: '3.0.17', url: 'https://github.com/OpenPEPPOL/peppol-bis-invoice-3/raw/master/rules/sch/PEPPOL-EN16931-UBL.sch', description: 'PEPPOL BIS Billing 3.0 validation rules for UBL', format: 'UBL' }, { name: 'PEPPOL-EN16931-CII', version: '3.0.17', url: 'https://github.com/OpenPEPPOL/peppol-bis-invoice-3/raw/master/rules/sch/PEPPOL-EN16931-CII.sch', description: 'PEPPOL BIS Billing 3.0 validation rules for CII', format: 'CII' } ] }; /** * Schematron downloader and cache manager */ export class SchematronDownloader { private cacheDir: string; private smartfile: any; constructor(cacheDir: string = 'assets_downloaded/schematron') { this.cacheDir = cacheDir; } /** * Initialize the downloader */ public async initialize(): Promise { // Ensure cache directory exists this.smartfile = await import('@push.rocks/smartfile'); await fs.mkdir(this.cacheDir, { recursive: true }); } /** * Download a Schematron file */ public async download(source: SchematronSource): Promise { const fileName = `${source.name}-v${source.version}.sch`; const filePath = path.join(this.cacheDir, fileName); // Check if already cached if (await this.isCached(filePath)) { console.log(`Using cached Schematron: ${fileName}`); return filePath; } console.log(`Downloading Schematron: ${source.name} v${source.version}`); try { // Download the file const response = await fetch(source.url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const content = await response.text(); // Validate it's actually Schematron if (!content.includes('schematron') && !content.includes('sch:schema')) { throw new Error('Downloaded file does not appear to be Schematron'); } // Save to cache await fs.writeFile(filePath, content, 'utf-8'); // Also save metadata const metaPath = filePath.replace('.sch', '.meta.json'); await fs.writeFile(metaPath, JSON.stringify({ source: source.name, version: source.version, url: source.url, format: source.format, downloadDate: new Date().toISOString() }, null, 2), 'utf-8'); console.log(`Successfully downloaded: ${fileName}`); return filePath; } catch (error) { throw new Error(`Failed to download ${source.name}: ${error.message}`); } } /** * Download all Schematron files for a standard */ public async downloadStandard( standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' ): Promise { const sources = SCHEMATRON_SOURCES[standard]; if (!sources) { throw new Error(`Unknown standard: ${standard}`); } const paths: string[] = []; for (const source of sources) { try { const path = await this.download(source); paths.push(path); } catch (error) { console.warn(`Failed to download ${source.name}: ${error.message}`); } } return paths; } /** * Check if a file is cached */ private async isCached(filePath: string): Promise { try { await fs.access(filePath); // Check if file is not empty const stats = await fs.stat(filePath); return stats.size > 0; } catch { return false; } } /** * Get cached Schematron files */ public async getCachedFiles(): Promise> { const files: Array<{ path: string; metadata: any }> = []; try { const entries = await fs.readdir(this.cacheDir); for (const entry of entries) { if (entry.endsWith('.sch')) { const filePath = path.join(this.cacheDir, entry); const metaPath = filePath.replace('.sch', '.meta.json'); try { const metadata = JSON.parse(await fs.readFile(metaPath, 'utf-8')); files.push({ path: filePath, metadata }); } catch { // No metadata file files.push({ path: filePath, metadata: null }); } } } } catch (error) { console.warn(`Failed to list cached files: ${error.message}`); } return files; } /** * Clear cache */ public async clearCache(): Promise { try { const entries = await fs.readdir(this.cacheDir); for (const entry of entries) { if (entry.endsWith('.sch') || entry.endsWith('.meta.json')) { await fs.unlink(path.join(this.cacheDir, entry)); } } console.log('Schematron cache cleared'); } catch (error) { console.warn(`Failed to clear cache: ${error.message}`); } } /** * Get the appropriate Schematron for a format */ public async getSchematronForFormat( standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL', format: 'UBL' | 'CII' ): Promise { const sources = SCHEMATRON_SOURCES[standard]; if (!sources) return null; const source = sources.find(s => s.format === format || s.format === 'BOTH'); if (!source) return null; return await this.download(source); } /** * Update all cached Schematron files */ public async updateAll(): Promise { console.log('Updating all Schematron files...'); for (const standard of ['EN16931', 'XRECHNUNG', 'PEPPOL'] as const) { await this.downloadStandard(standard); } console.log('All Schematron files updated'); } } /** * ISO Schematron skeleton URLs * These are needed to compile Schematron to XSLT */ export const ISO_SCHEMATRON_SKELETONS = { 'iso_dsdl_include.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_dsdl_include.xsl', 'iso_abstract_expand.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_abstract_expand.xsl', 'iso_svrl_for_xslt2.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_svrl_for_xslt2.xsl', 'iso_schematron_skeleton_for_saxon.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_schematron_skeleton_for_saxon.xsl' }; /** * Download ISO Schematron skeleton files */ export async function downloadISOSkeletons(targetDir: string = 'assets_downloaded/schematron/iso'): Promise { await fs.mkdir(targetDir, { recursive: true }); console.log('Downloading ISO Schematron skeleton files...'); for (const [name, url] of Object.entries(ISO_SCHEMATRON_SKELETONS)) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const content = await response.text(); const filePath = path.join(targetDir, name); await fs.writeFile(filePath, content, 'utf-8'); console.log(`Downloaded: ${name}`); } catch (error) { console.warn(`Failed to download ${name}: ${error.message}`); } } console.log('ISO Schematron skeleton download complete'); }