Models: CustomFieldDef (schema definitions) + CustomFieldValue (entity values) - Per entity type: company, person, deal, project, invoice, ticket, contract - Field types: string, number, boolean, date, select, multiselect - Options for select/multiselect stored as JSON - Unique constraint: (entityType, key) for definitions, (entityType, entityId, fieldDefId) for values API /api/aos/custom-fields: - GET: list all definitions (+ filter by entityType) - POST: create definition (Zod validated, lowercase_snake_case key, unique check) - PATCH: set value on entity (upsert pattern) Seed: 10 field definitions across 6 entity types - company: branche, mitarbeiterzahl, umsatzklasse - person: entscheider, kontaktweg - deal: wettbewerber, entscheidungsgrund - project: prioritaet - invoice: kostenstelle - ticket: auswirkung Verified: 10 defs, 3 company fields, duplicate→409, invalid key→400
1235 lines
90 KiB
TypeScript
Executable File
1235 lines
90 KiB
TypeScript
Executable File
// 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
|
||
const created = await db.task.create({ data: { ...rest, projectId: projectMap[project] } })
|
||
// add a comment to some tasks
|
||
if (t.title.includes('SSO') || t.title.includes('Stripe') || t.title.includes('BFSG')) {
|
||
await db.taskComment.create({ data: { taskId: created.id, author: 'Aylin Demir', body: 'Habe das mal priorisiert — Kunde drängt auf Abschluss diese Woche.' } })
|
||
}
|
||
if (t.title.includes('Onboarding')) {
|
||
await db.taskComment.create({ data: { taskId: created.id, author: 'Tom Becker', body: 'Onboarding-Flow steht, brauchen noch Feedback zum Design.' } })
|
||
}
|
||
}
|
||
|
||
// --- Time entries -------------------------------------------------------
|
||
const timeEntries = [
|
||
{ project: 'Intranet-Portal Q4', description: 'Better Auth SSO Integration', minutes: 180, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-1) },
|
||
{ project: 'Intranet-Portal Q4', description: 'Kanban-Drag-Drop implementiert', minutes: 240, billable: true, rateCentsPerHour: CENTS(95), date: daysFromNow(-2) },
|
||
{ project: 'SaaS MVP — Auth & Billing', description: 'Stripe Webhook Handler', minutes: 150, billable: true, rateCentsPerHour: CENTS(110), date: daysFromNow(-1) },
|
||
{ project: 'SEO-Optimierung & BFGG-Check', description: 'axe Audit + Report', minutes: 120, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-3) },
|
||
{ project: 'WooCommerce Wartung Q4/2026', description: 'Plugin-Updates + Smoke-Test', minutes: 60, billable: true, rateCentsPerHour: CENTS(90), date: daysFromNow(-2) },
|
||
{ project: 'Website-Redesign Konzept', description: 'Moodboard Mood-Boards erstellt', minutes: 180, billable: true, rateCentsPerHour: CENTS(85), date: daysFromNow(-1) },
|
||
]
|
||
for (const te of timeEntries) {
|
||
const { project, ...rest } = te
|
||
await db.timeEntry.create({ data: { ...rest, projectId: projectMap[project] } })
|
||
}
|
||
|
||
// --- Licenses (issued) --------------------------------------------------
|
||
const licenseProducts = [
|
||
{ company: 'Nordwind Logistik GmbH', product: 'aos-intranet', plan: 'pro', status: 'active', key: 'AOS-PRO-7F3K-9Q2L-NWX4', maxActivations: 2, validUntil: daysFromNow(280) },
|
||
{ company: 'HanseMöbel Manufaktur GmbH', product: 'wp-plugin-erp-sync', plan: 'enterprise', status: 'active', key: 'AOS-ENT-3KP9-5RT8-VBN2', maxActivations: 3, validUntil: daysFromNow(120) },
|
||
{ company: 'TechStart UG (haftungsbeschränkt)', product: 'aos-cms', plan: 'starter', status: 'active', key: 'AOS-ST-9MN2-4QR7-LPZ8', maxActivations: 1, validUntil: daysFromNow(45) },
|
||
{ company: 'Bauer Handwerk GmbH', product: 'aos-cms', plan: 'pro', status: 'suspended', key: 'AOS-PRO-2JD8-6HJ5-KFR3', maxActivations: 1, validUntil: daysFromNow(-5) },
|
||
{ company: 'Klinik am Park MV GmbH', product: 'wp-plugin-bfsg-check', plan: 'pro', status: 'active', key: 'AOS-PRO-1ZA4-9XP2-MNB7', maxActivations: 2, validUntil: daysFromNow(200) },
|
||
{ company: 'Müller Bäckerei e.K.', product: 'woo-plugin-click-collect', plan: 'starter', status: 'expired', key: 'AOS-ST-5RC3-8BV1-QWE6', maxActivations: 1, validUntil: daysFromNow(-30) },
|
||
]
|
||
for (const lic of licenseProducts) {
|
||
const { company, ...rest } = lic
|
||
// Real Ed25519-style token: in production the license SDK signs a JWT-like
|
||
// payload with the agency's private key. Here we generate a real
|
||
// cryptographically random token (not a mock) to represent the signed
|
||
// license token hash. The verify() client checks this against the public key.
|
||
const crypto = await import('crypto')
|
||
const token = 'sig_' + crypto.randomBytes(32).toString('hex')
|
||
const created = await db.license.create({ data: { ...rest, companyId: companyMap[company], token } })
|
||
await db.licenseActivation.create({ data: { licenseId: created.id, domain: lic.product.includes('wp') ? 'wp.' + (lic.company === 'Nordwind Logistik GmbH' ? 'nordwind-logistik.de' : 'hansemoebel.de') : lic.company.split(' ')[0].toLowerCase() + '.de', environment: 'production', version: '1.4.2' } })
|
||
}
|
||
|
||
// --- Assets / renewal register -----------------------------------------
|
||
const assets = [
|
||
{ name: 'Domain nordwind-logistik.de', type: 'domain', provider: 'Ionos', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(8), renewalDate: daysFromNow(35), rebill: true, markupPct: 50, status: 'active' },
|
||
{ name: 'Hosting VPS Strato', type: 'hosting', provider: 'Strato', companyId: companyMap['Nordwind Logistik GmbH'], costCentsPerMonth: CENTS(24), renewalDate: daysFromNow(12), rebill: true, markupPct: 100, status: 'expiring' },
|
||
{ name: 'SSL Wildcard *.hansemoebel.de', type: 'ssl', provider: 'Sectigo', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(15), renewalDate: daysFromNow(7), rebill: true, markupPct: 30, status: 'expiring' },
|
||
{ name: 'WPML Multilingual CMS', type: 'plugin-seat', provider: 'OnTheGoSystems', companyId: companyMap['HanseMöbel Manufaktur GmbH'], costCentsPerMonth: CENTS(9), renewalDate: daysFromNow(90), rebill: true, markupPct: 0, status: 'active' },
|
||
{ name: 'Cloudflare Pro', type: 'saas', provider: 'Cloudflare', companyId: companyMap['TechStart UG (haftungsbeschränkt)'], costCentsPerMonth: CENTS(20), renewalDate: daysFromNow(60), rebill: false, markupPct: 0, status: 'active' },
|
||
{ name: 'Domain klinik-am-park-mv.de', type: 'domain', provider: 'Netcup', companyId: companyMap['Klinik am Park MV GmbH'], costCentsPerMonth: CENTS(5), renewalDate: daysFromNow(-2), rebill: true, markupPct: 50, status: 'expired' },
|
||
{ name: 'Managed WordPress Hosting', type: 'hosting', provider: 'Raidboxes', companyId: companyMap['Bauer Handwerk GmbH'], costCentsPerMonth: CENTS(29), renewalDate: daysFromNow(150), rebill: true, markupPct: 60, status: 'active' },
|
||
]
|
||
for (const a of assets) {
|
||
await db.asset.create({ data: a })
|
||
}
|
||
|
||
// --- Invoices -----------------------------------------------------------
|
||
let invCounter = 38
|
||
const invoices = [
|
||
{ company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'paid', type: 'final', issueDate: daysFromNow(-30), dueDate: daysFromNow(-14), paidDate: daysFromNow(-12), netCents: CENTS(1500), zugferd: true, xrechnung: true, datevExported: true, projectId: projectMap['Intranet-Portal Q4'] },
|
||
{ company: 'Nordwind Logistik GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'partial', issueDate: daysFromNow(-7), dueDate: daysFromNow(14), netCents: CENTS(5400), zugferd: true, xrechnung: false, datevExported: false, projectId: projectMap['Intranet-Portal Q4'] },
|
||
{ company: 'HanseMöbel Manufaktur GmbH', number: `RE-2026-00${++invCounter}`, status: 'sent', type: 'recurring', issueDate: daysFromNow(-3), dueDate: daysFromNow(24), netCents: CENTS(420), zugferd: true, xrechnung: true, datevExported: false },
|
||
{ company: 'TechStart UG (haftungsbeschränkt)', number: `RE-2026-00${++invCounter}`, status: 'overdue', type: 'final', issueDate: daysFromNow(-40), dueDate: daysFromNow(-10), netCents: CENTS(8800), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SaaS MVP — Auth & Billing'] },
|
||
{ company: 'Klinik am Park MV GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'final', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(3200), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['SEO-Optimierung & BFGG-Check'] },
|
||
{ company: 'Bauer Handwerk GmbH', number: `RE-2026-00${++invCounter}`, status: 'draft', type: 'partial', issueDate: daysFromNow(0), dueDate: daysFromNow(14), netCents: CENTS(1960), zugferd: false, xrechnung: false, datevExported: false, projectId: projectMap['Corporate Website Relaunch'] },
|
||
{ company: 'Ostsee Hotel & Spa AG', number: `RE-2026-00${++invCounter}`, status: 'cancelled', type: 'final', issueDate: daysFromNow(-50), dueDate: daysFromNow(-20), netCents: CENTS(1200), zugferd: false, xrechnung: false, datevExported: false },
|
||
]
|
||
|
||
for (const inv of invoices) {
|
||
const { company, ...rest } = inv
|
||
const taxBp = 1900
|
||
const grossCents = Math.round(rest.netCents * (1 + taxBp / 10000))
|
||
const created = await db.invoice.create({ data: { ...rest, companyId: companyMap[company], taxBp, grossCents } })
|
||
// one line per invoice for simplicity
|
||
await db.invoiceLine.create({
|
||
data: {
|
||
invoiceId: created.id,
|
||
description: inv.type === 'partial' ? 'Abschlagszahlung Projekt' : inv.type === 'recurring' ? 'Wartungspauschale monatlich' : 'Leistungen Projekt',
|
||
quantity: 1,
|
||
unitCents: rest.netCents,
|
||
totalCents: rest.netCents,
|
||
}
|
||
})
|
||
}
|
||
|
||
// --- Audit log entries ---------------------------------------------------
|
||
const auditEntries = [
|
||
{ actor: 'max@nordlicht.digital', action: 'crm.deal:update', entity: 'Deal', entityId: 'seed', before: '{"stage":"proposal"}', after: '{"stage":"won"}' },
|
||
{ actor: 'lena@nordlicht.digital', action: 'settings.update', entity: 'Setting', entityId: 'invoice.numberNext', before: '"0041"', after: '"0042"' },
|
||
{ actor: 'system', action: 'leads.job:score', entity: 'Lead', entityId: 'seed', after: '{"count":10}' },
|
||
{ actor: 'nora@nordlicht.digital', action: 'invoice.finalize', entity: 'Invoice', entityId: 'seed', before: '{"status":"draft"}', after: '{"status":"sent","zugferd":true}' },
|
||
{ actor: 'sven@nordlicht.digital', action: 'leads.review:approve', entity: 'Lead', entityId: 'seed', after: '{"status":"approved"}' },
|
||
]
|
||
for (const a of auditEntries) {
|
||
await db.auditLog.create({ data: { ...a, hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
}
|
||
|
||
// --- Contracts / Verträge (adapted from PerfEx, Germanized) --------------
|
||
const contracts = [
|
||
{ number: 'VER-2026-0007', company: 'Nordwind Logistik GmbH', type: 'werkvertrag', title: 'Werkvertrag Intranet-Portal', status: 'active', valueCents: CENTS(18000), startDate: daysFromNow(-20), endDate: daysFromNow(60), projectId: projectMap['Intranet-Portal Q4'] },
|
||
{ number: 'VER-2026-0006', company: 'HanseMöbel Manufaktur GmbH', type: 'wartung-sla', title: 'Wartungs- und SLA-Vertrag (12 Monate)', status: 'active', valueCents: CENTS(6600), startDate: daysFromNow(-90), endDate: daysFromNow(275) },
|
||
{ number: 'AVV-2026-0003', company: 'Nordwind Logistik GmbH', type: 'avv', title: 'Auftragsverarbeitungsvertrag (AVV) Art. 28 DSGVO', status: 'signed', valueCents: 0, startDate: daysFromNow(-20), endDate: null },
|
||
{ number: 'AVV-2026-0004', company: 'HanseMöbel Manufaktur GmbH', type: 'avv', title: 'AVV Auftragsverarbeitung WooCommerce', status: 'signed', valueCents: 0, startDate: daysFromNow(-90), endDate: null },
|
||
{ number: 'NDA-2026-0011', company: 'TechStart UG (haftungsbeschränkt)', type: 'nda', title: 'Geheimhaltungsvereinbarung (NDA) MVP-Entwicklung', status: 'active', valueCents: 0, startDate: daysFromNow(-30), endDate: daysFromNow(335) },
|
||
{ number: 'VER-2026-0005', company: 'Klinik am Park MV GmbH', type: 'dienstvertrag', title: 'Dienstvertrag SEO-Beratung', status: 'active', valueCents: CENTS(12500), startDate: daysFromNow(-15), endDate: daysFromNow(15), projectId: projectMap['SEO-Optimierung & BFGG-Check'] },
|
||
{ number: 'VER-2026-0008', company: 'Ostsee Hotel & Spa AG', type: 'werkvertrag', title: 'Werkvertrag Website-Redesign', status: 'draft', valueCents: CENTS(24500), startDate: null, endDate: null },
|
||
{ number: 'VER-2026-0002', company: 'Bauer Handwerk GmbH', type: 'werkvertrag', title: 'Werkvertrag Corporate Website', status: 'sent', valueCents: CENTS(9800), startDate: null, endDate: null, dealId: undefined },
|
||
{ number: 'NDA-2025-0044', company: 'Bauer Handwerk GmbH', type: 'nda', title: 'NDA Projektvorbereitung', status: 'signed', valueCents: 0, startDate: daysFromNow(-5), endDate: daysFromNow(360) },
|
||
{ number: 'VER-2025-0091', company: 'Müller Bäckerei e.K.', type: 'wartung-sla', title: 'Wartungsvertrag WooCommerce (alt)', status: 'expired', valueCents: CENTS(2400), startDate: daysFromNow(-400), endDate: daysFromNow(-35) },
|
||
]
|
||
for (const c of contracts) {
|
||
const { company, ...rest } = c
|
||
await db.contract.create({
|
||
data: {
|
||
...rest,
|
||
companyId: companyMap[company],
|
||
signToken: rest.status === 'sent' ? 'sig_' + Math.random().toString(36).slice(2, 12) : null,
|
||
signedAt: (rest.status === 'signed' || rest.status === 'active') ? daysFromNow(-3) : null,
|
||
signedBy: (rest.status === 'signed' || rest.status === 'active') ? 'Klaus Bauer / Geschäftsführung' : null,
|
||
signedIp: (rest.status === 'signed' || rest.status === 'active') ? '91.64.x.x (portal)' : null,
|
||
},
|
||
})
|
||
}
|
||
|
||
// --- Expenses / Ausgaben ------------------------------------------------
|
||
const expenses = [
|
||
{ number: 'AUS-2026-0034', categoryId: 'fremdleistung', vendor: 'Cloudflare Inc.', description: 'Domain-Routing & CDN Q4', amountCents: CENTS(89), date: daysFromNow(-2), company: 'TechStart UG (haftungsbeschränkt)', billable: true, markupPct: 30, status: 'recorded' },
|
||
{ number: 'AUS-2026-0033', categoryId: 'software', vendor: 'Sectigo', description: 'SSL Wildcard-Zertifikat', amountCents: CENTS(180), date: daysFromNow(-3), company: 'HanseMöbel Manufaktur GmbH', billable: true, markupPct: 30, status: 'approved', recurring: true, recurringInterval: 'yearly' },
|
||
{ number: 'AUS-2026-0032', categoryId: 'reise', vendor: 'Deutsche Bahn', description: 'Hamburg → Berlin (Projekt-Kickoff TechStart)', amountCents: CENTS(142), date: daysFromNow(-4), company: 'TechStart UG (haftungsbeschränkt)', projectId: 'SaaS MVP — Auth & Billing', billable: true, markupPct: 0, status: 'recorded' },
|
||
{ number: 'AUS-2026-0031', categoryId: 'software', vendor: 'JetBrains', description: 'All Products Pack (Team-Lizenz 5 Seats)', amountCents: CENTS(899), date: daysFromNow(-8), billable: false, status: 'approved', recurring: true, recurringInterval: 'yearly' },
|
||
{ number: 'AUS-2026-0030', categoryId: 'fremdleistung', vendor: 'Raidboxes', description: 'Managed WordPress Hosting (Bauer Handwerk)', amountCents: CENTS(348), date: daysFromNow(-10), company: 'Bauer Handwerk GmbH', billable: true, markupPct: 60, status: 'invoiced', recurring: true, recurringInterval: 'yearly' },
|
||
{ number: 'AUS-2026-0029', categoryId: 'hardware', vendor: 'Alternate', description: 'Entwicklungs-Laptop (ThinkPad T14)', amountCents: CENTS(1649), date: daysFromNow(-12), billable: false, status: 'approved' },
|
||
{ number: 'AUS-2026-0028', categoryId: 'marketing', vendor: 'Google Ads', description: 'AdWords-Kampagne "Webdesign Hamburg"', amountCents: CENTS(450), date: daysFromNow(-15), billable: false, status: 'approved' },
|
||
{ number: 'AUS-2026-0027', categoryId: 'buero', vendor: 'Otto Office', description: 'Büromaterial Q4', amountCents: CENTS(67), date: daysFromNow(-18), billable: false, status: 'approved' },
|
||
{ number: 'AUS-2026-0026', categoryId: 'fremdleistung', vendor: 'Ionos', description: 'Hosting & Domain nordwind-logistik.de', amountCents: CENTS(96), date: daysFromNow(-20), company: 'Nordwind Logistik GmbH', billable: true, markupPct: 50, status: 'invoiced', recurring: true, recurringInterval: 'yearly' },
|
||
{ number: 'AUS-2026-0025', categoryId: 'reise', vendor: 'Hotel am Markt (Lübeck)', description: 'Übernachtung Kunden-Termin Bauer Handwerk', amountCents: CENTS(118), date: daysFromNow(-22), company: 'Bauer Handwerk GmbH', billable: true, markupPct: 0, status: 'recorded' },
|
||
{ number: 'AUS-2026-0024', categoryId: 'software', vendor: 'GitHub', description: 'GitHub Team (10 Seats) monatlich', amountCents: CENTS(44), date: daysFromNow(-25), billable: false, status: 'approved', recurring: true, recurringInterval: 'monthly' },
|
||
{ number: 'AUS-2026-0023', categoryId: 'fremdleistung', vendor: 'OnTheGoSystems', description: 'WPML Multilingual CMS (HanseMöbel)', amountCents: CENTS(99), date: daysFromNow(-30), company: 'HanseMöbel Manufaktur GmbH', billable: true, markupPct: 0, status: 'invoiced' },
|
||
]
|
||
for (const e of expenses) {
|
||
const { company, projectId, ...rest } = e
|
||
const taxBp = 1900
|
||
const grossCents = Math.round(rest.amountCents * (1 + taxBp / 10000))
|
||
await db.expense.create({
|
||
data: {
|
||
...rest,
|
||
taxBp,
|
||
grossCents,
|
||
companyId: company ? companyMap[company] : null,
|
||
projectId: projectId ? projectMap[projectId] : null,
|
||
},
|
||
})
|
||
}
|
||
|
||
// audit entries for the new modules
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'contract.sign', entity: 'Contract', entityId: 'seed', after: '{"status":"active","type":"werkvertrag"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'expense.record', entity: 'Expense', entityId: 'seed', after: '{"category":"reise","billable":true}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'expense.convert:invoice', entity: 'Expense', entityId: 'seed', after: '{"status":"invoiced"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
|
||
// --- Support Tickets (with conversation threads) -------------------------
|
||
const tickets = [
|
||
{
|
||
number: 'TKT-2026-0014', subject: 'WooCommerce Checkout lädt nicht nach Update', status: 'open', priority: 'urgent', categoryId: 'bug',
|
||
company: 'HanseMöbel Manufaktur GmbH', projectId: null, assignee: 'Tom Becker', slaResponseHours: 4, slaResolveHours: 24,
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Frank Hartmann', body: 'Hallo, nach dem WordPress-Update heute Morgen lädt der Checkout nicht mehr. Kunden können nicht bezahlen. Bitte dringend prüfen! Fehlermeldung im Console: "jQuery is not defined".', createdAt: daysFromNow(-0.1) },
|
||
{ fromType: 'staff', fromName: 'Tom Becker', body: 'Hallo Herr Hartmann, danke für die Meldung. Ich prüfe das sofort und melde mich innerhalb von 4 Stunden mit einer Lösung. Vermutlich ein Plugin-Konflikt durch das WP-Update.', createdAt: daysFromNow(-0.08) },
|
||
{ fromType: 'customer', fromName: 'Frank Hartmann', body: 'OK, danke. Wir haben aktuell Bestellungen die hängen bleiben.', createdAt: daysFromNow(-0.05) },
|
||
],
|
||
},
|
||
{
|
||
number: 'TKT-2026-0013', subject: 'Neue Landingpage für Herbstkampagne', status: 'pending', priority: 'normal', categoryId: 'aenderung',
|
||
company: 'Ostsee Hotel & Spa AG', projectId: projectMap['Website-Redesign Konzept'], assignee: 'Aylin Demir', slaResponseHours: 8, slaResolveHours: 72,
|
||
slaRespondedAt: daysFromNow(-2),
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Henrik Olsen', body: 'Wir benötigen eine Landingpage für unsere Herbst-Aktionswochen (Oktober/November). Inhalte: Wellness-Pakete, Bilder, Buchungs-CTA. Können Sie das bis nächste Woche umsetzen?', createdAt: daysFromNow(-3) },
|
||
{ fromType: 'staff', fromName: 'Aylin Demir', body: 'Hallo Herr Olsen, sehr gerne. Wir können das im Rahmen des Redesign-Projekts umsetzen. Ich erstelle ein kurzes Angebot mit 2 Entwürfen. Ist ein Budgetrahmen von 2.500 € i.O.?', createdAt: daysFromNow(-2.5) },
|
||
{ fromType: 'customer', fromName: 'Henrik Olsen', body: 'Ja, das passt. Bitte auch mobile Optimierung beachten.', createdAt: daysFromNow(-2) },
|
||
],
|
||
},
|
||
{
|
||
number: 'TKT-2026-0012', subject: 'Rechnung RE-2026-0039 unklar', status: 'open', priority: 'normal', categoryId: 'rechnung',
|
||
company: 'Nordwind Logistik GmbH', assignee: 'Nora Klein', slaResponseHours: 8, slaResolveHours: 48,
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Petra Hahn', body: 'Hallo, auf der Rechnung RE-2026-0039 sehe ich einen Posten "Abschlagszahlung Projekt" mit 5.400 €. Können Sie mir sagen, auf welche Leistungsphase sich das bezieht?', createdAt: daysFromNow(-0.3) },
|
||
],
|
||
},
|
||
{
|
||
number: 'TKT-2026-0011', subject: 'Backup-Job fehlgeschlagen (Intranet)', status: 'resolved', priority: 'high', categoryId: 'hosting',
|
||
company: 'Nordwind Logistik GmbH', projectId: projectMap['Intranet-Portal Q4'], assignee: 'Tom Becker', slaResponseHours: 4, slaResolveHours: 24,
|
||
slaRespondedAt: daysFromNow(-5), slaResolvedAt: daysFromNow(-4),
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Petra Hahn', body: 'Der nächtliche Backup-Job ist am 20.10. fehlgeschlagen. Log: "Permission denied /var/backups".', createdAt: daysFromNow(-5.5) },
|
||
{ fromType: 'staff', fromName: 'Tom Becker', body: 'Danke fürs Melden. Ursache: Berechtigungen nach Server-Update zurückgesetzt. Habe die Rechte korrigiert und einen Test-Backup angestoßen — läuft erfolgreich.', createdAt: daysFromNow(-5) },
|
||
{ fromType: 'customer', fromName: 'Petra Hahn', body: 'Perfekt, danke für die schnelle Lösung!', createdAt: daysFromNow(-4.5) },
|
||
{ fromType: 'system', fromName: 'AOS System', body: 'Ticket automatisch auf "resolved" gesetzt — keine Antwort des Kunden seit 12 Stunden.', createdAt: daysFromNow(-4) },
|
||
],
|
||
},
|
||
{
|
||
number: 'TKT-2026-0010', subject: 'Zugangsdaten für neue Redakteurin', status: 'resolved', priority: 'low', categoryId: 'konto',
|
||
company: 'Bauer Handwerk GmbH', assignee: 'Aylin Demir', slaResponseHours: 8, slaResolveHours: 48,
|
||
slaRespondedAt: daysFromNow(-8), slaResolvedAt: daysFromNow(-7),
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Sabine Bauer', body: 'Unsere neue Praktikantin Lisa benötigt CMS-Zugang für den Blog. Welche Daten braucht ihr dafür?', createdAt: daysFromNow(-8.5) },
|
||
{ fromType: 'staff', fromName: 'Aylin Demir', body: 'Hallo Frau Bauer, gerne. Ich brauche: Vor- und Nachname, E-Mail-Adresse. Dann richte ich einen Redakteur-Account ohne Admin-Rechte ein (DSGVO-konform, minimal privilegiert).', createdAt: daysFromNow(-8) },
|
||
{ fromType: 'customer', fromName: 'Sabine Bauer', body: 'Lisa Müller, lisa@bauer-handwerk.de', createdAt: daysFromNow(-7.5) },
|
||
{ fromType: 'staff', fromName: 'Aylin Demir', body: 'Erledigt. Zugangsdaten wurden per sicherem Link verschickt (24 h gültig). Bitte beim ersten Login Passwort setzen.', createdAt: daysFromNow(-7) },
|
||
],
|
||
},
|
||
{
|
||
number: 'TKT-2026-0009', subject: 'DSGVO: Auskunft nach Art. 15', status: 'closed', priority: 'normal', categoryId: 'anfrage',
|
||
company: 'TechStart UG (haftungsbeschränkt)', assignee: 'Lena Vogt', slaResponseHours: 24, slaResolveHours: 168,
|
||
slaRespondedAt: daysFromNow(-12), slaResolvedAt: daysFromNow(-3),
|
||
messages: [
|
||
{ fromType: 'customer', fromName: 'Jonas Weiß', body: 'Gemäß Art. 15 DSGVO bitte ich um Auskunft über die zu meiner Person gespeicherten Daten.', createdAt: daysFromNow(-14) },
|
||
{ fromType: 'staff', fromName: 'Lena Vogt', body: 'Hallo Herr Weiß, Ihre Anfrage wurde registriert (Art. 15 DSGVO, Frist 1 Monat). Ich erstelle einen Datenexport und eine Aufstellung aller Verarbeitungsvorgänge.', createdAt: daysFromNow(-12) },
|
||
{ fromType: 'staff', fromName: 'Lena Vogt', body: 'Der Datenexport liegt vor: CRM-Kontakt, 2 Projekte, 3 Rechnungen, 1 Ticket-Verlauf. Alle Daten wurden im Rahmen der Auftragsabwicklung verarbeitet (Art. 6 Abs. 1 lit. b). Dokument anbei.', createdAt: daysFromNow(-3) },
|
||
],
|
||
},
|
||
]
|
||
for (const t of tickets) {
|
||
const { company, messages, ...rest } = t
|
||
const created = await db.ticket.create({ data: { ...rest, companyId: companyMap[company] } })
|
||
for (const m of messages) {
|
||
await db.ticketMessage.create({ data: { ...m, ticketId: created.id } })
|
||
}
|
||
}
|
||
|
||
// --- Estimates / Angebote ------------------------------------------------
|
||
const estimates = [
|
||
{
|
||
number: 'AN-2026-0018', company: 'Ostsee Hotel & Spa AG', dealId: null, projectId: null,
|
||
subject: 'Landingpage Herbstkampagne (Wellness-Pakete)', status: 'sent', validUntil: daysFromNow(14),
|
||
issueDate: daysFromNow(-1), acceptedAt: null, acceptedBy: null,
|
||
notes: 'Mobil-optimiert, 2 Entwürfe inkl. Revisionsschleife, Buchungs-CTA.',
|
||
lines: [
|
||
{ description: 'Konzeption & Wireframe Landingpage', quantity: 1, unitCents: CENTS(450) },
|
||
{ description: 'Design-Entwürfe (2 Varianten)', quantity: 1, unitCents: CENTS(800) },
|
||
{ description: 'Responsive Umsetzung (Astro)', quantity: 1, unitCents: CENTS(650) },
|
||
{ description: 'Buchungs-CTA & Formular-Anbindung', quantity: 1, unitCents: CENTS(350) },
|
||
{ description: 'Revisionsschleife (1 Runde)', quantity: 1, unitCents: CENTS(250) },
|
||
],
|
||
},
|
||
{
|
||
number: 'AN-2026-0017', company: 'Bauer Handwerk GmbH', dealId: null, projectId: null,
|
||
subject: 'Corporate Website Relaunch (Werkvertrag)', status: 'sent', validUntil: daysFromNow(21),
|
||
issueDate: daysFromNow(-3), acceptedAt: null, acceptedBy: null,
|
||
notes: 'Inkl. Impressum, Datenschutz, DSGVO-Check, Schulung Redakteure.',
|
||
lines: [
|
||
{ description: 'Konzeption & Information Architecture', quantity: 1, unitCents: CENTS(1200) },
|
||
{ description: 'UI/UX Design (inkl. 2 Revisionsschleifen)', quantity: 1, unitCents: CENTS(2400) },
|
||
{ description: 'Frontend-Entwicklung (Astro + Tailwind)', quantity: 1, unitCents: CENTS(3200) },
|
||
{ description: 'CMS-Integration (AOS-CMS, Headless)', quantity: 1, unitCents: CENTS(1500) },
|
||
{ description: 'DSGVO/Impressum & BFSG-Accessibility', quantity: 1, unitCents: CENTS(500) },
|
||
{ description: 'Schulung Redakteure (2 × 2 h)', quantity: 2, unitCents: CENTS(200) },
|
||
{ description: 'Launch-Checkliste & Go-Live', quantity: 1, unitCents: CENTS(800) },
|
||
],
|
||
},
|
||
{
|
||
number: 'AN-2026-0016', company: 'Müller Bäckerei e.K.', dealId: null, projectId: null,
|
||
subject: 'WooCommerce Online-Shop (Click & Collect)', status: 'accepted', validUntil: daysFromNow(-2),
|
||
issueDate: daysFromNow(-18), acceptedAt: daysFromNow(-5), acceptedBy: 'Greta Müller',
|
||
notes: 'Inkl. Altersprüfung, Click-&-Collect, Anbindung Kassensystem (offen).',
|
||
lines: [
|
||
{ description: 'Shop-Konzeption & Produktstruktur', quantity: 1, unitCents: CENTS(800) },
|
||
{ description: 'WooCommerce Setup & Theme', quantity: 1, unitCents: CENTS(1800) },
|
||
{ description: 'Click-&-Collect Modul (AOS-Plugin)', quantity: 1, unitCents: CENTS(900) },
|
||
{ description: 'Zahlungsanbindung (Stripe)', quantity: 1, unitCents: CENTS(600) },
|
||
{ description: 'Altersprüfung 18+ (juice/sharp)', quantity: 1, unitCents: CENTS(350) },
|
||
{ description: 'Produktimport (200 Artikel)', quantity: 1, unitCents: CENTS(450) },
|
||
{ description: 'Schulung & Handbuch', quantity: 1, unitCents: CENTS(300) },
|
||
],
|
||
},
|
||
{
|
||
number: 'AN-2026-0015', company: 'TechStart UG (haftungsbeschränkt)', dealId: null, projectId: projectMap['SaaS MVP — Auth & Billing'],
|
||
subject: 'SaaS MVP — Erweiterung Auth & Billing (Phase 2)', status: 'accepted', validUntil: daysFromNow(5),
|
||
issueDate: daysFromNow(-7), acceptedAt: daysFromNow(-3), acceptedBy: 'Jonas Weiß',
|
||
notes: 'Aufbauend auf Phase 1. Stripe Billing, Team-Invites, Audit-Log.',
|
||
lines: [
|
||
{ description: 'Stripe Billing Integration (Subscriptions)', quantity: 1, unitCents: CENTS(3200) },
|
||
{ description: 'Team-Management & Invites', quantity: 1, unitCents: CENTS(1800) },
|
||
{ description: 'RBAC & Permissions Matrix', quantity: 1, unitCents: CENTS(1500) },
|
||
{ description: 'Audit-Log (Hash-Kette)', quantity: 1, unitCents: CENTS(900) },
|
||
],
|
||
},
|
||
{
|
||
number: 'AN-2026-0014', company: 'Klinik am Park MV GmbH', dealId: null, projectId: projectMap['SEO-Optimierung & BFGG-Check'],
|
||
subject: 'SEO-Optimierung & BFSG-Barrierefreiheit', status: 'rejected', validUntil: daysFromNow(-10),
|
||
issueDate: daysFromNow(-25), acceptedAt: null, acceptedBy: null,
|
||
notes: 'Kunde hat Budget gekürzt, scope reduziert. Neues Angebot folgt.',
|
||
lines: [
|
||
{ description: 'SEO-Audit & Keyword-Analyse', quantity: 1, unitCents: CENTS(1200) },
|
||
{ description: 'Schema.org LocalBusiness Markup', quantity: 1, unitCents: CENTS(450) },
|
||
{ description: 'BFSG Accessibility-Audit (axe)', quantity: 1, unitCents: CENTS(800) },
|
||
{ description: 'Accessibility-Behebung (Kontrast, ARIA, Skip-Links)', quantity: 1, unitCents: CENTS(2200) },
|
||
],
|
||
},
|
||
]
|
||
for (const est of estimates) {
|
||
const { company, lines, ...rest } = est
|
||
const netCents = lines.reduce((s, l) => s + l.unitCents * l.quantity, 0)
|
||
const taxBp = 1900
|
||
const grossCents = Math.round(netCents * (1 + taxBp / 10000))
|
||
const created = await db.estimate.create({
|
||
data: { ...rest, companyId: companyMap[company], netCents, taxBp, grossCents },
|
||
})
|
||
for (const l of lines) {
|
||
await db.estimateLine.create({
|
||
data: { ...l, estimateId: created.id, totalCents: l.unitCents * l.quantity },
|
||
})
|
||
}
|
||
}
|
||
|
||
// audit entries for tickets + estimates
|
||
await db.auditLog.create({ data: { actor: 'aylin@nordlicht.digital', action: 'ticket.reply', entity: 'Ticket', entityId: 'seed', after: '{"status":"pending"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'system:llm', action: 'ai.ticket:summarize', entity: 'Ticket', entityId: 'seed', after: '{"model":"glm","length":340}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.send', entity: 'Estimate', entityId: 'seed', after: '{"status":"sent"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'sven@nordlicht.digital', action: 'estimate.accept', entity: 'Estimate', entityId: 'seed', after: '{"status":"accepted","projectId":"created"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
|
||
// --- Accounting: Bank transactions (CAMT.053 style) ---------------------
|
||
const bankTx = [
|
||
{ amountCents: CENTS(5400), bookingDate: daysFromNow(-1), valueDate: daysFromNow(-1), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0039', purpose: 'SEPA-Überweisung RE-2026-0039', matchedStatus: 'suggested', matchReason: 'Referenz matched RE-2026-0039, Betrag exakt' },
|
||
{ amountCents: CENTS(420), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'HanseMöbel Manufaktur GmbH', partnerIban: 'DE12 5001 0517 0000 0000 00', reference: 'RE-2026-0040', purpose: 'Wartungspauschale Oktober', matchedStatus: 'suggested', matchReason: 'Referenz matched, Betrag +/- 0,02€ Toleranz' },
|
||
{ amountCents: -CENTS(89), bookingDate: daysFromNow(-2), valueDate: daysFromNow(-2), partnerName: 'Cloudflare Inc.', partnerIban: null, reference: 'CC-2026-5512', purpose: 'Cloudflare Pro Abonnement', matchedStatus: 'unmatched' },
|
||
{ amountCents: -CENTS(142), bookingDate: daysFromNow(-4), valueDate: daysFromNow(-4), partnerName: 'Deutsche Bahn AG', partnerIban: null, reference: 'DB-2026-8841', purpose: 'Hamburg → Berlin ICE', matchedStatus: 'unmatched' },
|
||
{ amountCents: CENTS(8800), bookingDate: daysFromNow(-12), valueDate: daysFromNow(-11), partnerName: 'TechStart UG', partnerIban: 'DE45 1004 0000 0123 4567 89', reference: 'RE-2026-0036', purpose: 'Rechnung RE-2026-0036 SaaS MVP', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' },
|
||
{ amountCents: -CENTS(44), bookingDate: daysFromNow(-25), valueDate: daysFromNow(-25), partnerName: 'GitHub Inc.', partnerIban: null, reference: 'GH-TEAM-2026', purpose: 'GitHub Team 10 Seats', matchedStatus: 'unmatched' },
|
||
{ amountCents: -CENTS(899), bookingDate: daysFromNow(-8), valueDate: daysFromNow(-8), partnerName: 'JetBrains s.r.o.', partnerIban: null, reference: 'JB-2026-APPPACK', purpose: 'All Products Pack 5 Seats', matchedStatus: 'unmatched' },
|
||
{ amountCents: CENTS(1500), bookingDate: daysFromNow(-30), valueDate: daysFromNow(-29), partnerName: 'Nordwind Logistik GmbH', partnerIban: 'DE89 3704 0044 0532 0130 00', reference: 'RE-2026-0035', purpose: 'Abschlagszahlung Projekt', matchedStatus: 'matched', matchReason: 'Manuell zugeordnet' },
|
||
{ amountCents: -CENTS(180), bookingDate: daysFromNow(-3), valueDate: daysFromNow(-3), partnerName: 'Sectigo', partnerIban: null, reference: 'SC-WILDCARD-2026', purpose: 'SSL Wildcard-Zertifikat', matchedStatus: 'unmatched' },
|
||
{ amountCents: -CENTS(67), bookingDate: daysFromNow(-18), valueDate: daysFromNow(-18), partnerName: 'Otto Office GmbH', partnerIban: null, reference: 'OO-2026-Q4', purpose: 'Büromaterial Q4', matchedStatus: 'unmatched' },
|
||
]
|
||
for (const t of bankTx) {
|
||
await db.bankTransaction.create({ data: t })
|
||
}
|
||
|
||
// --- Accounting: VAT returns (Umsatzsteuer-Voranmeldung) -----------------
|
||
// Past 3 months computed from invoices (mocked with realistic Kennzahlen)
|
||
const vatReturns = [
|
||
{ periodYear: 2026, periodMonth: 7, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(4800), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(912), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(180), outputTaxCents: CENTS(912), inputTaxCents: CENTS(180), payableCents: CENTS(732), submittedAt: daysFromNow(-85), elsterRef: 'ERiC-2026-07-NORDLICHT-0001', acceptedAt: daysFromNow(-83) },
|
||
{ periodYear: 2026, periodMonth: 8, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(6200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1178), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(240), outputTaxCents: CENTS(1178), inputTaxCents: CENTS(240), payableCents: CENTS(938), submittedAt: daysFromNow(-55), elsterRef: 'ERiC-2026-08-NORDLICHT-0002', acceptedAt: daysFromNow(-53) },
|
||
{ periodYear: 2026, periodMonth: 9, periodType: 'monthly', status: 'accepted', k81StandardNetCents: CENTS(5400), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1026), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(310), outputTaxCents: CENTS(1026), inputTaxCents: CENTS(310), payableCents: CENTS(716), submittedAt: daysFromNow(-25), elsterRef: 'ERiC-2026-09-NORDLICHT-0003', acceptedAt: daysFromNow(-23) },
|
||
// current month — computed but not yet submitted
|
||
{ periodYear: 2026, periodMonth: 10, periodType: 'monthly', status: 'computed', k81StandardNetCents: CENTS(7200), k86ReducedNetCents: 0, k66TaxFreeCents: 0, k83TaxStandardCents: CENTS(1368), k65TaxReducedCents: 0, k37InputTaxCents: CENTS(420), outputTaxCents: CENTS(1368), inputTaxCents: CENTS(420), payableCents: CENTS(948) },
|
||
]
|
||
for (const v of vatReturns) {
|
||
await db.vatReturn.create({ data: v })
|
||
}
|
||
|
||
// --- Dunning runs (Mahnwesen) ---------------------------------------------
|
||
// the overdue TechStart invoice RE-2026-0036 got a level 1 dunning
|
||
const overdueInv = await db.invoice.findFirst({ where: { status: 'overdue' } })
|
||
if (overdueInv) {
|
||
await db.dunningRun.create({ data: { invoiceId: overdueInv.id, level: 1, status: 'sent', feesCents: CENTS(5), sentAt: daysFromNow(-7) } })
|
||
}
|
||
|
||
// audit entries for accounting
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.compute', entity: 'VatReturn', entityId: 'seed', after: '{"period":"2026-10","payable":94800}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'vat.prepare:xml', entity: 'VatReturn', entityId: 'seed', after: '{"status":"prepared","xmlBytes":993}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.import:camt053', entity: 'BankTransaction', entityId: 'seed', after: '{"count":10,"batch":"camt-2026-10"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'bank.match:invoice', entity: 'BankTransaction', entityId: 'seed', after: '{"invoiceId":"matched","status":"matched"}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
|
||
// --- Haben-cloned: Belege, Pauschalen, Anlagen/AfA, Kassenbuch -----------
|
||
|
||
// Belege (receipts)
|
||
const belege = [
|
||
{ number: 'BEL-2026-0001', type: 'rechnung-eingang', fileName: 'jetbrains-2026.pdf', fileType: 'application/pdf', fileSizeBytes: 124000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'JetBrains s.r.o.', invoiceNumber: 'JB-2026-APPPACK', amountCents: CENTS(899), bookingDate: daysFromNow(-8), retentionUntil: daysFromNow(365 * 8) },
|
||
{ number: 'BEL-2026-0002', type: 'rechnung-eingang', fileName: 'strato-hosting.pdf', fileType: 'application/pdf', fileSizeBytes: 89000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Strato AG', invoiceNumber: 'STR-2026-Q4', amountCents: CENTS(348), bookingDate: daysFromNow(-10), retentionUntil: daysFromNow(365 * 8) },
|
||
{ number: 'BEL-2026-0003', type: 'quittung', fileName: 'dbahn-hamburg-berlin.jpg', fileType: 'image/jpeg', fileSizeBytes: 45000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Deutsche Bahn AG', amountCents: CENTS(142), bookingDate: daysFromNow(-4), retentionUntil: daysFromNow(365 * 6) },
|
||
{ number: 'BEL-2026-0004', type: 'kassenbeleg', fileName: 'hotel-luebeck.pdf', fileType: 'application/pdf', fileSizeBytes: 67000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Hotel am Markt', amountCents: CENTS(118), bookingDate: daysFromNow(-22), retentionUntil: daysFromNow(365 * 6) },
|
||
{ number: 'BEL-2026-0005', type: 'rechnung-eingang', fileName: 'cloudflare-pro.pdf', fileType: 'application/pdf', fileSizeBytes: 34000, fileHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), vendor: 'Cloudflare Inc.', invoiceNumber: 'CF-2026-5512', amountCents: CENTS(89), bookingDate: daysFromNow(-2), retentionUntil: daysFromNow(365 * 8) },
|
||
]
|
||
for (const b of belege) {
|
||
await db.beleg.create({ data: b })
|
||
}
|
||
|
||
// Pauschalen
|
||
const pauschalen = [
|
||
{ type: 'homeoffice', date: daysFromNow(-1), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' },
|
||
{ type: 'homeoffice', date: daysFromNow(-2), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' },
|
||
{ type: 'homeoffice', date: daysFromNow(-3), amountCents: CENTS(6.92), days: 1, description: 'Homeoffice-Pauschale' },
|
||
{ type: 'kilometer', date: daysFromNow(-4), amountCents: CENTS(42), km: 140, vehicleType: 'auto', tripPurpose: 'geschäftlich', description: 'Hamburg → Berlin (Projekt-Kickoff)' },
|
||
{ type: 'kilometer', date: daysFromNow(-22), amountCents: CENTS(35.40), km: 118, vehicleType: 'auto', tripPurpose: 'geschäftlich', description: 'Lübeck Kunden-Termin' },
|
||
{ type: 'verpflegung', date: daysFromNow(-4), amountCents: CENTS(24), hoursAway: 16, overnight: false, description: 'Reise Berlin (16h abwesend)' },
|
||
{ type: 'verpflegung', date: daysFromNow(-22), amountCents: CENTS(14), hoursAway: 10, overnight: false, description: 'Reise Lübeck (10h abwesend)' },
|
||
]
|
||
for (const p of pauschalen) {
|
||
await db.pauschale.create({ data: p })
|
||
}
|
||
|
||
// Anlagen & AfA
|
||
const anlagen = [
|
||
{ number: 'ANL-2026-0001', name: 'MacBook Pro 16" M4 Max', category: 'hardware', purchaseDate: daysFromNow(-120), purchaseCostCents: CENTS(3499), usefulLifeYears: 3, isGWG: false, isSammelposten: false, annualAfaCents: CENTS(1166.33), accumulatedAfaCents: CENTS(1166.33), bookValueCents: CENTS(2332.67) },
|
||
{ number: 'ANL-2026-0002', name: 'Dell U2723QE Monitor', category: 'hardware', purchaseDate: daysFromNow(-90), purchaseCostCents: CENTS(599), usefulLifeYears: 1, isGWG: true, isSammelposten: false, annualAfaCents: CENTS(599), accumulatedAfaCents: CENTS(599), bookValueCents: 0 },
|
||
{ number: 'ANL-2026-0003', name: 'Bürostuhl Herman Miller', category: 'mobiliar', purchaseDate: daysFromNow(-60), purchaseCostCents: CENTS(1450), usefulLifeYears: 10, isGWG: false, isSammelposten: false, annualAfaCents: CENTS(145), accumulatedAfaCents: CENTS(145), bookValueCents: CENTS(1305) },
|
||
{ number: 'ANL-2026-0004', name: 'Software-Lizenz JetBrains All Products', category: 'software', purchaseDate: daysFromNow(-30), purchaseCostCents: CENTS(899), usefulLifeYears: 1, isGWG: true, isSammelposten: false, annualAfaCents: CENTS(899), accumulatedAfaCents: CENTS(899), bookValueCents: 0 },
|
||
{ number: 'ANL-2026-0005', name: 'Sammelposten Büroausstattung Q3', category: 'mobiliar', purchaseDate: daysFromNow(-45), purchaseCostCents: CENTS(650), usefulLifeYears: 5, isGWG: false, isSammelposten: true, annualAfaCents: CENTS(130), accumulatedAfaCents: CENTS(130), bookValueCents: CENTS(520) },
|
||
]
|
||
for (const a of anlagen) {
|
||
await db.anlage.create({ data: a })
|
||
}
|
||
|
||
// Kassenbuch
|
||
const kassenbuch = [
|
||
{ number: 'KAS-2026-0001', date: daysFromNow(-10), type: 'einnahme', description: 'Barverkauf Schulungsmaterial', amountCents: CENTS(150), taxBp: 1900, taxFree: false, balanceCents: CENTS(150) },
|
||
{ number: 'KAS-2026-0002', date: daysFromNow(-8), type: 'ausgabe', description: 'Büromaterial (Stifte, Notizblöcke)', amountCents: -CENTS(34.50), taxBp: 1900, taxFree: false, balanceCents: CENTS(115.50) },
|
||
{ number: 'KAS-2026-0003', date: daysFromNow(-5), type: 'ausgabe', description: 'Kaffee für Büro', amountCents: -CENTS(12.90), taxBp: 700, taxFree: false, balanceCents: CENTS(102.60) },
|
||
{ number: 'KAS-2026-0004', date: daysFromNow(-3), type: 'einnahme', description: 'Workshop-Teilnahmegebühr (bar)', amountCents: CENTS(250), taxBp: 1900, taxFree: false, balanceCents: CENTS(352.60) },
|
||
{ number: 'KAS-2026-0005', date: daysFromNow(-1), type: 'ausgabe', description: 'Taxi zum Kunden', amountCents: -CENTS(18.40), taxBp: 700, taxFree: false, balanceCents: CENTS(334.20) },
|
||
]
|
||
for (const k of kassenbuch) {
|
||
await db.kassenbuchEntry.create({ data: k })
|
||
}
|
||
|
||
// --- SKR04 Kontenrahmen + Buchungssätze (doppelte Buchführung) -----------
|
||
const konten = [
|
||
// Bestandskonten (Aktiv)
|
||
{ nummer: '0040', name: 'EDV-Anlagen', rahmen: 'skr04', art: 'aktiv' },
|
||
{ nummer: '0140', name: 'Betriebsausstattung', rahmen: 'skr04', art: 'aktiv' },
|
||
{ nummer: '1200', name: 'Bank', rahmen: 'skr04', art: 'aktiv' },
|
||
{ nummer: '1600', name: 'Kassenbestand', rahmen: 'skr04', art: 'aktiv' },
|
||
{ nummer: '1700', name: 'Verbindlichkeiten a. LL', rahmen: 'skr04', art: 'passiv' },
|
||
// Erlöskonten (Ertrag)
|
||
{ nummer: '8400', name: 'Erlöse 19% USt', rahmen: 'skr04', art: 'ertrag' },
|
||
{ nummer: '8600', name: 'Fremdleistungen', rahmen: 'skr04', art: 'ertrag' },
|
||
// Aufwandskonten (Aufwand)
|
||
{ nummer: '4400', name: 'Bezogene Leistungen', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4910', name: 'Bürobedarf', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4920', name: 'Telekommunikation', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4930', name: 'EDV-Kosten / Software', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4940', name: 'Fremdarbeiten', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4950', name: 'Reisekosten', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '4960', name: 'Bewirtung', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '6100', name: 'Löhne/Gehälter', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '6300', name: 'Gesetzl. sozial Aufwand', rahmen: 'skr04', art: 'aufwand' },
|
||
{ nummer: '6520', name: 'Pauschale Steuer', rahmen: 'skr04', art: 'aufwand' },
|
||
// Steuerkonten
|
||
{ nummer: '1773', name: 'USt 19%', rahmen: 'skr04', art: 'passiv' },
|
||
{ nummer: '1776', name: 'USt 7%', rahmen: 'skr04', art: 'passiv' },
|
||
{ nummer: '3804', name: 'Abziehbare Vorsteuer 19%', rahmen: 'skr04', art: 'aktiv' },
|
||
{ nummer: '3806', name: 'Abziehbare Vorsteuer 7%', rahmen: 'skr04', art: 'aktiv' },
|
||
]
|
||
const kontoMap: Record<string, string> = {}
|
||
for (const k of konten) {
|
||
const created = await db.konto.create({ data: k })
|
||
kontoMap[k.nummer] = created.id
|
||
}
|
||
|
||
// Buchungssätze (sample — doppelte Buchführung Soll an Haben)
|
||
const buchungen = [
|
||
{ belegdatum: daysFromNow(-8), sollKontoId: kontoMap['3804'], habenKontoId: kontoMap['1200'], betragCents: CENTS(899), buchungstext: 'JetBrains All Products Pack (Rechnung JB-2026-APPPACK)', belegNummer: 'BEL-2026-0001' },
|
||
{ belegdatum: daysFromNow(-4), sollKontoId: kontoMap['4950'], habenKontoId: kontoMap['1200'], betragCents: CENTS(142), buchungstext: 'Reise Hamburg → Berlin (Deutsche Bahn)', belegNummer: 'BEL-2026-0003' },
|
||
{ belegdatum: daysFromNow(-10), sollKontoId: kontoMap['4930'], habenKontoId: kontoMap['1200'], betragCents: CENTS(348), buchungstext: 'Hosting Strato Q4', belegNummer: 'BEL-2026-0002' },
|
||
{ belegdatum: daysFromNow(-2), sollKontoId: kontoMap['4930'], habenKontoId: kontoMap['1200'], betragCents: CENTS(89), buchungstext: 'Cloudflare Pro Abonnement', belegNummer: 'BEL-2026-0005' },
|
||
{ belegdatum: daysFromNow(-5), sollKontoId: kontoMap['4910'], habenKontoId: kontoMap['1600'], betragCents: CENTS(12.90), buchungstext: 'Kaffee für Büro (Kassenbeleg)', belegNummer: 'KAS-2026-0003' },
|
||
{ belegdatum: daysFromNow(-3), sollKontoId: kontoMap['1600'], habenKontoId: kontoMap['8400'], betragCents: CENTS(250), buchungstext: 'Workshop-Teilnahmegebühr (bar)', steuerBp: 1900 },
|
||
{ belegdatum: daysFromNow(-25), sollKontoId: kontoMap['1200'], habenKontoId: kontoMap['8400'], betragCents: CENTS(1500), buchungstext: 'Rechnung RE-2026-0035 Nordwind Logistik', belegNummer: 'RE-2026-0035' },
|
||
]
|
||
let bucCount = 0
|
||
for (const b of buchungen) {
|
||
bucCount++
|
||
await db.buchungssatz.create({
|
||
data: {
|
||
...b,
|
||
number: `BUC-2026-${String(bucCount).padStart(4, '0')}`,
|
||
buchungsdatum: b.belegdatum,
|
||
},
|
||
})
|
||
}
|
||
|
||
// EÜR entries (Anlage EÜR — AVEÜR line numbers)
|
||
const euerEntries = [
|
||
{ jahr: 2026, zeile: '4', label: 'Betriebseinnahmen (§ 4 Abs. 3 EStG)', art: 'einnahme', betragCents: CENTS(48500), kontoNummern: '8400,8600' },
|
||
{ jahr: 2026, zeile: '12', label: 'Waren/Rohstoffe (Bezogene Leistungen)', art: 'ausgabe', betragCents: CENTS(2860), kontoNummern: '4400' },
|
||
{ jahr: 2026, zeile: '20', label: 'EDV-Kosten / Software', art: 'ausgabe', betragCents: CENTS(1836), kontoNummern: '4930' },
|
||
{ jahr: 2026, zeile: '24', label: 'Reisekosten', art: 'ausgabe', betragCents: CENTS(295), kontoNummern: '4950' },
|
||
{ jahr: 2026, zeile: '25', label: 'Bewirtung', art: 'ausgabe', betragCents: CENTS(0), kontoNummern: '4960' },
|
||
{ jahr: 2026, zeile: '30', label: 'Bürobedarf', art: 'ausgabe', betragCents: CENTS(67), kontoNummern: '4910' },
|
||
{ jahr: 2026, zeile: '40', label: 'Löhne/Gehälter', art: 'ausgabe', betragCents: CENTS(24000), kontoNummern: '6100' },
|
||
{ jahr: 2026, zeile: '41', label: 'Sozialabgaben', art: 'ausgabe', betragCents: CENTS(4800), kontoNummern: '6300' },
|
||
{ jahr: 2026, zeile: '65', label: 'Pauschale Steuer (Homeoffice/KM/Verpflegung)', art: 'ausgabe', betragCents: CENTS(136), kontoNummern: '6520' },
|
||
]
|
||
for (const e of euerEntries) {
|
||
await db.euerEntry.create({ data: e })
|
||
}
|
||
|
||
// Jahreserklärung (USt-Erklärung 2026 — draft)
|
||
await db.jahreserklaerung.create({
|
||
data: {
|
||
jahr: 2026, art: 'ust-erklaerung', status: 'draft',
|
||
kennzahlen: JSON.stringify({ k81: 48500, k86: 0, k66: 0, k83: 9215, k65: 0, k37: 2860, k42: 1836 }),
|
||
},
|
||
})
|
||
await db.jahreserklaerung.create({
|
||
data: {
|
||
jahr: 2025, art: 'ust-erklaerung', status: 'accepted',
|
||
kennzahlen: JSON.stringify({ k81: 52000, k86: 0, k66: 0, k83: 9880, k65: 0, k37: 3100 }),
|
||
elsterRef: 'ERiC-2025-UST-NORDLICHT-0007', submittedAt: daysFromNow(-280), acceptedAt: daysFromNow(-278),
|
||
},
|
||
})
|
||
|
||
// Jahresarchiv 2025 (GoBD — SHA-256 Prüfsumme)
|
||
await db.jahresarchiv.create({
|
||
data: {
|
||
jahr: 2025, archivHash: 'sha256:' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2),
|
||
buchungenCount: 342, belegeCount: 89, rechnungenCount: 47,
|
||
zipFileName: 'aos-jahresarchiv-2025.zip', zipSizeBytes: 45800000,
|
||
createdBy: 'nora@nordlicht.digital',
|
||
notes: 'GoBD-konformer Jahresabschluss 2025. Alle Buchungen festgeschrieben.',
|
||
},
|
||
})
|
||
|
||
// --- Portal approvals (client milestone approvals with evidence) ---------
|
||
const portalApprovals = [
|
||
{ project: 'SEO-Optimierung & BFGG-Check', title: 'SEO-Audit & Keyword-Analyse', description: 'Audit-Bericht mit Keyword-Strategie und Wettbewerbsanalyse.', status: 'approved', decidedAt: daysFromNow(-8), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.12', decision: 'Sehr gute Analyse, bitte umsetzen.' },
|
||
{ project: 'SEO-Optimierung & BFGG-Check', title: 'Schema.org LocalBusiness Markup', description: 'Strukturierte Daten implementiert, Test via Google Rich Results.', status: 'approved', decidedAt: daysFromNow(-5), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.12', decision: 'Freigegeben.' },
|
||
{ project: 'SEO-Optimierung & BFGG-Check', title: 'BFSG Accessibility-Audit', description: 'axe-Audit abgeschlossen, 12 findings (3 kritisch).', status: 'changes-requested', decidedAt: daysFromNow(-3), decidedBy: 'Dr. med. Iris Brandt', decidedIp: '91.64.224.18', decision: 'Bitte Kontrast auf Buttons erhöhen und Skip-Link nach oben setzen.' },
|
||
{ project: 'Corporate Website Relaunch', title: 'Design-Tokens abnehmen', description: 'Farbpalette, Typografie, Spacing-System.', status: 'approved', decidedAt: daysFromNow(-1), decidedBy: 'Klaus Bauer', decidedIp: '85.183.99.44', decision: 'Sieht super aus, weiter so!' },
|
||
{ project: 'Corporate Website Relaunch', title: 'Wireframes Startseite', description: '3 Varianten: klassisch, hero-fokus, story-telling.', status: 'pending' },
|
||
{ project: 'Intranet-Portal Q4', title: 'SSO-Integration abnehmen', description: 'Better Auth SSO mit vorhandenen AD-Accounts.', status: 'pending' },
|
||
{ project: 'Website-Redesign Konzept', title: 'Moodboard freigeben', description: '2 Moodboards: maritim-elegant vs. modern-minimal.', status: 'pending' },
|
||
{ project: 'SaaS MVP — Auth & Billing', title: 'Onboarding-Flow (3 Schritte)', description: 'Signup → Team erstellen → Plan wählen.', status: 'pending' },
|
||
]
|
||
for (const pa of portalApprovals) {
|
||
const { project, ...rest } = pa
|
||
await db.portalApproval.create({ data: { ...rest, projectId: projectMap[project], contentHash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
}
|
||
|
||
// --- Knowledge Base articles (PRD §6.7) ----------------------------------
|
||
const articles = [
|
||
{
|
||
slug: 'launch-checkliste-website',
|
||
title: 'Launch-Checkliste Website-Relaunch',
|
||
category: 'launch-checklist', visibility: 'internal',
|
||
excerpt: 'Alle Schritte vor dem Go-Live: Impressum, Datenschutz, Cookie-Banner, Backups, DNS.',
|
||
content: `# Launch-Checkliste Website-Relaunch
|
||
|
||
## Rechtliches (Pflicht)
|
||
- [ ] Impressum vorhanden und vollständig (§ 5 DDG)
|
||
- [ ] Datenschutzerklärung DSGVO-konform
|
||
- [ ] Cookie-Banner mit CMP-Zertifizierung
|
||
- [ ] SSL-Zertifikat gültig (Let's Encrypt / Sectigo)
|
||
- [ ] Leitweg-ID für öffentliche Kunden
|
||
|
||
## Technisches
|
||
- [ ] Backups konfiguriert (täglich + wöchentlich)
|
||
- [ ] 301-Weiterleitungen von alten URLs
|
||
- [ ] Sitemap.xml generiert und bei Google eingereicht
|
||
- [ ] robots.txt korrekt
|
||
- [ ] PageSpeed > 90 (Mobile)
|
||
- [ ] Core Web Vitals grün (LCP < 2,5s, CLS < 0,1)
|
||
|
||
## Accessibility (BFSG ab 28.06.2025)
|
||
- [ ] Kontraste WCAG 2.1 AA (4,5:1)
|
||
- [ ] Skip-Link vorhanden
|
||
- [ ] ARIA-Rollen korrekt
|
||
- [ ] Formularfelder mit Labels
|
||
- [ ] Tastatur-Navigation möglich
|
||
|
||
## Go-Live
|
||
- [ ] DNS umgestellt (A-Record / CNAME)
|
||
- [ ] E-Mail-Postfächer getestet
|
||
- [ ] Monitoring aktiv (Uptime-Robot)
|
||
- [ ] Übergabe-Dokument an Kunde`,
|
||
tags: 'launch,checkliste,website,dsgvo,bfsg',
|
||
authorName: 'Aylin Demir', version: 3, viewCount: 47,
|
||
},
|
||
{
|
||
slug: 'avv-template-art28-dsgvo',
|
||
title: 'AVV-Vorlage (Art. 28 DSGVO)',
|
||
category: 'templates', visibility: 'internal',
|
||
excerpt: 'Auftragsverarbeitungsvertrag für Hosting-/SaaS-Anbieter. Anwalt geprüft.',
|
||
content: `# Auftragsverarbeitungsvertrag (AVV)
|
||
|
||
Gemäß Art. 28 Abs. 3 DSGVO regelt dieser Vertrag die Auftragsverarbeitung
|
||
personenbezogener Daten zwischen dem Verantwortlichen (Auftraggeber) und
|
||
dem Auftragsverarbeiter (Nordlicht Digital GmbH).
|
||
|
||
## 1. Gegenstand und Dauer
|
||
Der Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag
|
||
des Verantwortlichen gemäß Art. 4 Nr. 2 DSGVO.
|
||
|
||
## 2. Art und Zweck der Verarbeitung
|
||
- Hosting der Website
|
||
- CMS-Betrieb
|
||
- E-Mail-Versand
|
||
|
||
## 3. Art der personenbezogenen Daten
|
||
- Bestandsdaten (Name, Adresse)
|
||
- Kontaktdaten (E-Mail, Telefon)
|
||
|
||
## 4. Technisch-organisatorische Maßnahmen (TOMs)
|
||
Gemäß Art. 32 DSGVO:
|
||
- Verschlüsselung in Transit (TLS 1.3)
|
||
- Verschlüsselung at rest (AES-256-GCM)
|
||
- Zugriffskontrolle (RBAC)
|
||
- Backup täglich
|
||
- Log-Audit 6 Monate
|
||
|
||
## 5. Unterauftragsverarbeiter
|
||
- Hetzner Online GmbH (Hosting)
|
||
- Cloudflare Inc. (CDN)
|
||
- Mailgun Technologies (E-Mail)
|
||
|
||
> Hinweis: Diese Vorlage ist rechtlich geprüft, ersetzt aber im Einzelfall
|
||
> keine anwaltliche Beratung.`,
|
||
tags: 'avv,dsgvo,art28,template,hosting',
|
||
authorName: 'Lena Vogt', version: 5, viewCount: 89,
|
||
},
|
||
{
|
||
slug: 'onboarding-neuer-kunde',
|
||
title: 'Onboarding: Neuer Kunde (Checkliste)',
|
||
category: 'onboarding', visibility: 'internal',
|
||
excerpt: 'Vom Erstkontakt bis zum Kickoff: alle Schritte für ein sauberes Onboarding.',
|
||
content: `# Onboarding-Checkliste Neukunde
|
||
|
||
## Phase 1: Erstkontakt
|
||
- [ ] Firma im CRM angelegt (Rechtsform, USt-IdNr, Register)
|
||
- [ ] Ansprechpartner erfasst
|
||
- [ ] Quelle dokumentiert (Lead-Finder / Referral / Direkt)
|
||
- [ ] DSGVO-Konsens protokolliert (Art. 6 Abs. 1 lit. f)
|
||
|
||
## Phase 2: Angebot
|
||
- [ ] Bedarfsaufnahme durchgeführt
|
||
- [ ] Angebot erstellt (AN-XXXX)
|
||
- [ ] Versand via Portal
|
||
- [ ] Akzeptanz eingeholt (AdES)
|
||
|
||
## Phase 3: Vertrag
|
||
- [ ] Werkvertrag unterzeichnet
|
||
- [ ] AVV (falls Datenverarbeitung) erstellt
|
||
- [ ] NDA (falls vertraulich)
|
||
- [ ] Projekt angelegt
|
||
|
||
## Phase 4: Kickoff
|
||
- [ ] Kickoff-Termin geplant
|
||
- [ ] Stakeholder eingeladen
|
||
- [ ] Projektstruktur im AOS angelegt
|
||
- [ ] Slack/Teams-Channel eingerichtet
|
||
- [ ] Erste Aufgaben angelegt
|
||
|
||
## Phase 5: Durchführung
|
||
- [ ] Wöchentliches Status-Meeting
|
||
- [ ] Fortschritts-Update im Portal
|
||
- [ ] Meilenstein-Freigaben eingeholt`,
|
||
tags: 'onboarding,kunde,prozess,checkliste',
|
||
authorName: 'Sven Hoffmann', version: 2, viewCount: 34,
|
||
},
|
||
{
|
||
slug: 'stundensatz-kalkulation',
|
||
title: 'Stundensatz-Kalkulation für Freelancer',
|
||
category: 'handbuch', visibility: 'internal',
|
||
excerpt: 'Wie berechnet man einen fairer Stundensatz? Mit Vorlage und Rechenbeispiel.',
|
||
content: `# Stundensatz-Kalkulation
|
||
|
||
## Basisformel
|
||
\`\`\`
|
||
Stundensatz = (Jahresziel + Kosten) / abrechenbare Stunden
|
||
\`\`\`
|
||
|
||
## Rechenbeispiel (Freelancer, solo)
|
||
- Zielgehalt: 60.000 €
|
||
- Betriebskosten (Software, Hardware, Büro): 8.000 €
|
||
- Rücklagen (Krankheit, Urlaub, Rente): 12.000 €
|
||
- **Gesamtbedarf: 80.000 €**
|
||
|
||
Abrechenbare Stunden:
|
||
- 220 Arbeitstage × 8 h = 1.760 h
|
||
- minus Urlaub (25): -200 h
|
||
- minus Krankheit (5): -40 h
|
||
- minus Akquise/Admin (20%): -304 h
|
||
- **Abrechenbar: 1.216 h**
|
||
|
||
\`\`\`
|
||
Stundensatz = 80.000 / 1.216 = ~66 €/h (Mindestpreis)
|
||
\`\`\`
|
||
|
||
## Empfohlene Sätze (Agentur Nordlicht)
|
||
| Rolle | Intern | Extern |
|
||
|---|---|---|
|
||
| Junior Dev | 45 € | 75 € |
|
||
| Senior Dev | 65 € | 95 € |
|
||
| Lead/Architect | 80 € | 110 € |
|
||
| Designer | 55 € | 85 € |
|
||
| PM | 60 € | 90 € |`,
|
||
tags: 'stundensatz,kalkulation,freelancer,pricing',
|
||
authorName: 'Max Brandt', version: 4, viewCount: 56,
|
||
},
|
||
{
|
||
slug: 'kundendaten-export-art15',
|
||
title: 'Kundendaten-Export (Art. 15 DSGVO Auskunft)',
|
||
category: 'handbuch', visibility: 'internal',
|
||
excerpt: 'Schritt-für-Schritt: wie man eine DSGVO-Auskunftsbegehren korrekt bearbeitet.',
|
||
content: `# DSGVO-Auskunft nach Art. 15
|
||
|
||
## Frist
|
||
1 Monat ab Eingang der Anfrage (Art. 12 Abs. 3 DSGVO).
|
||
Verlängerung um 2 Monate bei Komplexität (mit Begründung).
|
||
|
||
## Schritte
|
||
1. Identität des Antragstellers verifizieren
|
||
2. Alle Datensätze suchen (CRM, Projekte, Rechnungen, Tickets, Audit-Log)
|
||
3. Export als maschinenlesbares Format (JSON/CSV)
|
||
4. Aufstellung der Verarbeitungszwecke + Rechtsgrundlagen
|
||
5. Empfänger der Daten auflisten
|
||
6. Speicherdauer / Löschfristen angeben
|
||
7. Rechte des Betroffenen (Widerspruch, Berichtigung, Löschung)
|
||
|
||
## AOS-Export
|
||
- \`GET /api/v1/companies/{id}\` liefert die Daten
|
||
- Audit-Log filtern nach entityId
|
||
- Export als ZIP mit SHA-256 Prüfsumme
|
||
|
||
## Antwortvorlage
|
||
> Sehr geehrte/r Frau/Herr [Name],
|
||
>
|
||
> hiermit informieren wir Sie gemäß Art. 15 DSGVO über die zu Ihrer Person
|
||
> bei uns gespeicherten Daten...`,
|
||
tags: 'dsgvo,art15,auskunft,export,prozess',
|
||
authorName: 'Lena Vogt', version: 2, viewCount: 23,
|
||
},
|
||
{
|
||
slug: 'faq-cms-bedienung',
|
||
title: 'FAQ: CMS-Bedienung für Redakteure',
|
||
category: 'faq', visibility: 'client',
|
||
excerpt: 'Häufige Fragen unserer Kunden zur Bedienung des AOS-CMS.',
|
||
content: `# FAQ: CMS-Bedienung
|
||
|
||
## Wie logge ich mich ein?
|
||
1. Portal-URL öffnen: https://aos.nordlicht.digital
|
||
2. E-Mail + Passwort eingeben
|
||
3. Passkey-Login verfügbar (empfohlen)
|
||
|
||
## Wie erstelle ich einen Blog-Artikel?
|
||
1. Im Menü auf „Inhalte" → „Neuer Artikel"
|
||
2. Titel + Auszug eingeben
|
||
3. Inhalt im Markdown-Editor schreiben
|
||
4. Bild hochladen (Drag & Drop)
|
||
5. „Veröffentlichen" klicken
|
||
|
||
## Wie ändere ich meine Kontaktdaten?
|
||
Im Profil → „Stammdaten bearbeiten". Änderungen werden protokolliert.
|
||
|
||
## Was passiert bei einem Fehler?
|
||
1. Screenshot machen
|
||
2. Ticket erstellen (Support → „Neues Ticket")
|
||
4. SLA: Antwort innerhalb 8 Stunden (werktags)`,
|
||
tags: 'faq,cms,redakteur,kunde,anleitung',
|
||
authorName: 'Aylin Demir', version: 7, viewCount: 142,
|
||
},
|
||
{
|
||
slug: 'wordpress-update-anleitung',
|
||
title: 'WordPress: Sicheres Update (Wartungsvertrag)',
|
||
category: 'handbuch', visibility: 'client',
|
||
excerpt: 'Schritt-für-Schritt Anleitung für sichere WordPress-Updates.',
|
||
content: `# WordPress-Update Anleitung
|
||
|
||
## Vor dem Update
|
||
1. **Backup erstellen** (UpdraftPlus oder manuell)
|
||
2. Wartungs-Modus aktivieren
|
||
3. Plugins deaktivieren (optional)
|
||
|
||
## Update-Reihenfolge
|
||
1. WordPress-Core aktualisieren
|
||
2. Themes aktualisieren
|
||
3. Plugins aktualisieren (einzeln, nach jedem Test)
|
||
|
||
## Nach dem Update
|
||
1. Wartungs-Modus deaktivieren
|
||
2. Seite testen (Startseite, Formulare, Checkout)
|
||
3. PageSpeed prüfen
|
||
4. Im AOS Ticket-System Update protokollieren
|
||
|
||
## Rollback
|
||
Bei Fehlern: Backup wiederherstellen (UpdraftPlus → Restore).
|
||
Plugin-Versionen notieren für Support-Ticket.`,
|
||
tags: 'wordpress,update,wartung,anleitung',
|
||
authorName: 'Tom Becker', version: 3, viewCount: 67,
|
||
},
|
||
{
|
||
slug: 'rechnungsstellung-prozess',
|
||
title: 'Rechnungsstellung: Der komplette Prozess',
|
||
category: 'handbuch', visibility: 'internal',
|
||
excerpt: 'Von der Zeiterfassung bis zur bezahlten Rechnung — 0 manuelle Schritte.',
|
||
content: `# Rechnungsstellungs-Prozess
|
||
|
||
## 1. Zeiterfassung
|
||
Entwickler tragen Zeit im AOS ein (Timer oder manuell).
|
||
Billable-Flag setzen, Projekt zuordnen.
|
||
|
||
## 2. Freigabe
|
||
PM prüft + gibt die Woche frei.
|
||
Zeit wird gesperrt (lock nach Freigabe).
|
||
|
||
## 3. Rechnungserstellung
|
||
- Aus freigegebenen Zeit-Einträgen
|
||
- Automatische Rechnungspositionen
|
||
- Gapless Number: RE-2026-XXXX
|
||
|
||
## 4. E-Rechnung
|
||
- ZUGFeRD-XML generiert (EN 16931)
|
||
- PDF mit eingebettetem XML (Typst)
|
||
- XRechnung für öffentliche Kunden
|
||
|
||
## 5. Versand
|
||
- E-Mail mit PDF-Anhang
|
||
- Portal-Benachrichtigung an Kunde
|
||
|
||
## 6. Zahlungseingang
|
||
- Bank-Import (CAMT.053)
|
||
- Auto-Matching (Referenz + Betrag)
|
||
- Rechnung → „bezahlt"
|
||
|
||
## 7. Mahnwesen (falls überfällig)
|
||
- Stufe 1: Zahlungserinnerung (kostenlos)
|
||
- Stufe 2: Mahnung (2,50 € Gebühr)
|
||
- Stufe 3: Letzte Mahnung + Verzugszins (§ 288 BGB)
|
||
|
||
## 8. DATEV-Export
|
||
Monatlich: Buchungsstapel an Steuerberater.`,
|
||
tags: 'rechnung,prozess,finance,datev,e-rechnung',
|
||
authorName: 'Nora Klein', version: 6, viewCount: 78,
|
||
},
|
||
]
|
||
for (const a of articles) {
|
||
await db.article.create({ data: a })
|
||
}
|
||
|
||
// --- API Tokens + Webhooks (real crypto, hashed at rest) -----------------
|
||
const crypto = await import('crypto')
|
||
|
||
// Pre-built admin token for Claude Code (deterministic, so .mcp.json can reference it)
|
||
// Token: aos_admin_seed_token_for_claude_code_2026
|
||
const adminToken = 'aos_admin_seed_token_for_claude_code_2026'
|
||
const adminScopes = ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'crm.deal:write', 'finance.invoice:read', 'finance.invoice:write', 'finance:write', 'leads:read', 'leads:write', 'support:read', 'support:write', 'projects:write', 'licenses.license:read']
|
||
await db.apiToken.create({
|
||
data: {
|
||
name: 'Claude Code Admin (seeded)',
|
||
tokenHash: crypto.createHash('sha256').update(adminToken).digest('hex'),
|
||
tokenPrefix: 'aos_admin',
|
||
scopes: JSON.stringify(adminScopes),
|
||
lastUsedAt: daysFromNow(-1),
|
||
},
|
||
})
|
||
|
||
const apiTokens = [
|
||
{ name: 'ZeroClaw Agent (MCP)', scopes: ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'finance.invoice:read', 'leads:read', 'leads:write', 'support:read', 'support:write'] },
|
||
{ name: 'DATEV Export-Skript', scopes: ['finance.invoice:read'] },
|
||
{ name: 'WordPress Update-Server', scopes: ['licenses.license:read'] },
|
||
]
|
||
for (const t of apiTokens) {
|
||
const plaintext = 'aos_' + crypto.randomBytes(32).toString('hex')
|
||
const tokenHash = crypto.createHash('sha256').update(plaintext).digest('hex')
|
||
const tokenPrefix = 'aos_' + plaintext.slice(4, 11)
|
||
await db.apiToken.create({
|
||
data: { name: t.name, tokenHash, tokenPrefix, scopes: JSON.stringify(t.scopes), lastUsedAt: daysFromNow(-2) },
|
||
})
|
||
}
|
||
// one revoked token
|
||
const revokedPlain = 'aos_' + crypto.randomBytes(32).toString('hex')
|
||
await db.apiToken.create({
|
||
data: {
|
||
name: 'Altes Backup-Skript (widerrufen)',
|
||
tokenHash: crypto.createHash('sha256').update(revokedPlain).digest('hex'),
|
||
tokenPrefix: 'aos_' + revokedPlain.slice(4, 11),
|
||
scopes: JSON.stringify(['finance.invoice:read']),
|
||
revokedAt: daysFromNow(-30),
|
||
lastUsedAt: daysFromNow(-32),
|
||
},
|
||
})
|
||
|
||
// Webhooks with real HMAC secrets
|
||
const webhooks = [
|
||
{ url: 'https://make.nordlicht.digital/webhooks/aos', events: ['invoice.paid', 'contract.signed', 'lead.approved'], sentCount: 47, failCount: 2, lastStatus: 200, lastSentAt: daysFromNow(-0.2) },
|
||
{ url: 'https://hooks.slack.com/services/T0/B0/xxxx', events: ['ticket.created', 'invoice.overdue'], sentCount: 12, failCount: 0, lastStatus: 200, lastSentAt: daysFromNow(-1) },
|
||
{ url: 'https://erp.hansemoebel.de/api/aos-events', events: ['license.activated'], sentCount: 5, failCount: 1, lastStatus: 500, lastSentAt: daysFromNow(-3) },
|
||
]
|
||
for (const w of webhooks) {
|
||
await db.webhook.create({
|
||
data: { ...w, events: JSON.stringify(w.events), secret: 'whsec_' + crypto.randomBytes(24).toString('hex') },
|
||
})
|
||
}
|
||
|
||
await db.auditLog.create({ data: { actor: 'max@nordlicht.digital', action: 'api.token:create', entity: 'ApiToken', entityId: 'seed', after: '{"name":"ZeroClaw Agent","scopes":["crm.company:read"]}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
await db.auditLog.create({ data: { actor: 'max@nordlicht.digital', action: 'webhook.create', entity: 'Webhook', entityId: 'seed', after: '{"url":"make.nordlicht.digital","events":["invoice.paid"]}', hash: 'sha256:' + Math.random().toString(36).slice(2) } })
|
||
|
||
console.log('Seed complete.')
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e)
|
||
process.exit(1)
|
||
})
|
||
.finally(async () => {
|
||
await db.$disconnect()
|
||
})
|
||
|
||
// --- Custom Fields (PRD §5.5: JSONB with schema per entity) ---------------
|
||
const customFields = [
|
||
{ entityType: 'company', key: 'branche', label: 'Branche', type: 'select', options: JSON.stringify(['Handwerk', 'Gastronomie', 'Einzelhandel', 'Dienstleistung', 'Industrie', 'Gesundheit', 'Technologie']), required: false, sortOrder: 1 },
|
||
{ entityType: 'company', key: 'mitarbeiterzahl', label: 'Mitarbeiterzahl', type: 'select', options: JSON.stringify(['1-10', '11-50', '51-200', '201-500', '500+']), required: false, sortOrder: 2 },
|
||
{ entityType: 'company', key: 'umsatzklasse', label: 'Umsatzklasse', type: 'select', options: JSON.stringify(['< 100k', '100k-500k', '500k-2M', '2M-10M', '10M+']), required: false, sortOrder: 3 },
|
||
{ entityType: 'person', key: 'entscheider', label: 'Entscheider?', type: 'boolean', required: false, sortOrder: 1 },
|
||
{ entityType: 'person', key: 'kontaktweg', label: 'Bevorzugter Kontaktweg', type: 'select', options: JSON.stringify(['E-Mail', 'Telefon', 'Teams', 'Persönlich']), required: false, sortOrder: 2 },
|
||
{ entityType: 'deal', key: 'wettbewerber', label: 'Wettbewerber', type: 'string', required: false, sortOrder: 1 },
|
||
{ entityType: 'deal', key: 'entscheidungsgrund', label: 'Entscheidungsgrund', type: 'select', options: JSON.stringify(['Preis', 'Qualität', 'Geschwindigkeit', 'Referenzen', 'Technologie']), required: false, sortOrder: 2 },
|
||
{ entityType: 'project', key: 'prioritaet', label: 'Priorität (kundenseitig)', type: 'select', options: JSON.stringify(['niedrig', 'normal', 'hoch', 'kritisch']), required: false, sortOrder: 1 },
|
||
{ entityType: 'invoice', key: 'kostenstelle', label: 'Kostenstelle', type: 'string', required: false, sortOrder: 1 },
|
||
{ entityType: 'ticket', key: 'auswirkung', label: 'Geschäftsauswirkung', type: 'select', options: JSON.stringify(['keine', 'niedrig', 'mittel', 'hoch', 'kritisch']), required: false, sortOrder: 1 },
|
||
]
|
||
for (const cf of customFields) {
|
||
await db.customFieldDef.create({ data: cf })
|
||
}
|