Files
AOS/src/lib/seed.ts
T

746 lines
65 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, string> = {
'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<string, string> = {}
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<string, string> = {}
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
await db.task.create({ data: { ...rest, projectId: projectMap[project] } })
}
// --- 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) } })
// --- 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) } })
}
// --- API Tokens + Webhooks (real crypto, hashed at rest) -----------------
const crypto = await import('crypto')
const apiTokens = [
{ name: 'ZeroClaw Agent (read-only)', scopes: ['crm.company:read', 'finance.invoice:read', 'licenses.license:read'] },
{ 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()
})