746 lines
65 KiB
TypeScript
746 lines
65 KiB
TypeScript
// 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()
|
||
})
|