/** * SQLite store for company legal documents (`agb` — Allgemeine Geschäfts- * bedingungen, `avv` — Auftragsverarbeitungsvertrag Art. 28 DSGVO), editable at Settings → Legal Documents and * rendered as a branded PDF (server/utils/offers/agbPdf.ts) for contract * annexes, website export and the public signing portal. * * Frontend-owned operational content — NOT iDempiere business data. Same * fail-soft pattern as chatDb.ts / emailVerifyDb.ts: lazy open, every export * degrades to the bundled default (server/utils/offers/agbDefault.ts) on DB * failure so the server never crashes and contracts can always be sent. * * Every save keeps the previous text as a version row (`legal_document_versions`) * so an accidental edit can be recovered. */ import Database from 'better-sqlite3' import path from 'path' import fs from 'fs' import { AGB_DEFAULT_HTML_DE, AGB_DEFAULT_HTML_EN, AGB_DEFAULT_TITLE_DE, AGB_DEFAULT_TITLE_EN, AVV_DEFAULT_HTML_DE, AVV_DEFAULT_HTML_EN, AVV_DEFAULT_TITLE_DE, AVV_DEFAULT_TITLE_EN } from './offers/agbDefault' /** Documents managed at Settings → Legal Documents. */ export const LEGAL_DOC_KEYS = ['agb', 'avv'] as const export type LegalDocKey = typeof LEGAL_DOC_KEYS[number] export const isLegalDocKey = (k: any): k is LegalDocKey => (LEGAL_DOC_KEYS as readonly string[]).includes(String(k)) export interface LegalDocument { docKey: string language: 'de' | 'en' title: string contentHtml: string version: number updatedAt: string | null updatedBy: string | null isDefault: boolean } let db: any = null let initFailed = false const getDb = () => { if (db) return db if (initFailed) return null try { const dbPath = path.join(process.cwd(), 'data', 'legal-docs.db') const dataDir = path.dirname(dbPath) if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) const _db = new Database(dbPath) _db.pragma('journal_mode = WAL') _db.exec(` CREATE TABLE IF NOT EXISTS legal_documents ( doc_key TEXT NOT NULL, language TEXT NOT NULL, title TEXT NOT NULL, content_html TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL, updated_by TEXT, PRIMARY KEY (doc_key, language) ); CREATE TABLE IF NOT EXISTS legal_document_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, doc_key TEXT NOT NULL, language TEXT NOT NULL, version INTEGER NOT NULL, title TEXT NOT NULL, content_html TEXT NOT NULL, saved_at TEXT NOT NULL, saved_by TEXT ); CREATE INDEX IF NOT EXISTS idx_ldv_doc ON legal_document_versions(doc_key, language, version); `) db = _db console.log('[LegalDocsDB] ✅ SQLite database initialized at:', dbPath) return db } catch (err: any) { initFailed = true console.error('[LegalDocsDB] ❌ init failed; legal documents fall back to bundled defaults:', err?.message) return null } } const defaultFor = (docKey: string, language: 'de' | 'en'): LegalDocument => { if (docKey === 'agb') { return { docKey, language, title: language === 'en' ? AGB_DEFAULT_TITLE_EN : AGB_DEFAULT_TITLE_DE, contentHtml: language === 'en' ? AGB_DEFAULT_HTML_EN : AGB_DEFAULT_HTML_DE, version: 0, updatedAt: null, updatedBy: null, isDefault: true } } if (docKey === 'avv') { return { docKey, language, title: language === 'en' ? AVV_DEFAULT_TITLE_EN : AVV_DEFAULT_TITLE_DE, contentHtml: language === 'en' ? AVV_DEFAULT_HTML_EN : AVV_DEFAULT_HTML_DE, version: 0, updatedAt: null, updatedBy: null, isDefault: true } } return { docKey, language, title: '', contentHtml: '', version: 0, updatedAt: null, updatedBy: null, isDefault: true } } export const normalizeLegalLanguage = (l: any): 'de' | 'en' => (String(l || '').toLowerCase().startsWith('en') ? 'en' : 'de') /** Stored row, or the bundled default when nothing was saved yet / DB unavailable. */ export const getLegalDocument = (docKey: string, language: any): LegalDocument => { const lang = normalizeLegalLanguage(language) const d = getDb() if (!d) return defaultFor(docKey, lang) try { const row = d.prepare('SELECT * FROM legal_documents WHERE doc_key = ? AND language = ?').get(docKey, lang) if (!row) return defaultFor(docKey, lang) return { docKey, language: lang, title: row.title, contentHtml: row.content_html, version: Number(row.version) || 1, updatedAt: row.updated_at, updatedBy: row.updated_by, isDefault: false } } catch (err: any) { console.error('[LegalDocsDB] read failed:', err?.message) return defaultFor(docKey, lang) } } /** * The document to actually USE for a language: the requested language when it * has content, else the German original (the only authoritative version). */ export const getLegalDocumentWithFallback = (docKey: string, language: any): LegalDocument => { const doc = getLegalDocument(docKey, language) if (String(doc.contentHtml || '').trim()) return doc return getLegalDocument(docKey, 'de') } export const saveLegalDocument = (docKey: string, language: any, title: string, contentHtml: string, updatedBy: string): LegalDocument => { const lang = normalizeLegalLanguage(language) const d = getDb() if (!d) throw new Error('Legal document store unavailable') const now = new Date().toISOString() const tx = d.transaction(() => { const existing = d.prepare('SELECT * FROM legal_documents WHERE doc_key = ? AND language = ?').get(docKey, lang) if (existing) { d.prepare('INSERT INTO legal_document_versions (doc_key, language, version, title, content_html, saved_at, saved_by) VALUES (?, ?, ?, ?, ?, ?, ?)') .run(docKey, lang, existing.version, existing.title, existing.content_html, existing.updated_at, existing.updated_by) d.prepare('UPDATE legal_documents SET title = ?, content_html = ?, version = version + 1, updated_at = ?, updated_by = ? WHERE doc_key = ? AND language = ?') .run(title, contentHtml, now, updatedBy, docKey, lang) } else { d.prepare('INSERT INTO legal_documents (doc_key, language, title, content_html, version, updated_at, updated_by) VALUES (?, ?, ?, ?, 1, ?, ?)') .run(docKey, lang, title, contentHtml, now, updatedBy) } }) tx() return getLegalDocument(docKey, lang) } export const listLegalDocumentVersions = (docKey: string, language: any, limit = 20) => { const d = getDb() if (!d) return [] try { return d.prepare('SELECT id, version, title, saved_at AS savedAt, saved_by AS savedBy, length(content_html) AS size FROM legal_document_versions WHERE doc_key = ? AND language = ? ORDER BY version DESC LIMIT ?') .all(docKey, normalizeLegalLanguage(language), limit) } catch { return [] } } export const getLegalDocumentVersion = (id: number) => { const d = getDb() if (!d) return null try { return d.prepare('SELECT * FROM legal_document_versions WHERE id = ?').get(id) || null } catch { return null } }