/** * Seed-Script: Factory-Defaults aus backend/factory-defaults/ in die DB einspielen. * * - Liest alle JSON-Dateien aus den Unterordnern (providers/, contract-meta/, pdf-templates/) * - Merged mehrere Dateien pro Kategorie automatisch * - Nutzt Prisma upsert → idempotent, kann mehrfach aufgerufen werden * - Kopiert PDF-Dateien aus pdf-templates/ nach uploads/pdf-templates/ * * Aufruf: * npm run seed:defaults */ import fs from 'fs'; import path from 'path'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const ROOT = path.join(process.cwd(), 'factory-defaults'); const UPLOADS_ROOT = path.join(process.cwd(), 'uploads'); const PDF_UPLOAD_DIR = path.join(UPLOADS_ROOT, 'pdf-templates'); interface ProviderDef { name: string; portalUrl?: string | null; usernameFieldName?: string | null; passwordFieldName?: string | null; isActive?: boolean; tariffs?: { name: string; isActive?: boolean }[]; } interface CancellationPeriodDef { code: string; description: string; isActive?: boolean; } interface ContractDurationDef { code: string; description: string; isActive?: boolean; } interface ContractCategoryDef { code: string; name: string; icon?: string | null; color?: string | null; sortOrder?: number; isActive?: boolean; } interface PdfTemplateDef { name: string; description?: string | null; providerName?: string | null; originalName: string; fieldMapping: any; phoneFieldPrefix?: string | null; maxPhoneFields?: number | null; isActive?: boolean; pdfFilename: string; // Dateiname im pdf-templates/-Ordner } /** * Liest alle *.json Dateien aus einem Ordner und gibt die zusammengeführten Arrays zurück. */ function readJsonArrays(dir: string): T[] { if (!fs.existsSync(dir)) return []; const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')); const result: T[] = []; for (const f of files) { const content = fs.readFileSync(path.join(dir, f), 'utf-8'); try { const data = JSON.parse(content); if (Array.isArray(data)) { result.push(...data); } } catch (e) { console.warn(`⚠ Konnte ${f} nicht parsen – überspringe.`); } } return result; } /** * Dedupliziert Einträge per unique-Key (letzter Eintrag gewinnt). */ function dedupe(items: T[], keyFn: (item: T) => string): T[] { const map = new Map(); for (const item of items) { map.set(keyFn(item), item); } return Array.from(map.values()); } async function seedProviders() { const items = dedupe(readJsonArrays(path.join(ROOT, 'providers')), (p) => p.name); if (items.length === 0) { console.log(' providers/ – keine Einträge gefunden'); return; } let providerCount = 0; let tariffCount = 0; for (const p of items) { const provider = await prisma.provider.upsert({ where: { name: p.name }, update: { portalUrl: p.portalUrl ?? null, usernameFieldName: p.usernameFieldName ?? null, passwordFieldName: p.passwordFieldName ?? null, isActive: p.isActive ?? true, }, create: { name: p.name, portalUrl: p.portalUrl ?? null, usernameFieldName: p.usernameFieldName ?? null, passwordFieldName: p.passwordFieldName ?? null, isActive: p.isActive ?? true, }, }); providerCount++; if (p.tariffs && p.tariffs.length > 0) { for (const t of p.tariffs) { await prisma.tariff.upsert({ where: { providerId_name: { providerId: provider.id, name: t.name } }, update: { isActive: t.isActive ?? true }, create: { providerId: provider.id, name: t.name, isActive: t.isActive ?? true, }, }); tariffCount++; } } } console.log(` ✓ Anbieter: ${providerCount}, Tarife: ${tariffCount}`); } async function seedCancellationPeriods() { const items = dedupe( readJsonArrays(path.join(ROOT, 'contract-meta')).filter((i) => i.code && i.description), (i) => i.code, ); // Nur die relevanten Objekte (CancellationPeriod hat code+description, keine 'name') const relevant = items.filter((i) => 'code' in i && 'description' in i && !('name' in i) && !('icon' in i)); if (relevant.length === 0) { console.log(' cancellation-periods – keine Einträge'); return; } for (const c of relevant) { await prisma.cancellationPeriod.upsert({ where: { code: c.code }, update: { description: c.description, isActive: c.isActive ?? true }, create: { code: c.code, description: c.description, isActive: c.isActive ?? true }, }); } console.log(` ✓ Kündigungsfristen: ${relevant.length}`); } async function seedContractDurations() { const items = dedupe( readJsonArrays(path.join(ROOT, 'contract-meta')).filter((i) => i.code && i.description), (i) => i.code, ); const relevant = items.filter((i) => !('name' in i) && !('icon' in i)); if (relevant.length === 0) { console.log(' contract-durations – keine Einträge'); return; } for (const d of relevant) { await prisma.contractDuration.upsert({ where: { code: d.code }, update: { description: d.description, isActive: d.isActive ?? true }, create: { code: d.code, description: d.description, isActive: d.isActive ?? true }, }); } console.log(` ✓ Laufzeiten: ${relevant.length}`); } async function seedContractCategories() { const items = dedupe( readJsonArrays(path.join(ROOT, 'contract-meta')).filter((i) => i.code && (i as any).name), (i) => i.code, ); if (items.length === 0) { console.log(' contract-categories – keine Einträge'); return; } for (const c of items) { await prisma.contractCategory.upsert({ where: { code: c.code }, update: { name: c.name, icon: c.icon ?? null, color: c.color ?? null, sortOrder: c.sortOrder ?? 0, isActive: c.isActive ?? true, }, create: { code: c.code, name: c.name, icon: c.icon ?? null, color: c.color ?? null, sortOrder: c.sortOrder ?? 0, isActive: c.isActive ?? true, }, }); } console.log(` ✓ Vertragskategorien: ${items.length}`); } async function seedPdfTemplates() { const items = dedupe( readJsonArrays(path.join(ROOT, 'pdf-templates')), (t) => t.name, ); if (items.length === 0) { console.log(' pdf-templates – keine Einträge'); return; } // Upload-Verzeichnis sicherstellen if (!fs.existsSync(PDF_UPLOAD_DIR)) { fs.mkdirSync(PDF_UPLOAD_DIR, { recursive: true }); } let count = 0; let skipped = 0; for (const t of items) { const srcPdf = path.join(ROOT, 'pdf-templates', t.pdfFilename); if (!fs.existsSync(srcPdf)) { console.warn(` ⚠ PDF fehlt: ${t.pdfFilename} – Template "${t.name}" übersprungen`); skipped++; continue; } // PDF nach uploads/pdf-templates/ kopieren (mit eindeutigem Namen) const ext = path.extname(t.originalName || t.pdfFilename) || '.pdf'; const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const destFilename = `seed-${t.name.replace(/[^a-zA-Z0-9]/g, '-')}-${uniqueSuffix}${ext}`; const destPdf = path.join(PDF_UPLOAD_DIR, destFilename); const relativePath = `/uploads/pdf-templates/${destFilename}`; fs.copyFileSync(srcPdf, destPdf); const fieldMappingJson = JSON.stringify(t.fieldMapping || {}); // Bei existierendem Template: alten Pfad löschen, wenn Neuimport const existing = await prisma.pdfTemplate.findUnique({ where: { name: t.name } }); if (existing?.templatePath) { const oldRel = existing.templatePath.startsWith('/uploads/') ? existing.templatePath.substring('/uploads/'.length) : existing.templatePath; const oldAbs = path.join(UPLOADS_ROOT, oldRel); if (fs.existsSync(oldAbs)) { try { fs.unlinkSync(oldAbs); } catch { // ignore } } } await prisma.pdfTemplate.upsert({ where: { name: t.name }, update: { description: t.description ?? null, providerName: t.providerName ?? null, templatePath: relativePath, originalName: t.originalName, fieldMapping: fieldMappingJson, phoneFieldPrefix: t.phoneFieldPrefix ?? null, maxPhoneFields: t.maxPhoneFields ?? 8, isActive: t.isActive ?? true, }, create: { name: t.name, description: t.description ?? null, providerName: t.providerName ?? null, templatePath: relativePath, originalName: t.originalName, fieldMapping: fieldMappingJson, phoneFieldPrefix: t.phoneFieldPrefix ?? null, maxPhoneFields: t.maxPhoneFields ?? 8, isActive: t.isActive ?? true, }, }); count++; } console.log(` ✓ PDF-Vorlagen: ${count}${skipped > 0 ? ` (${skipped} übersprungen)` : ''}`); } async function main() { console.log('\n📦 Factory-Defaults werden eingespielt...\n'); if (!fs.existsSync(ROOT)) { console.error(`❌ Ordner nicht gefunden: ${ROOT}`); console.error(' Lege Export-Dateien unter backend/factory-defaults/ ab.'); process.exit(1); } await seedProviders(); await seedCancellationPeriods(); await seedContractDurations(); await seedContractCategories(); await seedPdfTemplates(); console.log('\n✅ Factory-Defaults erfolgreich eingespielt.\n'); } main() .catch((e) => { console.error('\n❌ Fehler beim Einspielen:', e); process.exit(1); }) .finally(() => prisma.$disconnect());