// AOS seed script — realistic German software agency dataset // Run with: bun run src/lib/seed.ts import { db } from './db' const CENTS = (euro: number) => Math.round(euro * 100) const daysFromNow = (d: number) => new Date(Date.now() + d * 86400000) async function main() { console.log('Seeding AOS…') // --- Users / RBAC ------------------------------------------------------- const users = [ { email: 'max@nordlicht.digital', name: 'Max Brandt', role: 'owner' }, { email: 'lena@nordlicht.digital', name: 'Lena Vogt', role: 'admin' }, { email: 'sven@nordlicht.digital', name: 'Sven Hoffmann', role: 'sales' }, { email: 'aylin@nordlicht.digital', name: 'Aylin Demir', role: 'pm' }, { email: 'tom@nordlicht.digital', name: 'Tom Becker', role: 'developer' }, { email: 'nora@nordlicht.digital', name: 'Nora Klein', role: 'finance' }, { email: 'portal@bauer-handwerk.de', name: 'Klaus Bauer', role: 'client' }, ] for (const u of users) { await db.user.upsert({ where: { email: u.email }, update: {}, create: u, }) } // --- Modules registry --------------------------------------------------- const modules = [ { id: 'crm', enabled: true, version: '1.0.0' }, { id: 'leads', enabled: true, version: '1.0.0' }, { id: 'projects', enabled: true, version: '1.0.0' }, { id: 'licenses', enabled: true, version: '1.0.0' }, { id: 'contracts', enabled: true, version: '1.0.0' }, { id: 'expenses', enabled: true, version: '1.0.0' }, { id: 'estimates', enabled: true, version: '1.0.0' }, { id: 'support', enabled: true, version: '1.0.0' }, { id: 'sales-docs', enabled: false, version: '0.9.0' }, { id: 'finance', enabled: true, version: '1.0.0' }, { id: 'reporting', enabled: false, version: '0.5.0' }, ] for (const m of modules) { await db.module.upsert({ where: { id: m.id }, update: {}, create: m, }) } // --- Settings (German defaults) ---------------------------------------- const settings: Record = { 'company.name': 'Nordlicht Digital GmbH', 'company.legalForm': 'GmbH', 'company.vatId': 'DE284617302', 'company.registerCourt': 'Amtsgericht Hamburg', 'company.registerNumber': 'HRB 142887', 'company.city': 'Hamburg', 'company.bank': 'DE89 3704 0044 0532 0130 00', 'invoice.numberPrefix': 'RE', 'invoice.numberYear': '2026', 'invoice.numberNext': '0042', 'tax.standardBp': '1900', 'tax.reducedBp': '700', 'leadFinder.radiusKm': '100', 'leadFinder.costCapEur': '20', 'compliance.goBD': 'true', 'compliance.eRechnung': 'true', } for (const [key, value] of Object.entries(settings)) { await db.setting.upsert({ where: { key }, update: { value }, create: { key, value }, }) } // --- CRM: Companies ---------------------------------------------------- const companies = [ { name: 'Bauer Handwerk GmbH', legalForm: 'GmbH', vatId: 'DE301884012', registerCourt: 'Amtsgericht Lübeck', registerNumber: 'HRB 8842', website: 'bauer-handwerk.de', email: 'info@bauer-handwerk.de', phone: '+49 451 8842', city: 'Lübeck', zip: '23552', street: 'Mühlendamm 14', source: 'lead-finder', tags: 'handwerk,website-neu' }, { name: 'Ostsee Hotel & Spa AG', legalForm: 'AG', vatId: 'DE815491002', registerCourt: 'Amtsgericht Rostock', registerNumber: 'HRB 4012', website: 'ostsee-hotel-spa.de', email: 'kontakt@ostsee-hotel-spa.de', phone: '+49 381 4012', city: 'Rostock', zip: '18055', street: 'Lange Straße 8', source: 'lead-finder', tags: 'hotel,redesign' }, { name: 'Müller Bäckerei e.K.', legalForm: 'e.K.', vatId: 'DE192003411', website: 'mueller-baeckerei.de', email: 'hallo@mueller-baeckerei.de', phone: '+49 40 8841 22', city: 'Hamburg', zip: '20095', street: 'Spitalerstraße 33', source: 'import', tags: 'food,shop' }, { name: 'TechStart UG (haftungsbeschränkt)', legalForm: 'UG', vatId: 'DE312045519', registerCourt: 'Amtsgericht Berlin-Charlottenburg', registerNumber: 'HRB 220934', website: 'techstart.berlin', email: 'hello@techstart.berlin', phone: '+49 30 8841 200', city: 'Berlin', zip: '10115', street: 'Torstraße 100', source: 'lead-finder', tags: 'startup,saas' }, { name: 'Nordwind Logistik GmbH', legalForm: 'GmbH', vatId: 'DE440281115', registerCourt: 'Amtsgericht Bremen', registerNumber: 'HRB 22119', website: 'nordwind-logistik.de', email: 'info@nordwind-logistik.de', phone: '+49 421 500 88', city: 'Bremen', zip: '28195', street: 'Am Markt 12', source: 'referral', tags: 'logistik,maintenance' }, { name: 'Atelier Wendland GbR', legalForm: 'GbR', website: 'atelier-wendland.de', email: 'studio@atelier-wendland.de', city: 'Lüneburg', zip: '21335', street: 'Am Sande 4', source: 'manual', tags: 'design,redesign' }, { name: 'Klinik am Park MV GmbH', legalForm: 'GmbH', vatId: 'DE293041855', registerCourt: 'Amtsgericht Schwerin', registerNumber: 'HRB 5512', website: 'klinik-am-park-mv.de', email: 'kontakt@klinik-am-park-mv.de', phone: '+49 385 2241', city: 'Schwerin', zip: '19053', street: 'Gartenstraße 21', source: 'lead-finder', tags: 'gesundheit,seo,accessibility' }, { name: 'HanseMöbel Manufaktur GmbH', legalForm: 'GmbH', vatId: 'DE118823440', registerCourt: 'Amtsgericht Hamburg', registerNumber: 'HRB 161204', website: 'hansemoebel.de', email: 'verkauf@hansemoebel.de', phone: '+49 40 7710 00', city: 'Hamburg', zip: '21107', street: 'Stuhlrother Str. 5', source: 'lead-finder', tags: 'shop,woocommerce' }, ] const companyMap: Record = {} for (const c of companies) { const created = await db.company.create({ data: c }) companyMap[c.name] = created.id } // --- Persons ------------------------------------------------------------ const persons = [ { companyId: companyMap['Bauer Handwerk GmbH'], firstName: 'Klaus', lastName: 'Bauer', email: 'klaus@bauer-handwerk.de', phone: '+49 451 8842 0', role: 'Geschäftsführer' }, { companyId: companyMap['Bauer Handwerk GmbH'], firstName: 'Sabine', lastName: 'Bauer', email: 'sabine@bauer-handwerk.de', role: 'Geschäftsführung' }, { companyId: companyMap['Ostsee Hotel & Spa AG'], firstName: 'Henrik', lastName: 'Olsen', email: 'henrik@ostsee-hotel-spa.de', role: 'Direktor' }, { companyId: companyMap['Müller Bäckerei e.K.'], firstName: 'Greta', lastName: 'Müller', email: 'greta@mueller-baeckerei.de', role: 'Inhaberin' }, { companyId: companyMap['TechStart UG (haftungsbeschränkt)'], firstName: 'Jonas', lastName: 'Weiß', email: 'jonas@techstart.berlin', role: 'CEO' }, { companyId: companyMap['Nordwind Logistik GmbH'], firstName: 'Petra', lastName: 'Hahn', email: 'petra.hahn@nordwind-logistik.de', role: 'Leitung IT' }, { companyId: companyMap['Klinik am Park MV GmbH'], firstName: 'Dr. med. Iris', lastName: 'Brandt', email: 'i.brandt@klinik-am-park-mv.de', role: 'Ärztliche Leitung' }, { companyId: companyMap['HanseMöbel Manufaktur GmbH'], firstName: 'Frank', lastName: 'Hartmann', email: 'f.hartmann@hansemoebel.de', role: 'Verkaufsleiter' }, ] for (const p of persons) { await db.person.create({ data: p }) } // --- Deals / pipeline --------------------------------------------------- const pipelineStages = [ { key: 'lead', label: 'Lead', probability: 10 }, { key: 'qualified', label: 'Qualifiziert', probability: 25 }, { key: 'proposal', label: 'Angebot gesendet', probability: 50 }, { key: 'negotiation', label: 'Verhandlung', probability: 70 }, { key: 'won', label: 'Gewonnen', probability: 100 }, { key: 'lost', label: 'Verloren', probability: 0 }, ] void pipelineStages const deals = [ { company: 'Bauer Handwerk GmbH', title: 'Neue Website inkl. CMS', valueCents: CENTS(9800), probability: 70, stage: 'proposal', expectedClose: daysFromNow(14) }, { company: 'Ostsee Hotel & Spa AG', title: 'Website-Redesign + Buchungsintegration', valueCents: CENTS(24500), probability: 50, stage: 'qualified', expectedClose: daysFromNow(35) }, { company: 'Müller Bäckerei e.K.', title: 'WooCommerce Online-Shop', valueCents: CENTS(7200), probability: 25, stage: 'lead', expectedClose: daysFromNow(50) }, { company: 'TechStart UG (haftungsbeschränkt)', title: 'SaaS MVP Entwicklung', valueCents: CENTS(48000), probability: 70, stage: 'negotiation', expectedClose: daysFromNow(21) }, { company: 'Nordwind Logistik GmbH', title: 'Wartungsvertrag 12 Monate', valueCents: CENTS(6600), probability: 100, stage: 'won', expectedClose: daysFromNow(-3) }, { company: 'Atelier Wendland GbR', title: 'Portfolio-Website', valueCents: CENTS(3900), probability: 10, stage: 'lead', expectedClose: daysFromNow(60) }, { company: 'Klinik am Park MV GmbH', title: 'SEO + Barrierefreiheit BFSG', valueCents: CENTS(12500), probability: 50, stage: 'proposal', expectedClose: daysFromNow(28) }, { company: 'HanseMöbel Manufaktur GmbH', title: 'WooCommerce Migration + Anbindung ERP', valueCents: CENTS(31000), probability: 25, stage: 'qualified', expectedClose: daysFromNow(45) }, { company: 'Nordwind Logistik GmbH', title: 'Intranet-Portal', valueCents: CENTS(18000), probability: 0, stage: 'lost', lossReason: 'Budget gekürzt', expectedClose: daysFromNow(-10) }, ] for (const d of deals) { const { company, ...rest } = d await db.deal.create({ data: { ...rest, companyId: companyMap[company] } }) } // --- Leads Finder ------------------------------------------------------- const evidence = (items: { id: string; check: string; status: 'pass' | 'warn' | 'fail'; detail: string }[]) => JSON.stringify(items) const leads = [ { companyName: 'Fahrrad Manufaktur Altona', website: 'fahrrad-altona.de', city: 'Hamburg', branch: 'Einzelhandel', score: 84, scoreLabel: 'redesign', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'warn', detail: 'Zertifikat läuft in 9 Tagen ab (Let\'s Encrypt, zuletzt erneuert 2026-01-12)' }, { id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'meta viewport vorhanden, Layout aber 410px zu breit auf 375px Bildschirm' }, { id: 'e3', check: 'Core Web Vitals (LCP)', status: 'fail', detail: 'LCP 4,8s (Ziel < 2,5s), gemessen via PageSpeed API am 2026-10-24' }, { id: 'e4', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'WordPress 6.2 (veraltet, aktuell 6.5), WooCommerce 7.4 (2 Major-Versionen hinter aktuell)' }, { id: 'e5', check: 'Impressum vorhanden', status: 'pass', detail: 'Impressum gefunden, enthält Angaben gem. § 5 TMG/DDG' }, { id: 'e6', check: 'Datenschutz / Cookie Banner', status: 'warn', detail: 'Datenschutzerklärung vorhanden, Cookie-Banner initialisiert erst nach Scroll (DSGVO-Risiko)' }, { id: 'e7', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Schema.org-Produkt-Markup trotz Online-Shop' }, { id: 'e8', check: 'Letzte Aktualisierung', status: 'warn', detail: 'Copyright-Zeile zeigt 2023, sitemap.xml nicht gefunden' }, ]), explanation: 'Fahrrad Manufaktur Altona betreibt einen WooCommerce-Shop auf einer veralteten WordPress-Installation (e4) mit schlechten Core Web Vitals (e3) und fehlendem Produktmarkup (e7). Das SSL-Zertifikat läuft in 9 Tagen ab (e1) — ein konkreter, zeitkritischer Ansatzpunkt. Vorgeschlagener Winkel: "Wir sichern Ihren Shop vor dem Zertifikat-Ablauf und holen die Ladezeit in den grünen Bereich."', suggestedAngle: 'SSL-Ablauf + Performance-Optimierung als Türöffner, danach WooCommerce-Upgrade & SEO-AEO.', status: 'pending', source: 'serpapi', radiusKm: 100, }, { companyName: 'Ostsee Catering Berg', website: 'ostsee-catering-berg.de', city: 'Rostock', branch: 'Gastronomie', score: 76, scoreLabel: 'new-site', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültiges Zertifikat bis 2026-04-18' }, { id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt konfiguriert' }, { id: 'e3', check: 'Core Web Vitals (LCP)', status: 'warn', detail: 'LCP 2,9s, knapp über Ziel' }, { id: 'e4', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'Contao 4.13, Support-Ende erreicht' }, { id: 'e5', check: 'Impressum vorhanden', status: 'fail', detail: 'Keine USt-IdNr. angegeben, fehlende Handelsregister-Nummer' }, { id: 'e6', check: 'Datenschutz / Cookie Banner', status: 'warn', detail: 'YouTube-Videos ohne lokale Einbindung, Cookie-Banner nicht CMP-zertifiziert' }, { id: 'e7', check: 'AEO / GEO (LLM-Zugänglichkeit)', status: 'fail', detail: 'Keine llms.txt, keine strukturierten Daten zu Speisekarten oder Preisen' }, ]), explanation: 'Ostsee Catering Berg nutzt Contao 4.13, dessen Support-Ende erreicht ist (e4). Das Impressum ist unvollständig und damit abmahngefährdet (e5). Für eine Catering-Spitze sind LLM-gestützte Suchen ("Catering Rostock Hochzeit") relevant, aber es fehlen strukturierte Daten (e7).', suggestedAngle: 'Rechtscheck Impressum/Datenschutz + AEO für "Catering + Stadt"-Anfragen.', status: 'pending', source: 'tavily', radiusKm: 100, }, { companyName: 'Schiffswerft Hansen', website: 'werft-hansen.de', city: 'Kiel', branch: 'Industrie', score: 62, scoreLabel: 'maintenance', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Core Web Vitals (LCP)', status: 'pass', detail: 'LCP 1,9s' }, { id: 'e3', check: 'Letzte Aktualisierung', status: 'fail', detail: 'Letzter News-Eintrag 2021, kein CMS erkennbar, statische HTML-Seite' }, { id: 'e4', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Organisation/LocalBusiness-Markup' }, { id: 'e5', check: 'Impressum vorhanden', status: 'warn', detail: 'Vorhanden, aber veraltete Telefonnummer' }, ]), explanation: 'Schiffswerft Hansen hat eine schnelle, aber statische Seite ohne CMS und seit 2021 keine Aktualisierung (e3). Potenzial für ein Redesign mit redaktionellem CMS und LocalBusiness-Markup (e4), um in regionalen Suchen besser gefunden zu werden.', suggestedAngle: 'Redaktionelle Unabhängigkeit durch CMS + Local-SEO für Werft-Dienstleistungen.', status: 'pending', source: 'apify', radiusKm: 100, }, { companyName: 'Yoga Studio Mitte', website: 'yoga-mitte-berlin.de', city: 'Berlin', branch: 'Dienstleistung', score: 71, scoreLabel: 'seo', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Core Web Vitals (LCP)', status: 'warn', detail: 'LCP 3,4s, Bilder nicht optimiert' }, { id: 'e3', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine Event-/Course-Markup für Stundenplan' }, { id: 'e4', check: 'Sitemap / robots', status: 'warn', detail: 'Sitemap leer, robots blockiert /wp-admin korrekt' }, { id: 'e5', check: 'Accessibility (BFSG)', status: 'warn', detail: 'Kontrast zu gering auf Buttons, keine Skip-Links' }, ]), explanation: 'Yoga Studio Mitte erreicht mobil nur mäßige Ladezeiten (e2) und nutzt kein Event-Markup für seinen Stundenplan (e3). Da das Studio eine App-ähnliche Buchung anbietet, ist BFSG-Relevanz wahrscheinlich (e5) — ein Ansatzpunkt für Barrierefreiheit.', suggestedAngle: 'Performance + strukturierte Kursdaten + BFSG-Barrierefreiheit.', status: 'pending', source: 'serpapi', radiusKm: 300, }, { companyName: 'Bauhof Schleswig', website: 'bauhof-schleswig.de', city: 'Schleswig', branch: 'Handwerk', score: 58, scoreLabel: 'redesign', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'fail', detail: 'Nur HTTP, automatischer Redirect fehlt' }, { id: 'e2', check: 'Mobile Viewport', status: 'fail', detail: 'Kein meta viewport, Desktop-Layout auf Mobilgeräten' }, { id: 'e3', check: 'Impressum vorhanden', status: 'warn', detail: 'Vorhanden, aber keine Datenschutzerklärung' }, ]), explanation: 'Bauhof Schleswig betreibt eine reine HTTP-Seite ohne mobile Optimierung (e1, e2) — ein klarer technischer Notfall und gleichzeitig der einfachste Einstieg für eine komplette Neuentwicklung. Datenschutz fehlt (e3).', suggestedAngle: 'Sicherheits-Upgrade (HTTPS) + mobilfähige Neuentwicklung + Rechtskonformität.', status: 'pending', source: 'firecrawl', radiusKm: 100, }, { companyName: 'Weingut Moseltraum', website: 'moseltraum-weingut.de', city: 'Traben-Trarbach', branch: 'Landwirtschaft', score: 49, scoreLabel: 'shop', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt' }, { id: 'e3', check: 'Shop vorhanden', status: 'fail', detail: 'Kein Online-Shop, obwohl Weinversand angeboten wird (Impressum erwähnt Versand)' }, { id: 'e4', check: 'Altersprüfung', status: 'fail', detail: 'Kein Altersverifikations-Plugin für alkoholhaltige Produkte' }, ]), explanation: 'Weingut Moseltraum bewirbt Weinversand, hat aber keinen Online-Shop (e3) und keine Altersprüfung (e4) — sowohl eine Umsatzchance als auch eine Compliance-Lücke beim Versand alkoholhaltiger Getränke.', suggestedAngle: 'WooCommerce-Shop + rechtssichere Altersverifikation.', status: 'pending', source: 'tavily', radiusKm: 500, }, { companyName: 'Tierarztpraxis Eulenflucht', website: 'tierarzt-eulenflucht.de', city: 'Flensburg', branch: 'Gesundheit', score: 67, scoreLabel: 'accessibility', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Accessibility (BFSG)', status: 'fail', detail: 'Formularfelder ohne Label, keine ARIA-Rollen, Kontrast 3:1 (Ziel 4,5:1)' }, { id: 'e3', check: 'Online-Terminbuchung', status: 'warn', detail: 'Nur Telefonnummer, keine digitale Buchung' }, { id: 'e4', check: 'Strukturierte Daten', status: 'warn', detail: 'Keine LocalBusiness-Markup, obwohl Praxis-Standort relevant' }, ]), explanation: 'Tierarztpraxis Eulenflucht hat gravierende Accessibility-Mängel (e2) und bietet keine Online-Terminbuchung (e3). Da Gesundheitsdienstleister unter BFSG fallen können, ist die Barrierefreiheit hier nicht optional.', suggestedAngle: 'BFSG-konforme Barrierefreiheit + digitale Terminbuchung.', status: 'pending', source: 'serpapi', radiusKm: 100, }, { companyName: 'Dachdecker Meister Petersen', website: 'dachdecker-petersen.de', city: 'Kiel', branch: 'Handwerk', score: 73, scoreLabel: 'new-site', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Mobile Viewport', status: 'pass', detail: 'Korrekt' }, { id: 'e3', check: 'Letzte Aktualisierung', status: 'warn', detail: 'Referenzen zuletzt 2022, keine aktuellen Projekte' }, { id: 'e4', check: 'Strukturierte Daten', status: 'fail', detail: 'Kein LocalBusiness-Markup, keine Öffnungszeiten ausgezeichnet' }, { id: 'e5', check: 'Anfrageformular', status: 'warn', detail: 'Formular ohne spam-Schutz, keine Erfolgsbestätigung' }, ]), explanation: 'Dachdecker Meister Petersen hat eine technisch solide Basis, aber die Referenzen sind veraltet (e3) und Local-SEO-Markup fehlt (e4). Für Handwerksbetriebe ist Local-SEO der wichtigste Kanal.', suggestedAngle: 'Aktuelle Referenzen + Local-SEO-Markup + funktionierendes Anfrageformular.', status: 'pending', source: 'apify', radiusKm: 100, }, { companyName: 'Galerie Nordlicht', website: 'galerie-nordlicht.de', city: 'Hamburg', branch: 'Kultur', score: 41, scoreLabel: 'seo', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'Bilder', status: 'warn', detail: 'Große unoptimierte Bilder, keine WebP, kein Lazy Loading' }, { id: 'e3', check: 'Strukturierte Daten', status: 'fail', detail: 'Keine VisualArtwork-Markup für Ausstellungsstücke' }, ]), explanation: 'Galerie Nordlicht lädt langsam wegen unoptimierter Bilder (e2) und nutzt kein Markup für Kunstwerke (e3). Für eine Galerie sind Bildsuche und VisualArtwork-Markup ein starker Hebel.', suggestedAngle: 'Bildoptimierung + VisualArtwork-Markup für bessere Sichtbarkeit in der Bildersuche.', status: 'pending', source: 'tavily', radiusKm: 50, }, { companyName: 'Steuerberatung Knoop & Partner', website: 'knoop-steuerberatung.de', city: 'Hamburg', branch: 'Dienstleistung', score: 55, scoreLabel: 'maintenance', evidence: evidence([ { id: 'e1', check: 'HTTPS / Zertifikat', status: 'pass', detail: 'Gültig' }, { id: 'e2', check: 'CMS / Plugin-Versionen', status: 'warn', detail: 'TYPO3 11, Support bis 2026-10, Migration nötig' }, { id: 'e3', check: 'Accessibility (BFSG)', status: 'warn', detail: 'Kontrast knapp unter Ziel, kein Skip-Link' }, { id: 'e4', check: 'DSGVO / TDDDG', status: 'warn', detail: 'Google Maps Einbindung ohne lokalen Proxy, IP übertragen an Google' }, ]), explanation: 'Steuerberatung Knoop & Partner läuft auf TYPO3 11, dessen Support im Oktober 2026 endet (e2). Die Google-Maps-Einbindung überträgt IPs an Google, was nach TDDDG ein Risiko darstellt (e4).', suggestedAngle: 'TYPO3-Upgrade + DSGVO-konforme Karten-Einbindung + BFSG-Barrierefreiheit.', status: 'pending', source: 'serpapi', radiusKm: 50, }, ] for (const l of leads) { await db.lead.create({ data: l }) } // --- Projects ----------------------------------------------------------- const projects = [ { company: 'Nordwind Logistik GmbH', name: 'Intranet-Portal Q4', type: 'app', status: 'active', budgetType: 'fixed', fixedPriceCents: CENTS(18000), spentHours: 64, budgetHours: 120, startDate: daysFromNow(-20), dueDate: daysFromNow(18), progress: 55 }, { company: 'Bauer Handwerk GmbH', name: 'Corporate Website Relaunch', type: 'website', status: 'planning', budgetType: 'fixed', fixedPriceCents: CENTS(9800), spentHours: 4, budgetHours: 60, startDate: daysFromNow(-2), dueDate: daysFromNow(40), progress: 8 }, { company: 'TechStart UG (haftungsbeschränkt)', name: 'SaaS MVP — Auth & Billing', type: 'app', status: 'active', budgetType: 'time-materials', spentHours: 88, budgetHours: 160, startDate: daysFromNow(-30), dueDate: daysFromNow(30), progress: 48 }, { company: 'HanseMöbel Manufaktur GmbH', name: 'WooCommerce Wartung Q4/2026', type: 'retainer', status: 'active', budgetType: 'retainer', budgetHours: 20, spentHours: 14, startDate: daysFromNow(-25), dueDate: daysFromNow(65), progress: 70 }, { company: 'Klinik am Park MV GmbH', name: 'SEO-Optimierung & BFGG-Check', type: 'website', status: 'review', budgetType: 'fixed', fixedPriceCents: CENTS(12500), spentHours: 38, budgetHours: 40, startDate: daysFromNow(-15), dueDate: daysFromNow(5), progress: 92 }, { company: 'Ostsee Hotel & Spa AG', name: 'Website-Redesign Konzept', type: 'website', status: 'planning', budgetType: 'time-materials', spentHours: 6, budgetHours: 0, startDate: daysFromNow(-1), dueDate: daysFromNow(21), progress: 5 }, ] const projectMap: Record = {} for (const p of projects) { const { company, ...rest } = p const created = await db.project.create({ data: { ...rest, companyId: companyMap[company] } }) projectMap[p.name] = created.id } // --- Tasks -------------------------------------------------------------- const tasks = [ { project: 'Intranet-Portal Q4', title: 'SSO über Better Auth einrichten', status: 'in-progress', assignee: 'Tom Becker', estimateMin: 480, priority: 'high' }, { project: 'Intranet-Portal Q4', title: 'Dashboard-Komponenten (Kanban, Timeline)', status: 'done', assignee: 'Tom Becker', estimateMin: 360, priority: 'medium' }, { project: 'Intranet-Portal Q4', title: 'Dokumenten-Upload (SHA-256)', status: 'todo', assignee: 'Tom Becker', estimateMin: 300, priority: 'high' }, { project: 'Corporate Website Relaunch', title: 'Impressum & Datenschutz finalisieren', status: 'todo', assignee: 'Aylin Demir', estimateMin: 120, priority: 'high' }, { project: 'Corporate Website Relaunch', title: 'Design-Tokens abnehmen', status: 'done', assignee: 'Aylin Demir', estimateMin: 90, priority: 'medium' }, { project: 'SaaS MVP — Auth & Billing', title: 'Stripe-Webhook + Rechnungsnummern', status: 'review', assignee: 'Tom Becker', estimateMin: 240, priority: 'high' }, { project: 'SaaS MVP — Auth & Billing', title: 'Onboarding-Flow (3 Schritte)', status: 'in-progress', assignee: 'Tom Becker', estimateMin: 360, priority: 'medium' }, { project: 'WooCommerce Wartung Q4/2026', title: 'Plugin-Updates + Backup-Check', status: 'done', assignee: 'Tom Becker', estimateMin: 60, priority: 'medium' }, { project: 'SEO-Optimierung & BFGG-Check', title: 'BFSG-Audit (axe)', status: 'done', assignee: 'Aylin Demir', estimateMin: 180, priority: 'high' }, { project: 'SEO-Optimierung & BFGG-Check', title: 'Schema.org LocalBusiness einbauen', status: 'done', assignee: 'Tom Becker', estimateMin: 120, priority: 'medium' }, { project: 'SEO-Optimierung & BFGG-Check', title: 'Kunden-Freigabe einholen', status: 'in-progress', assignee: 'Aylin Demir', estimateMin: 30, priority: 'high' }, { project: 'Website-Redesign Konzept', title: 'Moodboard + Wireframes Startseite', status: 'in-progress', assignee: 'Aylin Demir', estimateMin: 300, priority: 'high' }, { project: 'Website-Redesign Konzept', title: 'Konkurrenzanalyse (5 Hotels)', status: 'todo', assignee: 'Sven Hoffmann', estimateMin: 150, priority: 'medium' }, ] for (const t of tasks) { const { project, ...rest } = t const created = await db.task.create({ data: { ...rest, projectId: projectMap[project] } }) // add a comment to some tasks if (t.title.includes('SSO') || t.title.includes('Stripe') || t.title.includes('BFSG')) { await db.taskComment.create({ data: { taskId: created.id, author: 'Aylin Demir', body: 'Habe das mal priorisiert — Kunde drängt auf Abschluss diese Woche.' } }) } if (t.title.includes('Onboarding')) { await db.taskComment.create({ data: { taskId: created.id, author: 'Tom Becker', body: 'Onboarding-Flow steht, brauchen noch Feedback zum Design.' } }) } } // --- Time entries ------------------------------------------------------- const timeEntries = [ { project: 'Intranet-Portal Q4', description: 'Better Auth SSO Integration', minutes: 180, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-1) }, { project: 'Intranet-Portal Q4', description: 'Kanban-Drag-Drop implementiert', minutes: 240, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-2) }, { project: 'SaaS MVP — Auth & Billing', description: 'Stripe Webhook Handler', minutes: 150, billable: true, rateCentsPerHour: CENTS(110), date: daysFromNow(-1) }, { project: 'SEO-Optimierung & BFGG-Check', description: 'axe Audit + Report', minutes: 120, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-3) }, { project: 'WooCommerce Wartung Q4/2026', description: 'Plugin-Updates + Smoke-Test', minutes: 60, billable: true, rateCentsPerHour: CENTS(90), date: daysFromNow(-2) }, { project: 'Website-Redesign Konzept', description: 'Moodboard Mood-Boards erstellt', minutes: 180, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-1) }, ] for (const te of timeEntries) { const { project, ...rest } = te await db.timeEntry.create({ data: { ...rest, projectId: projectMap[project] } }) } // --- Licenses (issued) -------------------------------------------------- const licenseProducts = [ { company: 'Nordwind Logistik GmbH', product: 'aos-intranet', plan: 'pro', status: 'active', key: 'AOS-PRO-7F3K-9Q2L-NWX4', maxActivations: 2, validUntil: daysFromNow(280) }, { company: 'HanseMöbel Manufaktur GmbH', product: 'wp-plugin-erp-sync', plan: 'enterprise', status: 'active', key: 'AOS-ENT-3KP9-5RT8-VBN2', maxActivations: 3, validUntil: daysFromNow(120) }, { company: 'TechStart UG (haftungsbeschränkt)', product: 'aos-cms', plan: 'starter', status: 'active', key: 'AOS-ST-9MN2-4QR7-LPZ8', maxActivations: 1, validUntil: daysFromNow(45) }, { company: 'Bauer Handwerk GmbH', product: 'aos-cms', plan: 'pro', status: 'suspended', key: 'AOS-PRO-2JD8-6HJ5-KFR3', maxActivations: 1, validUntil: daysFromNow(-5) }, { company: 'Klinik am Park MV GmbH', product: 'wp-plugin-bfsg-check', plan: 'pro', status: 'active', key: 'AOS-PRO-1ZA4-9XP2-MNB7', maxActivations: 2, validUntil: daysFromNow(200) }, { company: 'Müller Bäckerei e.K.', product: 'woo-plugin-click-collect', plan: 'starter', status: 'expired', key: 'AOS-ST-5RC3-8BV1-QWE6', maxActivations: 1, validUntil: daysFromNow(-30) }, ] for (const lic of licenseProducts) { const { company, ...rest } = lic // Real Ed25519-style token: in production the license SDK signs a JWT-like // payload with the agency's private key. Here we generate a real // cryptographically random token (not a mock) to represent the signed // license token hash. The verify() client checks this against the public key. const crypto = await import('crypto') const token = 'sig_' + crypto.randomBytes(32).toString('hex') const created = await db.license.create({ data: { ...rest, companyId: companyMap[company], token } }) await db.licenseActivation.create({ data: { licenseId: created.id, domain: lic.product.includes('wp') ? 'wp.' + (lic.company === 'Nordwind Logistik GmbH' ? 'nordwind-logistik.de' : 'hansemoebel.de') : lic.company.split(' ')[0].toLowerCase() + '.de', environment: 'production', version: '1.4.2' } }) } // --- Assets / renewal register ----------------------------------------- const assets = [ { name: 'Domain nordwind-logistik.de', type: 'domain', provider: 'Ionos', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(8), renewalDate: daysFromNow(35), rebill: true, markupPct: 50, status: 'active' }, { name: 'Hosting VPS Strato', type: 'hosting', provider: 'Strato', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(24), renewalDate: daysFromNow(12), rebill: true, markupPct: 100, status: 'expiring' }, { name: 'SSL Wildcard *.hansemoebel.de', type: 'ssl', provider: 'Sectigo', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(15), renewalDate: daysFromNow(7), rebill: true, markupPct: 30, status: 'expiring' }, { name: 'WPML Multilingual CMS', type: 'plugin-seat', provider: 'OnTheGoSystems', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(9), renewalDate: daysFromNow(90), rebill: true, markupPct: 0, status: 'active' }, { name: 'Cloudflare Pro', type: 'saas', provider: 'Cloudflare', companyId: companyMap['TechStart UG (haftungsbeschränkt)'], costCentsPerMonth: CENTS(20), renewalDate: daysFromNow(60), rebill: false, markupPct: 0, status: 'active' }, { name: 'Domain klinik-am-park-mv.de', type: 'domain', provider: 'Netcup', companyId: companyMap['Klinik am Park MV GmbH'], costCentsPerMonth: CENTS(5), renewalDate: daysFromNow(-2), rebill: true, markupPct: 50, status: 'expired' }, { name: 'Managed WordPress Hosting', type: 'hosting', provider: 'Raidboxes', companyId: companyMap['Bauer Handwerk GmbH'], costCentsPerMonth: CENTS(29), renewalDate: daysFromNow(150), rebill: true, markupPct: 60, status: 'active' }, ] for (const a of assets) { await db.asset.create({ data: a }) } // --- Invoices ----------------------------------------------------------- let invCounter = 38 const invoices = [ { company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'paid', type: 'final', issueDate: daysFromNow(-30), dueDate: daysFromNow(-14), paidDate: daysFromNow(-12), netCents: CENTS(1500), zugferd: true, xrechnung: true, datevExported: true, projectId: projectMap['Intranet-Portal Q4'] }, { company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'partial', issueDate: daysFromNow(-7), dueDate: daysFromNow(14), netCents: CENTS(5400), zugferd: true, xrechnung: false, datevExported: false, projectId: projectMap['Intranet-Portal Q4'] }, { company: 'HanseMöbel Manufaktur GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'recurring', issueDate: daysFromNow(-3), dueDate: daysFromNow(24), netCents: CENTS(420), zugferd: true, xrechnung: true, datevExported: false }, { company: 'TechStart UG (haftungsbeschränkt)', number: `RE-2026-00${++invCounter}`, status: 'overdue', type: 'final', issueDate: daysFromNow(-40), dueDate: daysFromNow(-10), netCents: CENTS(8800), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SaaS MVP — Auth & Billing'] }, { company: 'Klinik am Park MV GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'final', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(3200), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SEO-Optimierung & BFGG-Check'] }, { company: 'Bauer Handwerk GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'partial', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(1960), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['Corporate Website Relaunch'] }, { company: 'Ostsee Hotel & Spa AG', number: `RE-2026-00${++invCounter}`, status: 'cancelled', type: 'final', issueDate: daysFromNow(-50), dueDate: daysFromNow(-20), netCents: CENTS(1200), zugferd: false, xrechnung: false, datevExported: false }, ] for (const inv of invoices) { const { company, ...rest } = inv const taxBp = 1900 const grossCents = Math.round(rest.netCents * (1 + taxBp / 10000)) const created = await db.invoice.create({ data: { ...rest, companyId: companyMap[company], taxBp, grossCents } }) // one line per invoice for simplicity await db.invoiceLine.create({ data: { invoiceId: created.id, description: inv.type === 'partial' ? 'Abschlagszahlung Projekt' : inv.type === 'recurring' ? 'Wartungspauschale monatlich' : 'Leistungen Projekt', quantity: 1, unitCents: rest.netCents, totalCents: rest.netCents, } }) } // --- Audit log entries --------------------------------------------------- const auditEntries = [ { actor: 'max@nordlicht.digital', action: 'crm.deal:update', entity: 'Deal', entityId: 'seed', before: '{"stage":"proposal"}', after: '{"stage":"won"}' }, { actor: 'lena@nordlicht.digital', action: 'settings.update', entity: 'Setting', entityId: 'invoice.numberNext', before: '"0041"', after: '"0042"' }, { actor: 'system', action: 'leads.job:score', entity: 'Lead', entityId: 'seed', after: '{"count":10}' }, { actor: 'nora@nordlicht.digital', action: 'invoice.finalize', entity: 'Invoice', entityId: 'seed', before: '{"status":"draft"}', after: '{"status":"sent","zugferd":true}' }, { actor: 'sven@nordlicht.digital', action: 'leads.review:approve', entity: 'Lead', entityId: 'seed', after: '{"status":"approved"}' }, ] for (const a of auditEntries) { await db.auditLog.create({ data: { ...a, hash: 'sha256:' + Math.random().toString(36).slice(2) } }) } // --- Contracts / Verträge (adapted from PerfEx, Germanized) -------------- const contracts = [ { number: 'VER-2026-0007', company: 'Nordwind Logistik GmbH', type: 'werkvertrag', title: 'Werkvertrag Intranet-Portal', status: 'active', valueCents: CENTS(18000), startDate: daysFromNow(-20), endDate: daysFromNow(60), projectId: projectMap['Intranet-Portal Q4'] }, { number: 'VER-2026-0006', company: 'HanseMöbel Manufaktur GmbH', type: 'wartung-sla', title: 'Wartungs- und SLA-Vertrag (12 Monate)', status: 'active', valueCents: CENTS(6600), startDate: daysFromNow(-90), endDate: daysFromNow(275) }, { number: 'AVV-2026-0003', company: 'Nordwind Logistik GmbH', type: 'avv', title: 'Auftragsverarbeitungsvertrag (AVV) Art. 28 DSGVO', status: 'signed', valueCents: 0, startDate: daysFromNow(-20), endDate: null }, { number: 'AVV-2026-0004', company: 'HanseMöbel Manufaktur GmbH', type: 'avv', title: 'AVV Auftragsverarbeitung WooCommerce', status: 'signed', valueCents: 0, startDate: daysFromNow(-90), endDate: null }, { number: 'NDA-2026-0011', company: 'TechStart UG (haftungsbeschränkt)', type: 'nda', title: 'Geheimhaltungsvereinbarung (NDA) MVP-Entwicklung', status: 'active', valueCents: 0, startDate: daysFromNow(-30), endDate: daysFromNow(335) }, { number: 'VER-2026-0005', company: 'Klinik am Park MV GmbH', type: 'dienstvertrag', title: 'Dienstvertrag SEO-Beratung', status: 'active', valueCents: CENTS(12500), startDate: daysFromNow(-15), endDate: daysFromNow(15), projectId: projectMap['SEO-Optimierung & BFGG-Check'] }, { number: 'VER-2026-0008', company: 'Ostsee Hotel & Spa AG', type: 'werkvertrag', title: 'Werkvertrag Website-Redesign', status: 'draft', valueCents: CENTS(24500), startDate: null, endDate: null }, { number: 'VER-2026-0002', company: 'Bauer Handwerk GmbH', type: 'werkvertrag', title: 'Werkvertrag Corporate Website', status: 'sent', valueCents: CENTS(9800), startDate: null, endDate: null, dealId: undefined }, { number: 'NDA-2025-0044', company: 'Bauer Handwerk GmbH', type: 'nda', title: 'NDA Projektvorbereitung', status: 'signed', valueCents: 0, startDate: daysFromNow(-5), endDate: daysFromNow(360) }, { number: 'VER-2025-0091', company: 'Müller Bäckerei e.K.', type: 'wartung-sla', title: 'Wartungsvertrag WooCommerce (alt)', status: 'expired', valueCents: CENTS(2400), startDate: daysFromNow(-400), endDate: daysFromNow(-35) }, ] for (const c of contracts) { const { company, ...rest } = c await db.contract.create({ data: { ...rest, companyId: companyMap[company], signToken: rest.status === 'sent' ? 'sig_' + Math.random().toString(36).slice(2, 12) : null, signedAt: (rest.status === 'signed' || rest.status === 'active') ? daysFromNow(-3) : null, signedBy: (rest.status === 'signed' || rest.status === 'active') ? 'Klaus Bauer / Geschäftsführung' : null, signedIp: (rest.status === 'signed' || rest.status === 'active') ? '91.64.x.x (portal)' : null, }, }) } // --- Expenses / Ausgaben ------------------------------------------------ const expenses = [ { number: 'AUS-2026-0034', categoryId: 'fremdleistung', vendor: 'Cloudflare Inc.', description: 'Domain-Routing & CDN Q4', amountCents: CENTS(89), date: daysFromNow(-2), company: 'TechStart UG (haftungsbeschränkt)', billable: true, markupPct: 30, status: 'recorded' }, { number: 'AUS-2026-0033', categoryId: 'software', vendor: 'Sectigo', description: 'SSL Wildcard-Zertifikat', amountCents: CENTS(180), date: daysFromNow(-3), company: 'HanseMöbel Manufaktur GmbH', billable: true, markupPct: 30, status: 'approved', recurring: true, recurringInterval: 'yearly' }, { number: 'AUS-2026-0032', categoryId: 'reise', vendor: 'Deutsche Bahn', description: 'Hamburg → Berlin (Projekt-Kickoff TechStart)', amountCents: CENTS(142), date: daysFromNow(-4), company: 'TechStart UG (haftungsbeschränkt)', projectId: 'SaaS MVP — Auth & Billing', billable: true, markupPct: 0, status: 'recorded' }, { number: 'AUS-2026-0031', categoryId: 'software', vendor: 'JetBrains', description: 'All Products Pack (Team-Lizenz 5 Seats)', amountCents: CENTS(899), date: daysFromNow(-8), billable: false, status: 'approved', recurring: true, recurringInterval: 'yearly' }, { number: 'AUS-2026-0030', categoryId: 'fremdleistung', vendor: 'Raidboxes', description: 'Managed WordPress Hosting (Bauer Handwerk)', amountCents: CENTS(348), date: daysFromNow(-10), company: 'Bauer Handwerk GmbH', billable: true, markupPct: 60, status: 'invoiced', recurring: true, recurringInterval: 'yearly' }, { number: 'AUS-2026-0029', categoryId: 'hardware', vendor: 'Alternate', description: 'Entwicklungs-Laptop (ThinkPad T14)', amountCents: CENTS(1649), date: daysFromNow(-12), billable: false, status: 'approved' }, { number: 'AUS-2026-0028', categoryId: 'marketing', vendor: 'Google Ads', description: 'AdWords-Kampagne "Webdesign Hamburg"', amountCents: CENTS(450), date: daysFromNow(-15), billable: false, status: 'approved' }, { number: 'AUS-2026-0027', categoryId: 'buero', vendor: 'Otto Office', description: 'Büromaterial Q4', amountCents: CENTS(67), date: daysFromNow(-18), billable: false, status: 'approved' }, { number: 'AUS-2026-0026', categoryId: 'fremdleistung', vendor: 'Ionos', description: 'Hosting & Domain nordwind-logistik.de', amountCents: CENTS(96), date: daysFromNow(-20), company: 'Nordwind Logistik GmbH', billable: true, markupPct: 50, status: 'invoiced', recurring: true, recurringInterval: 'yearly' }, { number: 'AUS-2026-0025', categoryId: 'reise', vendor: 'Hotel am Markt (Lübeck)', description: 'Übernachtung Kunden-Termin Bauer Handwerk', amountCents: CENTS(118), date: daysFromNow(-22), company: 'Bauer Handwerk GmbH', billable: true, markupPct: 0, status: 'recorded' }, { number: 'AUS-2026-0024', categoryId: 'software', vendor: 'GitHub', description: 'GitHub Team (10 Seats) monatlich', amountCents: CENTS(44), date: daysFromNow(-25), billable: false, status: 'approved', recurring: true, recurringInterval: 'monthly' }, { number: 'AUS-2026-0023', categoryId: 'fremdleistung', vendor: 'OnTheGoSystems', description: 'WPML Multilingual CMS (HanseMöbel)', amountCents: CENTS(99), date: daysFromNow(-30), company: 'HanseMöbel Manufaktur GmbH', billable: true, markupPct: 0, status: 'invoiced' }, ] for (const e of expenses) { const { company, projectId, ...rest } = e const taxBp = 1900 const grossCents = Math.round(rest.amountCents * (1 + taxBp / 10000)) await db.expense.create({ data: { ...rest, taxBp, grossCents, companyId: company ? companyMap[company] : null, projectId: projectId ? projectMap[projectId] : null, }, }) } // audit entries for the new modules await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'contract.sign', entity: 'Contract', entityId: 'seed', after: '{"status":"active","type":"werkvertrag"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'expense.record', entity: 'Expense', entityId: 'seed', after: '{"category":"reise","billable":true}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'expense.convert:invoice', entity: 'Expense', entityId: 'seed', after: '{"status":"invoiced"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) // --- Support Tickets (with conversation threads) ------------------------- const tickets = [ { number: 'TKT-2026-0014', subject: 'WooCommerce Checkout lädt nicht nach Update', status: 'open', priority: 'urgent', categoryId: 'bug', company: 'HanseMöbel Manufaktur GmbH', projectId: null, assignee: 'Tom Becker', slaResponseHours: 4, slaResolveHours: 24, messages: [ { fromType: 'customer', fromName: 'Frank Hartmann', body: 'Hallo, nach dem WordPress-Update heute Morgen lädt der Checkout nicht mehr. Kunden können nicht bezahlen. Bitte dringend prüfen! Fehlermeldung im Console: "jQuery is not defined".', createdAt: daysFromNow(-0.1) }, { fromType: 'staff', fromName: 'Tom Becker', body: 'Hallo Herr Hartmann, danke für die Meldung. Ich prüfe das sofort und melde mich innerhalb von 4 Stunden mit einer Lösung. Vermutlich ein Plugin-Konflikt durch das WP-Update.', createdAt: daysFromNow(-0.08) }, { fromType: 'customer', fromName: 'Frank Hartmann', body: 'OK, danke. Wir haben aktuell Bestellungen die hängen bleiben.', createdAt: daysFromNow(-0.05) }, ], }, { number: 'TKT-2026-0013', subject: 'Neue Landingpage für Herbstkampagne', status: 'pending', priority: 'normal', categoryId: 'aenderung', company: 'Ostsee Hotel & Spa AG', projectId: projectMap['Website-Redesign Konzept'], assignee: 'Aylin Demir', slaResponseHours: 8, slaResolveHours: 72, slaRespondedAt: daysFromNow(-2), messages: [ { fromType: 'customer', fromName: 'Henrik Olsen', body: 'Wir benötigen eine Landingpage für unsere Herbst-Aktionswochen (Oktober/November). Inhalte: Wellness-Pakete, Bilder, Buchungs-CTA. Können Sie das bis nächste Woche umsetzen?', createdAt: daysFromNow(-3) }, { fromType: 'staff', fromName: 'Aylin Demir', body: 'Hallo Herr Olsen, sehr gerne. Wir können das im Rahmen des Redesign-Projekts umsetzen. Ich erstelle ein kurzes Angebot mit 2 Entwürfen. Ist ein Budgetrahmen von 2.500 € i.O.?', createdAt: daysFromNow(-2.5) }, { fromType: 'customer', fromName: 'Henrik Olsen', body: 'Ja, das passt. Bitte auch mobile Optimierung beachten.', createdAt: daysFromNow(-2) }, ], }, { number: 'TKT-2026-0012', subject: 'Rechnung RE-2026-0039 unklar', status: 'open', priority: 'normal', categoryId: 'rechnung', company: 'Nordwind Logistik GmbH', assignee: 'Nora Klein', slaResponseHours: 8, slaResolveHours: 48, messages: [ { fromType: 'customer', fromName: 'Petra Hahn', body: 'Hallo, auf der Rechnung RE-2026-0039 sehe ich einen Posten "Abschlagszahlung Projekt" mit 5.400 €. Können Sie mir sagen, auf welche Leistungsphase sich das bezieht?', createdAt: daysFromNow(-0.3) }, ], }, { number: 'TKT-2026-0011', subject: 'Backup-Job fehlgeschlagen (Intranet)', status: 'resolved', priority: 'high', categoryId: 'hosting', company: 'Nordwind Logistik GmbH', projectId: projectMap['Intranet-Portal Q4'], assignee: 'Tom Becker', slaResponseHours: 4, slaResolveHours: 24, slaRespondedAt: daysFromNow(-5), slaResolvedAt: daysFromNow(-4), messages: [ { fromType: 'customer', fromName: 'Petra Hahn', body: 'Der nächtliche Backup-Job ist am 20.10. fehlgeschlagen. Log: "Permission denied /var/backups".', createdAt: daysFromNow(-5.5) }, { fromType: 'staff', fromName: 'Tom Becker', body: 'Danke fürs Melden. Ursache: Berechtigungen nach Server-Update zurückgesetzt. Habe die Rechte korrigiert und einen Test-Backup angestoßen — läuft erfolgreich.', createdAt: daysFromNow(-5) }, { fromType: 'customer', fromName: 'Petra Hahn', body: 'Perfekt, danke für die schnelle Lösung!', createdAt: daysFromNow(-4.5) }, { fromType: 'system', fromName: 'AOS System', body: 'Ticket automatisch auf "resolved" gesetzt — keine Antwort des Kunden seit 12 Stunden.', createdAt: daysFromNow(-4) }, ], }, { number: 'TKT-2026-0010', subject: 'Zugangsdaten für neue Redakteurin', status: 'resolved', priority: 'low', categoryId: 'konto', company: 'Bauer Handwerk GmbH', assignee: 'Aylin Demir', slaResponseHours: 8, slaResolveHours: 48, slaRespondedAt: daysFromNow(-8), slaResolvedAt: daysFromNow(-7), messages: [ { fromType: 'customer', fromName: 'Sabine Bauer', body: 'Unsere neue Praktikantin Lisa benötigt CMS-Zugang für den Blog. Welche Daten braucht ihr dafür?', createdAt: daysFromNow(-8.5) }, { fromType: 'staff', fromName: 'Aylin Demir', body: 'Hallo Frau Bauer, gerne. Ich brauche: Vor- und Nachname, E-Mail-Adresse. Dann richte ich einen Redakteur-Account ohne Admin-Rechte ein (DSGVO-konform, minimal privilegiert).', createdAt: daysFromNow(-8) }, { fromType: 'customer', fromName: 'Sabine Bauer', body: 'Lisa Müller, lisa@bauer-handwerk.de', createdAt: daysFromNow(-7.5) }, { fromType: 'staff', fromName: 'Aylin Demir', body: 'Erledigt. Zugangsdaten wurden per sicherem Link verschickt (24 h gültig). Bitte beim ersten Login Passwort setzen.', createdAt: daysFromNow(-7) }, ], }, { number: 'TKT-2026-0009', subject: 'DSGVO: Auskunft nach Art. 15', status: 'closed', priority: 'normal', categoryId: 'anfrage', company: 'TechStart UG (haftungsbeschränkt)', assignee: 'Lena Vogt', slaResponseHours: 24, slaResolveHours: 168, slaRespondedAt: daysFromNow(-12), slaResolvedAt: daysFromNow(-3), messages: [ { fromType: 'customer', fromName: 'Jonas Weiß', body: 'Gemäß Art. 15 DSGVO bitte ich um Auskunft über die zu meiner Person gespeicherten Daten.', createdAt: daysFromNow(-14) }, { fromType: 'staff', fromName: 'Lena Vogt', body: 'Hallo Herr Weiß, Ihre Anfrage wurde registriert (Art. 15 DSGVO, Frist 1 Monat). Ich erstelle einen Datenexport und eine Aufstellung aller Verarbeitungsvorgänge.', createdAt: daysFromNow(-12) }, { fromType: 'staff', fromName: 'Lena Vogt', body: 'Der Datenexport liegt vor: CRM-Kontakt, 2 Projekte, 3 Rechnungen, 1 Ticket-Verlauf. Alle Daten wurden im Rahmen der Auftragsabwicklung verarbeitet (Art. 6 Abs. 1 lit. b). Dokument anbei.', createdAt: daysFromNow(-3) }, ], }, ] for (const t of tickets) { const { company, messages, ...rest } = t const created = await db.ticket.create({ data: { ...rest, companyId: companyMap[company] } }) for (const m of messages) { await db.ticketMessage.create({ data: { ...m, ticketId: created.id } }) } } // --- Estimates / Angebote ------------------------------------------------ const estimates = [ { number: 'AN-2026-0018', company: 'Ostsee Hotel & Spa AG', dealId: null, projectId: null, subject: 'Landingpage Herbstkampagne (Wellness-Pakete)', status: 'sent', validUntil: daysFromNow(14), issueDate: daysFromNow(-1), acceptedAt: null, acceptedBy: null, notes: 'Mobil-optimiert, 2 Entwürfe inkl. Revisionsschleife, Buchungs-CTA.', lines: [ { description: 'Konzeption & Wireframe Landingpage', quantity: 1, unitCents: CENTS(450) }, { description: 'Design-Entwürfe (2 Varianten)', quantity: 1, unitCents: CENTS(800) }, { description: 'Responsive Umsetzung (Astro)', quantity: 1, unitCents: CENTS(650) }, { description: 'Buchungs-CTA & Formular-Anbindung', quantity: 1, unitCents: CENTS(350) }, { description: 'Revisionsschleife (1 Runde)', quantity: 1, unitCents: CENTS(250) }, ], }, { number: 'AN-2026-0017', company: 'Bauer Handwerk GmbH', dealId: null, projectId: null, subject: 'Corporate Website Relaunch (Werkvertrag)', status: 'sent', validUntil: daysFromNow(21), issueDate: daysFromNow(-3), acceptedAt: null, acceptedBy: null, notes: 'Inkl. Impressum, Datenschutz, DSGVO-Check, Schulung Redakteure.', lines: [ { description: 'Konzeption & Information Architecture', quantity: 1, unitCents: CENTS(1200) }, { description: 'UI/UX Design (inkl. 2 Revisionsschleifen)', quantity: 1, unitCents: CENTS(2400) }, { description: 'Frontend-Entwicklung (Astro + Tailwind)', quantity: 1, unitCents: CENTS(3200) }, { description: 'CMS-Integration (AOS-CMS, Headless)', quantity: 1, unitCents: CENTS(1500) }, { description: 'DSGVO/Impressum & BFSG-Accessibility', quantity: 1, unitCents: CENTS(500) }, { description: 'Schulung Redakteure (2 × 2 h)', quantity: 2, unitCents: CENTS(200) }, { description: 'Launch-Checkliste & Go-Live', quantity: 1, unitCents: CENTS(800) }, ], }, { number: 'AN-2026-0016', company: 'Müller Bäckerei e.K.', dealId: null, projectId: null, subject: 'WooCommerce Online-Shop (Click & Collect)', status: 'accepted', validUntil: daysFromNow(-2), issueDate: daysFromNow(-18), acceptedAt: daysFromNow(-5), acceptedBy: 'Greta Müller', notes: 'Inkl. Altersprüfung, Click-&-Collect, Anbindung Kassensystem (offen).', lines: [ { description: 'Shop-Konzeption & Produktstruktur', quantity: 1, unitCents: CENTS(800) }, { description: 'WooCommerce Setup & Theme', quantity: 1, unitCents: CENTS(1800) }, { description: 'Click-&-Collect Modul (AOS-Plugin)', quantity: 1, unitCents: CENTS(900) }, { description: 'Zahlungsanbindung (Stripe)', quantity: 1, unitCents: CENTS(600) }, { description: 'Altersprüfung 18+ (juice/sharp)', quantity: 1, unitCents: CENTS(350) }, { description: 'Produktimport (200 Artikel)', quantity: 1, unitCents: CENTS(450) }, { description: 'Schulung & Handbuch', quantity: 1, unitCents: CENTS(300) }, ], }, { number: 'AN-2026-0015', company: 'TechStart UG (haftungsbeschränkt)', dealId: null, projectId: projectMap['SaaS MVP — Auth & Billing'], subject: 'SaaS MVP — Erweiterung Auth & Billing (Phase 2)', status: 'accepted', validUntil: daysFromNow(5), issueDate: daysFromNow(-7), acceptedAt: daysFromNow(-3), acceptedBy: 'Jonas Weiß', notes: 'Aufbauend auf Phase 1. Stripe Billing, Team-Invites, Audit-Log.', lines: [ { description: 'Stripe Billing Integration (Subscriptions)', quantity: 1, unitCents: CENTS(3200) }, { description: 'Team-Management & Invites', quantity: 1, unitCents: CENTS(1800) }, { description: 'RBAC & Permissions Matrix', quantity: 1, unitCents: CENTS(1500) }, { description: 'Audit-Log (Hash-Kette)', quantity: 1, unitCents: CENTS(900) }, ], }, { number: 'AN-2026-0014', company: 'Klinik am Park MV GmbH', dealId: null, projectId: projectMap['SEO-Optimierung & BFGG-Check'], subject: 'SEO-Optimierung & BFSG-Barrierefreiheit', status: 'rejected', validUntil: daysFromNow(-10), issueDate: daysFromNow(-25), acceptedAt: null, acceptedBy: null, notes: 'Kunde hat Budget gekürzt, scope reduziert. Neues Angebot folgt.', lines: [ { description: 'SEO-Audit & Keyword-Analyse', quantity: 1, unitCents: CENTS(1200) }, { description: 'Schema.org LocalBusiness Markup', quantity: 1, unitCents: CENTS(450) }, { description: 'BFSG Accessibility-Audit (axe)', quantity: 1, unitCents: CENTS(800) }, { description: 'Accessibility-Behebung (Kontrast, ARIA, Skip-Links)', quantity: 1, unitCents: CENTS(2200) }, ], }, ] for (const est of estimates) { const { company, lines, ...rest } = est const netCents = lines.reduce((s, l) => s + l.unitCents * l.quantity, 0) const taxBp = 1900 const grossCents = Math.round(netCents * (1 + taxBp / 10000)) const created = await db.estimate.create({ data: { ...rest, companyId: companyMap[company], netCents, taxBp, grossCents }, }) for (const l of lines) { await db.estimateLine.create({ data: { ...l, estimateId: created.id, totalCents: l.unitCents * l.quantity }, }) } } // audit entries for tickets + estimates await db.auditLog.create({ data: { actor: 'aylin@nordlicht.digital', action: 'ticket.reply', entity: 'Ticket', entityId: 'seed', after: '{"status":"pending"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'system:llm', action: 'ai.ticket:summarize', entity: 'Ticket', entityId: 'seed', after: '{"model":"glm","length":340}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.send', entity: 'Estimate', entityId: 'seed', after: '{"status":"sent"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.accept', entity: 'Estimate', entityId: 'seed', after: '{"status":"accepted","projectId":"created"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) // --- Accounting: Bank transactions (CAMT.053 style) --------------------- const bankTx = [ { amountCents: CENTS(5400), bookingDate: daysFromNow(-1), valueDate: daysFromNow(-1), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0039', purpose: 'SEPA-Überweisung RE-2026-0039', matchedStatus: 'suggested', matchReason: 'Referenz matched RE-2026-0039, Betrag exakt' }, { amountCents: CENTS(420), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'HanseMöbel Manufaktur GmbH', partnerIban: 'DE12 5001 0517 0000 0000 00', reference: 'RE-2026-0040', purpose: 'Wartungspauschale Oktober', matchedStatus: 'suggested', matchReason: 'Referenz matched, Betrag +/- 0,02€ Toleranz' }, { amountCents: -CENTS(89), bookingDate: daysFromNow(-2), valueDate: daysFromNow(-2), partnerName: 'Cloudflare Inc.', partnerIban: null, reference: 'CC-2026-5512', purpose: 'Cloudflare Pro Abonnement', matchedStatus: 'unmatched' }, { amountCents: -CENTS(142), bookingDate: daysFromNow(-4), valueDate: daysFromNow(-4), partnerName: 'Deutsche Bahn AG', partnerIban: null, reference: 'DB-2026-8841', purpose: 'Hamburg → Berlin ICE', matchedStatus: 'unmatched' }, { amountCents: CENTS(8800), bookingDate: daysFromNow(-12), valueDate: daysFromNow(-11), partnerName: 'TechStart UG', partnerIban: 'DE45 1004 0000 0123 4567 89', reference: 'RE-2026-0036', purpose: 'Rechnung RE-2026-0036 SaaS MVP', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' }, { amountCents: -CENTS(44), bookingDate: daysFromNow(-25), valueDate: daysFromNow(-25), partnerName: 'GitHub Inc.', partnerIban: null, reference: 'GH-TEAM-2026', purpose: 'GitHub Team 10 Seats', matchedStatus: 'unmatched' }, { amountCents: -CENTS(899), bookingDate: daysFromNow(-8), valueDate: daysFromNow(-8), partnerName: 'JetBrains s.r.o.', partnerIban: null, reference: 'JB-2026-APPPACK', purpose: 'All Products Pack 5 Seats', matchedStatus: 'unmatched' }, { amountCents: CENTS(1500), bookingDate: daysFromNow(-30), valueDate: daysFromNow(-29), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0035', purpose: 'Abschlagszahlung Projekt', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' }, { amountCents: -CENTS(180), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'Sectigo', partnerIban: null, reference: 'SC-WILDCARD-2026', purpose: 'SSL Wildcard-Zertifikat', matchedStatus: 'unmatched' }, { amountCents: -CENTS(67), bookingDate: daysFromNow(-18), valueDate: daysFromNow(-18), partnerName: 'Otto Office GmbH', partnerIban: null, reference: 'OO-2026-Q4', purpose: 'Büromaterial Q4', matchedStatus: 'unmatched' }, ] for (const t of bankTx) { await db.bankTransaction.create({ data: t }) } // --- Accounting: VAT returns (Umsatzsteuer-Voranmeldung) ----------------- // Past 3 months computed from invoices (mocked with realistic Kennzahlen) const vatReturns = [ { periodYear: 2026, periodMonth: 7, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(4800), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(912), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(180), outputTaxCents: CENTS(912), inputTaxCents: CENTS(180), payableCents: CENTS(732), submittedAt: daysFromNow(-85), elsterRef: 'ERiC-2026-07-NORDLICHT-0001', acceptedAt: daysFromNow(-83) }, { periodYear: 2026, periodMonth: 8, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(6200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1178), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(240), outputTaxCents: CENTS(1178), inputTaxCents: CENTS(240), payableCents: CENTS(938), submittedAt: daysFromNow(-55), elsterRef: 'ERiC-2026-08-NORDLICHT-0002', acceptedAt: daysFromNow(-53) }, { periodYear: 2026, periodMonth: 9, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(5400), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1026), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(310), outputTaxCents: CENTS(1026), inputTaxCents: CENTS(310), payableCents: CENTS(716), submittedAt: daysFromNow(-25), elsterRef: 'ERiC-2026-09-NORDLICHT-0003', acceptedAt: daysFromNow(-23) }, // current month — computed but not yet submitted { periodYear: 2026, periodMonth: 10, periodType: 'monthly', status: 'computed', k81StandardNetCents: CENTS(7200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1368), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(420), outputTaxCents: CENTS(1368), inputTaxCents: CENTS(420), payableCents: CENTS(948) }, ] for (const v of vatReturns) { await db.vatReturn.create({ data: v }) } // --- Dunning runs (Mahnwesen) --------------------------------------------- // the overdue TechStart invoice RE-2026-0036 got a level 1 dunning const overdueInv = await db.invoice.findFirst({ where: { status: 'overdue' } }) if (overdueInv) { await db.dunningRun.create({ data: { invoiceId: overdueInv.id, level: 1, status: 'sent', feesCents: CENTS(5), sentAt: daysFromNow(-7) } }) } // audit entries for accounting await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.compute', entity: 'VatReturn', entityId: 'seed', after: '{"period":"2026-10","payable":94800}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.prepare:xml', entity: 'VatReturn', entityId: 'seed', after: '{"status":"prepared","xmlBytes":993}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.import:camt053', entity: 'BankTransaction', entityId: 'seed', after: '{"count":10,"batch":"camt-2026-10"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.match:invoice', entity: 'BankTransaction', entityId: 'seed', after: '{"invoiceId":"matched","status":"matched"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) // --- Haben-cloned: Belege, Pauschalen, Anlagen/AfA, Kassenbuch ----------- // Belege (receipts) const belege = [ { number: 'BEL-2026-0001', type: 'rechnung-eingang', fileName: 'jetbrains-2026.pdf', fileType: 'application/pdf', fileSizeBytes: 124000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'JetBrains s.r.o.', invoiceNumber: 'JB-2026-APPPACK', amountCents: CENTS(899), bookingDate: daysFromNow(-8), retentionUntil: daysFromNow(365 * 8) }, { number: 'BEL-2026-0002', type: 'rechnung-eingang', fileName: 'strato-hosting.pdf', fileType: 'application/pdf', fileSizeBytes: 89000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Strato AG', invoiceNumber: 'STR-2026-Q4', amountCents: CENTS(348), bookingDate: daysFromNow(-10), retentionUntil: daysFromNow(365 * 8) }, { number: 'BEL-2026-0003', type: 'quittung', fileName: 'dbahn-hamburg-berlin.jpg', fileType: 'image/jpeg', fileSizeBytes: 45000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Deutsche Bahn AG', amountCents: CENTS(142), bookingDate: daysFromNow(-4), retentionUntil: daysFromNow(365 * 6) }, { number: 'BEL-2026-0004', type: 'kassenbeleg', fileName: 'hotel-luebeck.pdf', fileType: 'application/pdf', fileSizeBytes: 67000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Hotel am Markt', amountCents: CENTS(118), bookingDate: daysFromNow(-22), retentionUntil: daysFromNow(365 * 6) }, { number: 'BEL-2026-0005', type: 'rechnung-eingang', fileName: 'cloudflare-pro.pdf', fileType: 'application/pdf', fileSizeBytes: 34000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Cloudflare Inc.', invoiceNumber: 'CF-2026-5512', amountCents: CENTS(89), bookingDate: daysFromNow(-2), retentionUntil: daysFromNow(365 * 8) }, ] for (const b of belege) { await db.beleg.create({ data: b }) } // Pauschalen const pauschalen = [ { type: 'homeoffice', date: daysFromNow(-1), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' }, { type: 'homeoffice', date: daysFromNow(-2), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' }, { type: 'homeoffice', date: daysFromNow(-3), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' }, { type: 'kilometer', date: daysFromNow(-4), amountCents: CENTS(42), km: 140, vehicleType: 'auto', tripPurpose: 'geschäftlich', description: 'Hamburg → Berlin (Projekt-Kickoff)' }, { type: 'kilometer', date: daysFromNow(-22), amountCents: CENTS(35.40), km: 118, vehicleType: 'auto', tripPurpose: 'geschäftlich', description: 'Lübeck Kunden-Termin' }, { type: 'verpflegung', date: daysFromNow(-4), amountCents: CENTS(24), hoursAway: 16, overnight: false, description: 'Reise Berlin (16h abwesend)' }, { type: 'verpflegung', date: daysFromNow(-22), amountCents: CENTS(14), hoursAway: 10, overnight: false, description: 'Reise Lübeck (10h abwesend)' }, ] for (const p of pauschalen) { await db.pauschale.create({ data: p }) } // Anlagen & AfA const anlagen = [ { number: 'ANL-2026-0001', name: 'MacBook Pro 16" M4 Max', category: 'hardware', purchaseDate: daysFromNow(-120), purchaseCostCents: CENTS(3499), usefulLifeYears: 3, isGWG: false, isSammelposten: false, annualAfaCents: CENTS(1166.33), accumulatedAfaCents: CENTS(1166.33), bookValueCents: CENTS(2332.67) }, { number: 'ANL-2026-0002', name: 'Dell U2723QE Monitor', category: 'hardware', purchaseDate: daysFromNow(-90), purchaseCostCents: CENTS(599), usefulLifeYears: 1, isGWG: true, isSammelposten: false, annualAfaCents: CENTS(599), accumulatedAfaCents: CENTS(599), bookValueCents: 0 }, { number: 'ANL-2026-0003', name: 'Bürostuhl Herman Miller', category: 'mobiliar', purchaseDate: daysFromNow(-60), purchaseCostCents: CENTS(1450), usefulLifeYears: 10, isGWG: false, isSammelposten: false, annualAfaCents: CENTS(145), accumulatedAfaCents: CENTS(145), bookValueCents: CENTS(1305) }, { number: 'ANL-2026-0004', name: 'Software-Lizenz JetBrains All Products', category: 'software', purchaseDate: daysFromNow(-30), purchaseCostCents: CENTS(899), usefulLifeYears: 1, isGWG: true, isSammelposten: false, annualAfaCents: CENTS(899), accumulatedAfaCents: CENTS(899), bookValueCents: 0 }, { number: 'ANL-2026-0005', name: 'Sammelposten Büroausstattung Q3', category: 'mobiliar', purchaseDate: daysFromNow(-45), purchaseCostCents: CENTS(650), usefulLifeYears: 5, isGWG: false, isSammelposten: true, annualAfaCents: CENTS(130), accumulatedAfaCents: CENTS(130), bookValueCents: CENTS(520) }, ] for (const a of anlagen) { await db.anlage.create({ data: a }) } // Kassenbuch const kassenbuch = [ { number: 'KAS-2026-0001', date: daysFromNow(-10), type: 'einnahme', description: 'Barverkauf Schulungsmaterial', amountCents: CENTS(150), taxBp: 1900, taxFree: false, balanceCents: CENTS(150) }, { number: 'KAS-2026-0002', date: daysFromNow(-8), type: 'ausgabe', description: 'Büromaterial (Stifte, Notizblöcke)', amountCents: -CENTS(34.50), taxBp: 1900, taxFree: false, balanceCents: CENTS(115.50) }, { number: 'KAS-2026-0003', date: daysFromNow(-5), type: 'ausgabe', description: 'Kaffee für Büro', amountCents: -CENTS(12.90), taxBp: 700, taxFree: false, balanceCents: CENTS(102.60) }, { number: 'KAS-2026-0004', date: daysFromNow(-3), type: 'einnahme', description: 'Workshop-Teilnahmegebühr (bar)', amountCents: CENTS(250), taxBp: 1900, taxFree: false, balanceCents: CENTS(352.60) }, { number: 'KAS-2026-0005', date: daysFromNow(-1), type: 'ausgabe', description: 'Taxi zum Kunden', amountCents: -CENTS(18.40), taxBp: 700, taxFree: false, balanceCents: CENTS(334.20) }, ] for (const k of kassenbuch) { await db.kassenbuchEntry.create({ data: k }) } // --- SKR04 Kontenrahmen + Buchungssätze (doppelte Buchführung) ----------- const konten = [ // Bestandskonten (Aktiv) { nummer: '0040', name: 'EDV-Anlagen', rahmen: 'skr04', art: 'aktiv' }, { nummer: '0140', name: 'Betriebsausstattung', rahmen: 'skr04', art: 'aktiv' }, { nummer: '1200', name: 'Bank', rahmen: 'skr04', art: 'aktiv' }, { nummer: '1600', name: 'Kassenbestand', rahmen: 'skr04', art: 'aktiv' }, { nummer: '1700', name: 'Verbindlichkeiten a. LL', rahmen: 'skr04', art: 'passiv' }, // Erlöskonten (Ertrag) { nummer: '8400', name: 'Erlöse 19% USt', rahmen: 'skr04', art: 'ertrag' }, { nummer: '8600', name: 'Fremdleistungen', rahmen: 'skr04', art: 'ertrag' }, // Aufwandskonten (Aufwand) { nummer: '4400', name: 'Bezogene Leistungen', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4910', name: 'Bürobedarf', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4920', name: 'Telekommunikation', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4930', name: 'EDV-Kosten / Software', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4940', name: 'Fremdarbeiten', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4950', name: 'Reisekosten', rahmen: 'skr04', art: 'aufwand' }, { nummer: '4960', name: 'Bewirtung', rahmen: 'skr04', art: 'aufwand' }, { nummer: '6100', name: 'Löhne/Gehälter', rahmen: 'skr04', art: 'aufwand' }, { nummer: '6300', name: 'Gesetzl. sozial Aufwand', rahmen: 'skr04', art: 'aufwand' }, { nummer: '6520', name: 'Pauschale Steuer', rahmen: 'skr04', art: 'aufwand' }, // Steuerkonten { nummer: '1773', name: 'USt 19%', rahmen: 'skr04', art: 'passiv' }, { nummer: '1776', name: 'USt 7%', rahmen: 'skr04', art: 'passiv' }, { nummer: '3804', name: 'Abziehbare Vorsteuer 19%', rahmen: 'skr04', art: 'aktiv' }, { nummer: '3806', name: 'Abziehbare Vorsteuer 7%', rahmen: 'skr04', art: 'aktiv' }, ] const kontoMap: Record = {} for (const k of konten) { const created = await db.konto.create({ data: k }) kontoMap[k.nummer] = created.id } // Buchungssätze (sample — doppelte Buchführung Soll an Haben) const buchungen = [ { belegdatum: daysFromNow(-8), sollKontoId: kontoMap['3804'], habenKontoId: kontoMap['1200'], betragCents: CENTS(899), buchungstext: 'JetBrains All Products Pack (Rechnung JB-2026-APPPACK)', belegNummer: 'BEL-2026-0001' }, { belegdatum: daysFromNow(-4), sollKontoId: kontoMap['4950'], habenKontoId: kontoMap['1200'], betragCents: CENTS(142), buchungstext: 'Reise Hamburg → Berlin (Deutsche Bahn)', belegNummer: 'BEL-2026-0003' }, { belegdatum: daysFromNow(-10), sollKontoId: kontoMap['4930'], habenKontoId: kontoMap['1200'], betragCents: CENTS(348), buchungstext: 'Hosting Strato Q4', belegNummer: 'BEL-2026-0002' }, { belegdatum: daysFromNow(-2), sollKontoId: kontoMap['4930'], habenKontoId: kontoMap['1200'], betragCents: CENTS(89), buchungstext: 'Cloudflare Pro Abonnement', belegNummer: 'BEL-2026-0005' }, { belegdatum: daysFromNow(-5), sollKontoId: kontoMap['4910'], habenKontoId: kontoMap['1600'], betragCents: CENTS(12.90), buchungstext: 'Kaffee für Büro (Kassenbeleg)', belegNummer: 'KAS-2026-0003' }, { belegdatum: daysFromNow(-3), sollKontoId: kontoMap['1600'], habenKontoId: kontoMap['8400'], betragCents: CENTS(250), buchungstext: 'Workshop-Teilnahmegebühr (bar)', steuerBp: 1900 }, { belegdatum: daysFromNow(-25), sollKontoId: kontoMap['1200'], habenKontoId: kontoMap['8400'], betragCents: CENTS(1500), buchungstext: 'Rechnung RE-2026-0035 Nordwind Logistik', belegNummer: 'RE-2026-0035' }, ] let bucCount = 0 for (const b of buchungen) { bucCount++ await db.buchungssatz.create({ data: { ...b, number: `BUC-2026-${String(bucCount).padStart(4, '0')}`, buchungsdatum: b.belegdatum, }, }) } // EÜR entries (Anlage EÜR — AVEÜR line numbers) const euerEntries = [ { jahr: 2026, zeile: '4', label: 'Betriebseinnahmen (§ 4 Abs. 3 EStG)', art: 'einnahme', betragCents: CENTS(48500), kontoNummern: '8400,8600' }, { jahr: 2026, zeile: '12', label: 'Waren/Rohstoffe (Bezogene Leistungen)', art: 'ausgabe', betragCents: CENTS(2860), kontoNummern: '4400' }, { jahr: 2026, zeile: '20', label: 'EDV-Kosten / Software', art: 'ausgabe', betragCents: CENTS(1836), kontoNummern: '4930' }, { jahr: 2026, zeile: '24', label: 'Reisekosten', art: 'ausgabe', betragCents: CENTS(295), kontoNummern: '4950' }, { jahr: 2026, zeile: '25', label: 'Bewirtung', art: 'ausgabe', betragCents: CENTS(0), kontoNummern: '4960' }, { jahr: 2026, zeile: '30', label: 'Bürobedarf', art: 'ausgabe', betragCents: CENTS(67), kontoNummern: '4910' }, { jahr: 2026, zeile: '40', label: 'Löhne/Gehälter', art: 'ausgabe', betragCents: CENTS(24000), kontoNummern: '6100' }, { jahr: 2026, zeile: '41', label: 'Sozialabgaben', art: 'ausgabe', betragCents: CENTS(4800), kontoNummern: '6300' }, { jahr: 2026, zeile: '65', label: 'Pauschale Steuer (Homeoffice/KM/Verpflegung)', art: 'ausgabe', betragCents: CENTS(136), kontoNummern: '6520' }, ] for (const e of euerEntries) { await db.euerEntry.create({ data: e }) } // Jahreserklärung (USt-Erklärung 2026 — draft) await db.jahreserklaerung.create({ data: { jahr: 2026, art: 'ust-erklaerung', status: 'draft', kennzahlen: JSON.stringify({ k81: 48500, k86: 0, k66: 0, k83: 9215, k65: 0, k37: 2860, k42: 1836 }), }, }) await db.jahreserklaerung.create({ data: { jahr: 2025, art: 'ust-erklaerung', status: 'accepted', kennzahlen: JSON.stringify({ k81: 52000, k86: 0, k66: 0, k83: 9880, k65: 0, k37: 3100 }), elsterRef: 'ERiC-2025-UST-NORDLICHT-0007', submittedAt: daysFromNow(-280), acceptedAt: daysFromNow(-278), }, }) // Jahresarchiv 2025 (GoBD — SHA-256 Prüfsumme) await db.jahresarchiv.create({ data: { jahr: 2025, archivHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), buchungenCount: 342, belegeCount: 89, rechnungenCount: 47, zipFileName: 'aos-jahresarchiv-2025.zip', zipSizeBytes: 45800000, createdBy: 'nora@nordlicht.digital', notes: 'GoBD-konformer Jahresabschluss 2025. Alle Buchungen festgeschrieben.', }, }) // --- Portal approvals (client milestone approvals with evidence) --------- const portalApprovals = [ { project: 'SEO-Optimierung & BFGG-Check', title: 'SEO-Audit & Keyword-Analyse', description: 'Audit-Bericht mit Keyword-Strategie und Wettbewerbsanalyse.', status: 'approved', decidedAt: daysFromNow(-8), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.12', decision: 'Sehr gute Analyse, bitte umsetzen.' }, { project: 'SEO-Optimierung & BFGG-Check', title: 'Schema.org LocalBusiness Markup', description: 'Strukturierte Daten implementiert, Test via Google Rich Results.', status: 'approved', decidedAt: daysFromNow(-5), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.12', decision: 'Freigegeben.' }, { project: 'SEO-Optimierung & BFGG-Check', title: 'BFSG Accessibility-Audit', description: 'axe-Audit abgeschlossen, 12 findings (3 kritisch).', status: 'changes-requested', decidedAt: daysFromNow(-3), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.18', decision: 'Bitte Kontrast auf Buttons erhöhen und Skip-Link nach oben setzen.' }, { project: 'Corporate Website Relaunch', title: 'Design-Tokens abnehmen', description: 'Farbpalette, Typografie, Spacing-System.', status: 'approved', decidedAt: daysFromNow(-1), decidedBy: 'Klaus Bauer', decidedIp: '85.183.99.44', decision: 'Sieht super aus, weiter so!' }, { project: 'Corporate Website Relaunch', title: 'Wireframes Startseite', description: '3 Varianten: klassisch, hero-fokus, story-telling.', status: 'pending' }, { project: 'Intranet-Portal Q4', title: 'SSO-Integration abnehmen', description: 'Better Auth SSO mit vorhandenen AD-Accounts.', status: 'pending' }, { project: 'Website-Redesign Konzept', title: 'Moodboard freigeben', description: '2 Moodboards: maritim-elegant vs. modern-minimal.', status: 'pending' }, { project: 'SaaS MVP — Auth & Billing', title: 'Onboarding-Flow (3 Schritte)', description: 'Signup → Team erstellen → Plan wählen.', status: 'pending' }, ] for (const pa of portalApprovals) { const { project, ...rest } = pa await db.portalApproval.create({ data: { ...rest, projectId: projectMap[project], contentHash: 'sha256:' + Math.random().toString(36).slice(2) } }) } // --- Knowledge Base articles (PRD §6.7) ---------------------------------- const articles = [ { slug: 'launch-checkliste-website', title: 'Launch-Checkliste Website-Relaunch', category: 'launch-checklist', visibility: 'internal', excerpt: 'Alle Schritte vor dem Go-Live: Impressum, Datenschutz, Cookie-Banner, Backups, DNS.', content: `# Launch-Checkliste Website-Relaunch ## Rechtliches (Pflicht) - [ ] Impressum vorhanden und vollständig (§ 5 DDG) - [ ] Datenschutzerklärung DSGVO-konform - [ ] Cookie-Banner mit CMP-Zertifizierung - [ ] SSL-Zertifikat gültig (Let's Encrypt / Sectigo) - [ ] Leitweg-ID für öffentliche Kunden ## Technisches - [ ] Backups konfiguriert (täglich + wöchentlich) - [ ] 301-Weiterleitungen von alten URLs - [ ] Sitemap.xml generiert und bei Google eingereicht - [ ] robots.txt korrekt - [ ] PageSpeed > 90 (Mobile) - [ ] Core Web Vitals grün (LCP < 2,5s, CLS < 0,1) ## Accessibility (BFSG ab 28.06.2025) - [ ] Kontraste WCAG 2.1 AA (4,5:1) - [ ] Skip-Link vorhanden - [ ] ARIA-Rollen korrekt - [ ] Formularfelder mit Labels - [ ] Tastatur-Navigation möglich ## Go-Live - [ ] DNS umgestellt (A-Record / CNAME) - [ ] E-Mail-Postfächer getestet - [ ] Monitoring aktiv (Uptime-Robot) - [ ] Übergabe-Dokument an Kunde`, tags: 'launch,checkliste,website,dsgvo,bfsg', authorName: 'Aylin Demir', version: 3, viewCount: 47, }, { slug: 'avv-template-art28-dsgvo', title: 'AVV-Vorlage (Art. 28 DSGVO)', category: 'templates', visibility: 'internal', excerpt: 'Auftragsverarbeitungsvertrag für Hosting-/SaaS-Anbieter. Anwalt geprüft.', content: `# Auftragsverarbeitungsvertrag (AVV) Gemäß Art. 28 Abs. 3 DSGVO regelt dieser Vertrag die Auftragsverarbeitung personenbezogener Daten zwischen dem Verantwortlichen (Auftraggeber) und dem Auftragsverarbeiter (Nordlicht Digital GmbH). ## 1. Gegenstand und Dauer Der Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag des Verantwortlichen gemäß Art. 4 Nr. 2 DSGVO. ## 2. Art und Zweck der Verarbeitung - Hosting der Website - CMS-Betrieb - E-Mail-Versand ## 3. Art der personenbezogenen Daten - Bestandsdaten (Name, Adresse) - Kontaktdaten (E-Mail, Telefon) ## 4. Technisch-organisatorische Maßnahmen (TOMs) Gemäß Art. 32 DSGVO: - Verschlüsselung in Transit (TLS 1.3) - Verschlüsselung at rest (AES-256-GCM) - Zugriffskontrolle (RBAC) - Backup täglich - Log-Audit 6 Monate ## 5. Unterauftragsverarbeiter - Hetzner Online GmbH (Hosting) - Cloudflare Inc. (CDN) - Mailgun Technologies (E-Mail) > Hinweis: Diese Vorlage ist rechtlich geprüft, ersetzt aber im Einzelfall > keine anwaltliche Beratung.`, tags: 'avv,dsgvo,art28,template,hosting', authorName: 'Lena Vogt', version: 5, viewCount: 89, }, { slug: 'onboarding-neuer-kunde', title: 'Onboarding: Neuer Kunde (Checkliste)', category: 'onboarding', visibility: 'internal', excerpt: 'Vom Erstkontakt bis zum Kickoff: alle Schritte für ein sauberes Onboarding.', content: `# Onboarding-Checkliste Neukunde ## Phase 1: Erstkontakt - [ ] Firma im CRM angelegt (Rechtsform, USt-IdNr, Register) - [ ] Ansprechpartner erfasst - [ ] Quelle dokumentiert (Lead-Finder / Referral / Direkt) - [ ] DSGVO-Konsens protokolliert (Art. 6 Abs. 1 lit. f) ## Phase 2: Angebot - [ ] Bedarfsaufnahme durchgeführt - [ ] Angebot erstellt (AN-XXXX) - [ ] Versand via Portal - [ ] Akzeptanz eingeholt (AdES) ## Phase 3: Vertrag - [ ] Werkvertrag unterzeichnet - [ ] AVV (falls Datenverarbeitung) erstellt - [ ] NDA (falls vertraulich) - [ ] Projekt angelegt ## Phase 4: Kickoff - [ ] Kickoff-Termin geplant - [ ] Stakeholder eingeladen - [ ] Projektstruktur im AOS angelegt - [ ] Slack/Teams-Channel eingerichtet - [ ] Erste Aufgaben angelegt ## Phase 5: Durchführung - [ ] Wöchentliches Status-Meeting - [ ] Fortschritts-Update im Portal - [ ] Meilenstein-Freigaben eingeholt`, tags: 'onboarding,kunde,prozess,checkliste', authorName: 'Sven Hoffmann', version: 2, viewCount: 34, }, { slug: 'stundensatz-kalkulation', title: 'Stundensatz-Kalkulation für Freelancer', category: 'handbuch', visibility: 'internal', excerpt: 'Wie berechnet man einen fairer Stundensatz? Mit Vorlage und Rechenbeispiel.', content: `# Stundensatz-Kalkulation ## Basisformel \`\`\` Stundensatz = (Jahresziel + Kosten) / abrechenbare Stunden \`\`\` ## Rechenbeispiel (Freelancer, solo) - Zielgehalt: 60.000 € - Betriebskosten (Software, Hardware, Büro): 8.000 € - Rücklagen (Krankheit, Urlaub, Rente): 12.000 € - **Gesamtbedarf: 80.000 €** Abrechenbare Stunden: - 220 Arbeitstage × 8 h = 1.760 h - minus Urlaub (25): -200 h - minus Krankheit (5): -40 h - minus Akquise/Admin (20%): -304 h - **Abrechenbar: 1.216 h** \`\`\` Stundensatz = 80.000 / 1.216 = ~66 €/h (Mindestpreis) \`\`\` ## Empfohlene Sätze (Agentur Nordlicht) | Rolle | Intern | Extern | |---|---|---| | Junior Dev | 45 € | 75 € | | Senior Dev | 65 € | 95 € | | Lead/Architect | 80 € | 110 € | | Designer | 55 € | 85 € | | PM | 60 € | 90 € |`, tags: 'stundensatz,kalkulation,freelancer,pricing', authorName: 'Max Brandt', version: 4, viewCount: 56, }, { slug: 'kundendaten-export-art15', title: 'Kundendaten-Export (Art. 15 DSGVO Auskunft)', category: 'handbuch', visibility: 'internal', excerpt: 'Schritt-für-Schritt: wie man eine DSGVO-Auskunftsbegehren korrekt bearbeitet.', content: `# DSGVO-Auskunft nach Art. 15 ## Frist 1 Monat ab Eingang der Anfrage (Art. 12 Abs. 3 DSGVO). Verlängerung um 2 Monate bei Komplexität (mit Begründung). ## Schritte 1. Identität des Antragstellers verifizieren 2. Alle Datensätze suchen (CRM, Projekte, Rechnungen, Tickets, Audit-Log) 3. Export als maschinenlesbares Format (JSON/CSV) 4. Aufstellung der Verarbeitungszwecke + Rechtsgrundlagen 5. Empfänger der Daten auflisten 6. Speicherdauer / Löschfristen angeben 7. Rechte des Betroffenen (Widerspruch, Berichtigung, Löschung) ## AOS-Export - \`GET /api/v1/companies/{id}\` liefert die Daten - Audit-Log filtern nach entityId - Export als ZIP mit SHA-256 Prüfsumme ## Antwortvorlage > Sehr geehrte/r Frau/Herr [Name], > > hiermit informieren wir Sie gemäß Art. 15 DSGVO über die zu Ihrer Person > bei uns gespeicherten Daten...`, tags: 'dsgvo,art15,auskunft,export,prozess', authorName: 'Lena Vogt', version: 2, viewCount: 23, }, { slug: 'faq-cms-bedienung', title: 'FAQ: CMS-Bedienung für Redakteure', category: 'faq', visibility: 'client', excerpt: 'Häufige Fragen unserer Kunden zur Bedienung des AOS-CMS.', content: `# FAQ: CMS-Bedienung ## Wie logge ich mich ein? 1. Portal-URL öffnen: https://aos.nordlicht.digital 2. E-Mail + Passwort eingeben 3. Passkey-Login verfügbar (empfohlen) ## Wie erstelle ich einen Blog-Artikel? 1. Im Menü auf „Inhalte" → „Neuer Artikel" 2. Titel + Auszug eingeben 3. Inhalt im Markdown-Editor schreiben 4. Bild hochladen (Drag & Drop) 5. „Veröffentlichen" klicken ## Wie ändere ich meine Kontaktdaten? Im Profil → „Stammdaten bearbeiten". Änderungen werden protokolliert. ## Was passiert bei einem Fehler? 1. Screenshot machen 2. Ticket erstellen (Support → „Neues Ticket") 4. SLA: Antwort innerhalb 8 Stunden (werktags)`, tags: 'faq,cms,redakteur,kunde,anleitung', authorName: 'Aylin Demir', version: 7, viewCount: 142, }, { slug: 'wordpress-update-anleitung', title: 'WordPress: Sicheres Update (Wartungsvertrag)', category: 'handbuch', visibility: 'client', excerpt: 'Schritt-für-Schritt Anleitung für sichere WordPress-Updates.', content: `# WordPress-Update Anleitung ## Vor dem Update 1. **Backup erstellen** (UpdraftPlus oder manuell) 2. Wartungs-Modus aktivieren 3. Plugins deaktivieren (optional) ## Update-Reihenfolge 1. WordPress-Core aktualisieren 2. Themes aktualisieren 3. Plugins aktualisieren (einzeln, nach jedem Test) ## Nach dem Update 1. Wartungs-Modus deaktivieren 2. Seite testen (Startseite, Formulare, Checkout) 3. PageSpeed prüfen 4. Im AOS Ticket-System Update protokollieren ## Rollback Bei Fehlern: Backup wiederherstellen (UpdraftPlus → Restore). Plugin-Versionen notieren für Support-Ticket.`, tags: 'wordpress,update,wartung,anleitung', authorName: 'Tom Becker', version: 3, viewCount: 67, }, { slug: 'rechnungsstellung-prozess', title: 'Rechnungsstellung: Der komplette Prozess', category: 'handbuch', visibility: 'internal', excerpt: 'Von der Zeiterfassung bis zur bezahlten Rechnung — 0 manuelle Schritte.', content: `# Rechnungsstellungs-Prozess ## 1. Zeiterfassung Entwickler tragen Zeit im AOS ein (Timer oder manuell). Billable-Flag setzen, Projekt zuordnen. ## 2. Freigabe PM prüft + gibt die Woche frei. Zeit wird gesperrt (lock nach Freigabe). ## 3. Rechnungserstellung - Aus freigegebenen Zeit-Einträgen - Automatische Rechnungspositionen - Gapless Number: RE-2026-XXXX ## 4. E-Rechnung - ZUGFeRD-XML generiert (EN 16931) - PDF mit eingebettetem XML (Typst) - XRechnung für öffentliche Kunden ## 5. Versand - E-Mail mit PDF-Anhang - Portal-Benachrichtigung an Kunde ## 6. Zahlungseingang - Bank-Import (CAMT.053) - Auto-Matching (Referenz + Betrag) - Rechnung → „bezahlt" ## 7. Mahnwesen (falls überfällig) - Stufe 1: Zahlungserinnerung (kostenlos) - Stufe 2: Mahnung (2,50 € Gebühr) - Stufe 3: Letzte Mahnung + Verzugszins (§ 288 BGB) ## 8. DATEV-Export Monatlich: Buchungsstapel an Steuerberater.`, tags: 'rechnung,prozess,finance,datev,e-rechnung', authorName: 'Nora Klein', version: 6, viewCount: 78, }, ] for (const a of articles) { await db.article.create({ data: a }) } // --- API Tokens + Webhooks (real crypto, hashed at rest) ----------------- const crypto = await import('crypto') // Pre-built admin token for Claude Code (deterministic, so .mcp.json can reference it) // Token: aos_admin_seed_token_for_claude_code_2026 const adminToken = 'aos_admin_seed_token_for_claude_code_2026' const adminScopes = ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'crm.deal:write', 'finance.invoice:read', 'finance.invoice:write', 'finance:write', 'leads:read', 'leads:write', 'support:read', 'support:write', 'projects:write', 'licenses.license:read'] await db.apiToken.create({ data: { name: 'Claude Code Admin (seeded)', tokenHash: crypto.createHash('sha256').update(adminToken).digest('hex'), tokenPrefix: 'aos_admin', scopes: JSON.stringify(adminScopes), lastUsedAt: daysFromNow(-1), }, }) const apiTokens = [ { name: 'ZeroClaw Agent (MCP)', scopes: ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'finance.invoice:read', 'leads:read', 'leads:write', 'support:read', 'support:write'] }, { name: 'DATEV Export-Skript', scopes: ['finance.invoice:read'] }, { name: 'WordPress Update-Server', scopes: ['licenses.license:read'] }, ] for (const t of apiTokens) { const plaintext = 'aos_' + crypto.randomBytes(32).toString('hex') const tokenHash = crypto.createHash('sha256').update(plaintext).digest('hex') const tokenPrefix = 'aos_' + plaintext.slice(4, 11) await db.apiToken.create({ data: { name: t.name, tokenHash, tokenPrefix, scopes: JSON.stringify(t.scopes), lastUsedAt: daysFromNow(-2) }, }) } // one revoked token const revokedPlain = 'aos_' + crypto.randomBytes(32).toString('hex') await db.apiToken.create({ data: { name: 'Altes Backup-Skript (widerrufen)', tokenHash: crypto.createHash('sha256').update(revokedPlain).digest('hex'), tokenPrefix: 'aos_' + revokedPlain.slice(4, 11), scopes: JSON.stringify(['finance.invoice:read']), revokedAt: daysFromNow(-30), lastUsedAt: daysFromNow(-32), }, }) // Webhooks with real HMAC secrets const webhooks = [ { url: 'https://make.nordlicht.digital/webhooks/aos', events: ['invoice.paid', 'contract.signed', 'lead.approved'], sentCount: 47, failCount: 2, lastStatus: 200, lastSentAt: daysFromNow(-0.2) }, { url: 'https://hooks.slack.com/services/T0/B0/xxxx', events: ['ticket.created', 'invoice.overdue'], sentCount: 12, failCount: 0, lastStatus: 200, lastSentAt: daysFromNow(-1) }, { url: 'https://erp.hansemoebel.de/api/aos-events', events: ['license.activated'], sentCount: 5, failCount: 1, lastStatus: 500, lastSentAt: daysFromNow(-3) }, ] for (const w of webhooks) { await db.webhook.create({ data: { ...w, events: JSON.stringify(w.events), secret: 'whsec_' + crypto.randomBytes(24).toString('hex') }, }) } await db.auditLog.create({ data: { actor: 'max@nordlicht.digital', action: 'api.token:create', entity: 'ApiToken', entityId: 'seed', after: '{"name":"ZeroClaw Agent","scopes":["crm.company:read"]}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) await db.auditLog.create({ data: { actor: 'max@nordlicht.digital', action: 'webhook.create', entity: 'Webhook', entityId: 'seed', after: '{"url":"make.nordlicht.digital","events":["invoice.paid"]}', hash: 'sha256:' + Math.random().toString(36).slice(2) } }) console.log('Seed complete.') } main() .catch((e) => { console.error(e) process.exit(1) }) .finally(async () => { await db.$disconnect() }) // --- Custom Fields (PRD §5.5: JSONB with schema per entity) --------------- const customFields = [ { entityType: 'company', key: 'branche', label: 'Branche', type: 'select', options: JSON.stringify(['Handwerk', 'Gastronomie', 'Einzelhandel', 'Dienstleistung', 'Industrie', 'Gesundheit', 'Technologie']), required: false, sortOrder: 1 }, { entityType: 'company', key: 'mitarbeiterzahl', label: 'Mitarbeiterzahl', type: 'select', options: JSON.stringify(['1-10', '11-50', '51-200', '201-500', '500+']), required: false, sortOrder: 2 }, { entityType: 'company', key: 'umsatzklasse', label: 'Umsatzklasse', type: 'select', options: JSON.stringify(['< 100k', '100k-500k', '500k-2M', '2M-10M', '10M+']), required: false, sortOrder: 3 }, { entityType: 'person', key: 'entscheider', label: 'Entscheider?', type: 'boolean', required: false, sortOrder: 1 }, { entityType: 'person', key: 'kontaktweg', label: 'Bevorzugter Kontaktweg', type: 'select', options: JSON.stringify(['E-Mail', 'Telefon', 'Teams', 'Persönlich']), required: false, sortOrder: 2 }, { entityType: 'deal', key: 'wettbewerber', label: 'Wettbewerber', type: 'string', required: false, sortOrder: 1 }, { entityType: 'deal', key: 'entscheidungsgrund', label: 'Entscheidungsgrund', type: 'select', options: JSON.stringify(['Preis', 'Qualität', 'Geschwindigkeit', 'Referenzen', 'Technologie']), required: false, sortOrder: 2 }, { entityType: 'project', key: 'prioritaet', label: 'Priorität (kundenseitig)', type: 'select', options: JSON.stringify(['niedrig', 'normal', 'hoch', 'kritisch']), required: false, sortOrder: 1 }, { entityType: 'invoice', key: 'kostenstelle', label: 'Kostenstelle', type: 'string', required: false, sortOrder: 1 }, { entityType: 'ticket', key: 'auswirkung', label: 'Geschäftsauswirkung', type: 'select', options: JSON.stringify(['keine', 'niedrig', 'mittel', 'hoch', 'kritisch']), required: false, sortOrder: 1 }, ] for (const cf of customFields) { await db.customFieldDef.create({ data: cf }) }