326 lines
9.6 KiB
TypeScript
326 lines
9.6 KiB
TypeScript
/**
|
||
* 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<T>(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<T>(items: T[], keyFn: (item: T) => string): T[] {
|
||
const map = new Map<string, T>();
|
||
for (const item of items) {
|
||
map.set(keyFn(item), item);
|
||
}
|
||
return Array.from(map.values());
|
||
}
|
||
|
||
async function seedProviders() {
|
||
const items = dedupe(readJsonArrays<ProviderDef>(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<CancellationPeriodDef>(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<ContractDurationDef>(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<ContractCategoryDef>(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<PdfTemplateDef>(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());
|