Files
AOS/tool-results/read_1793199730058_7543b5fad96f.txt
T

895 lines
42 KiB
Plaintext

1→// AOS — Agency Operating System
2→// One deployment = one agency (single tenant). SQLite for this build.
3→// Money is integer cents, tax rates are basis points. No floats for money.
4→
5→generator client {
6→ provider = "prisma-client-js"
7→}
8→
9→datasource db {
10→ provider = "sqlite"
11→ url = env("DATABASE_URL")
12→}
13→
14→// ---------------------------------------------------------------------------
15→// Platform
16→// ---------------------------------------------------------------------------
17→
18→model User {
19→ id String @id @default(cuid())
20→ email String @unique
21→ name String
22→ role String @default("owner") // owner|admin|sales|pm|developer|finance|readonly|client
23→ createdAt DateTime @default(now())
24→ updatedAt DateTime @updatedAt
25→}
26→
27→// Module registry — installed/enabled/disabled state of each domain module
28→model Module {
29→ id String @id // crm|leads|projects|licenses|sales-docs|finance|support|...
30→ version String @default("1.0.0")
31→ enabled Boolean @default(true)
32→ installed Boolean @default(true)
33→ nav String? // JSON: [{label,route,permission}]
34→ events String? // JSON: {emits:[],listens:[]}
35→ updatedAt DateTime @updatedAt
36→ createdAt DateTime @default(now())
37→}
38→
39→model Setting {
40→ key String @id
41→ value String // typed at app layer (encrypted secrets handled separately)
42→ updatedAt DateTime @updatedAt
43→}
44→
45→model AuditLog {
46→ id String @id @default(cuid())
47→ actor String? // user id or "system"
48→ action String // e.g. "crm.deal:update"
49→ entity String
50→ entityId String
51→ before String? // JSON
52→ after String? // JSON
53→ hash String? // hash chain
54→ createdAt DateTime @default(now())
55→}
56→
57→// ---------------------------------------------------------------------------
58→// CRM (Phase 1)
59→// ---------------------------------------------------------------------------
60→
61→model Company {
62→ id String @id @default(cuid())
63→ name String
64→ legalForm String? // GmbH, UG, e.K., GbR, AG, Einzelunternehmen...
65→ vatId String? // USt-IdNr
66→ registerCourt String?
67→ registerNumber String? // HRB ...
68→ website String?
69→ email String?
70→ phone String?
71→ city String?
72→ zip String?
73→ street String?
74→ source String? // lead-finder|import|manual|referral|agency|self-signup
75→ tags String? // comma separated
76→
77→ // --- B2B Agency / Reseller (PRD §6.5) ---
78→ // A company may be a regular client OR an agency (reseller). Agencies
79→ // onboard their own clients and license them; they cannot create invoices
80→ // (they must use their own accounting system, e.g. lexoffice/sevdesk via REST API).
81→ isAgency Boolean @default(false)
82→ // Agency approval: pending (just signed up) → approved (admin allowed) → rejected.
83→ agencyStatus String? // null when not an agency; "pending"|"approved"|"rejected"
84→ agencyAppliedAt DateTime?
85→ agencyApprovedAt DateTime?
86→ agencyRejectedAt DateTime?
87→ agencyRejectReason String?
88→ // Admin-set discount percentage applied to all licenses this agency buys
89→ // from us. Range 0-20 (admin decision only). Default 0.
90→ agencyDiscountPct Int @default(0)
91→ // Self-serve clients belong to an agency. NULL = direct client of ours.
92→ parentAgencyId String? // FK to Company(id) — the agency that owns this client
93→ parentAgency Company? @relation("AgencyClients", fields: [parentAgencyId], references: [id])
94→ agencyClients Company[] @relation("AgencyClients")
95→
96→ createdAt DateTime @default(now())
97→ updatedAt DateTime @updatedAt
98→ persons Person[]
99→ deals Deal[]
100→ leads Lead[]
101→ licenses License[]
102→ invoices Invoice[]
103→ projects Project[]
104→ contracts Contract[]
105→ expenses Expense[]
106→ tickets Ticket[]
107→ agencyApplication AgencyApplication?
108→}
109→
110→model Person {
111→ id String @id @default(cuid())
112→ companyId String?
113→ company Company? @relation(fields: [companyId], references: [id])
114→ firstName String
115→ lastName String
116→ email String?
117→ phone String?
118→ role String? // Geschäftsführer, Marketing, IT...
119→ createdAt DateTime @default(now())
120→ updatedAt DateTime @updatedAt
121→}
122→
123→model Deal {
124→ id String @id @default(cuid())
125→ companyId String
126→ company Company @relation(fields: [companyId], references: [id])
127→ title String
128→ valueCents Int @default(0)
129→ probability Int @default(0) // 0-100
130→ stage String @default("lead") // pipeline stage key
131→ expectedClose DateTime?
132→ lossReason String?
133→ ownerId String?
134→ createdAt DateTime @default(now())
135→ updatedAt DateTime @updatedAt
136→}
137→
138→// ---------------------------------------------------------------------------
139→// Leads Finder (Phase 2)
140→// ---------------------------------------------------------------------------
141→
142→model Lead {
143→ id String @id @default(cuid())
144→ // discovered company info (may become a Company when approved)
145→ companyName String
146→ website String?
147→ city String?
148→ branch String?
149→ // audit + scoring
150→ score Int @default(0) // 0-100 weighted rule score
151→ scoreLabel String? // new-site|redesign|seo|aeo|shop|maintenance|accessibility
152→ evidence String? // JSON array of {id,check,status,detail}
153→ explanation String? // LLM draft "why this lead" — every claim links to an evidence id
154→ suggestedAngle String? // proposed outreach angle
155→ status String @default("pending") // pending|approved|rejected|snoozed|converted
156→ reviewReason String?
157→ source String? // serpapi|tavily|apify|firecrawl
158→ radiusKm Int?
159→ // when converted, link to CRM company + deal
160→ companyId String?
161→ company Company? @relation(fields: [companyId], references: [id])
162→ createdAt DateTime @default(now())
163→ reviewedAt DateTime?
164→ updatedAt DateTime @updatedAt
165→}
166→
167→// ---------------------------------------------------------------------------
168→// Projects (Phase 3)
169→// ---------------------------------------------------------------------------
170→
171→model Project {
172→ id String @id @default(cuid())
173→ companyId String
174→ company Company @relation(fields: [companyId], references: [id])
175→ name String
176→ type String // website|shop|app|retainer
177→ status String @default("planning") // planning|active|review|done|on-hold|cancelled
178→ budgetType String @default("fixed") // fixed|time-materials|retainer
179→ budgetHours Int?
180→ spentHours Int @default(0)
181→ fixedPriceCents Int?
182→ startDate DateTime?
183→ dueDate DateTime?
184→ progress Int @default(0) // 0-100
185→ createdAt DateTime @default(now())
186→ updatedAt DateTime @updatedAt
187→ tasks Task[]
188→ timeEntries TimeEntry[]
189→ approvals PortalApproval[]
190→}
191→
192→model Task {
193→ id String @id @default(cuid())
194→ projectId String
195→ project Project @relation(fields: [projectId], references: [id])
196→ title String
197→ description String?
198→ status String @default("todo") // todo|in-progress|review|done
199→ assignee String?
200→ estimateMin Int?
201→ priority String @default("medium") // low|medium|high
202→ createdAt DateTime @default(now())
203→ updatedAt DateTime @updatedAt
204→ comments TaskComment[]
205→}
206→
207→model TaskComment {
208→ id String @id @default(cuid())
209→ taskId String
210→ task Task @relation(fields: [taskId], references: [id])
211→ author String
212→ body String
213→ createdAt DateTime @default(now())
214→}
215→
216→model TimeEntry {
217→ id String @id @default(cuid())
218→ projectId String
219→ project Project @relation(fields: [projectId], references: [id])
220→ taskId String? // optional link to a specific task
221→ userId String?
222→ description String
223→ minutes Int
224→ billable Boolean @default(true)
225→ rateCentsPerHour Int?
226→ invoiced Boolean @default(false)
227→ locked Boolean @default(false) // locked after PM approval
228→ date DateTime @default(now())
229→ createdAt DateTime @default(now())
230→}
231→
232→// ---------------------------------------------------------------------------
233→// Licenses (Phase 4)
234→// ---------------------------------------------------------------------------
235→
236→model License {
237→ id String @id @default(cuid())
238→ key String @unique
239→ companyId String
240→ company Company @relation(fields: [companyId], references: [id])
241→ product String // e.g. aos-cms, restov1, wp-plugin-xyz
242→ plan String // starter|pro|enterprise
243→ status String @default("active") // active|suspended|revoked|expired|cancelled
244→ // environment: demo licenses are feature-limited / watermarked / time-boxed
245→ // and can be promoted to production via the license server.
246→ environment String @default("production") // demo|production
247→ // Modular licensing: a license may be the "core" license, or a module add-on.
248→ // Module licenses reference their parent core license via parentId.
249→ parentId String? // null = core license; non-null = module add-on
250→ parent License? @relation("LicenseModules", fields: [parentId], references: [id])
251→ modules License[] @relation("LicenseModules")
252→ // The module key this license grants (only when parentId is set).
253→ // e.g. "online-order", "payment", "reservation", "delivery", "table-service"
254→ module String? // null for core licenses
255→ maxActivations Int @default(1)
256→ validUntil DateTime?
257→ token String? // Ed25519-signed token (mocked)
258→ // Suspend/bloke reason + timestamp — set when status -> suspended.
259→ suspendReason String?
260→ suspendedAt DateTime?
261→
262→ // --- B2B Subscription & Billing (German law) ---
263→ // Kündigung frist: 3 months (90 days) notice period before the next renewal.
264→ // A client can cancel, but the cancellation only takes effect at the end of
265→ // the notice period. Set on issuance, defaults to 90 days.
266→ noticePeriodDays Int @default(90)
267→ // Trial: first 14 days are a free trial. Subscription can be cancelled within
268→ // these 14 days at any time with immediate effect (no charge). Day 15 the
269→ // paid subscription starts.
270→ trialEndsAt DateTime? // null = no trial (e.g. agency-issued)
271→ subscriptionStartsAt DateTime? // when the paid subscription begins (trial + 1 day)
272→ // Billing cycle: monthly or yearly. Yearly gets a discount on the total.
273→ billingCycle String @default("monthly") // monthly|yearly
274→ // The price in cents per billing cycle BEFORE any discount.
275→ // For monthly: this is the monthly price (e.g. 5000 = 50 €).
276→ // For yearly: this is the yearly total (e.g. 60000 = 600 € before yearly discount).
277→ priceCents Int @default(0)
278→ // Discount percentage applied to this license (0-100).
279→ // For agencies: copied from Company.agencyDiscountPct at issue time.
280→ // For yearly billing: a yearly discount (e.g. 10%) is applied on top.
281→ discountPct Int @default(0)
282→ // The effective amount actually charged (priceCents - all discounts).
283→ // Computed at issue time and stored for audit/GoBD.
284→ netPriceCents Int @default(0)
285→ // Cancellation tracking (Kündigung).
286→ cancelledAt DateTime? // when the client cancelled
287→ cancellationEffectiveAt DateTime? // when the cancellation takes effect (notice period end)
288→ cancellationReason String?
289→ // Stripe integration (optional — populated when paid via Stripe).
290→ stripeCustomerId String?
291→ stripeSubscriptionId String?
292→ stripePriceId String?
293→
294→ createdAt DateTime @default(now())
295→ activations LicenseActivation[]
296→}
297→
298→model LicenseActivation {
299→ id String @id @default(cuid())
300→ licenseId String
301→ license License @relation(fields: [licenseId], references: [id])
302→ domain String
303→ fingerprint String?
304→ environment String @default("production") // production|staging
305→ version String?
306→ lastCheck DateTime @default(now())
307→ createdAt DateTime @default(now())
308→}
309→
310→// Asset & renewal register (what the agency manages/pays for on behalf of clients)
311→model Asset {
312→ id String @id @default(cuid())
313→ name String
314→ type String // domain|hosting|ssl|plugin-seat|saas
315→ provider String? // Ionos, Strato, Cloudflare...
316→ companyId String? // may be internal asset
317→ costCentsPerMonth Int?
318→ renewalDate DateTime
319→ rebill Boolean @default(false)
320→ markupPct Int @default(0)
321→ status String @default("active") // active|expiring|expired|cancelled
322→ createdAt DateTime @default(now())
323→ updatedAt DateTime @updatedAt
324→}
325→
326→// ---------------------------------------------------------------------------
327→// Finance (Phase 5)
328→// ---------------------------------------------------------------------------
329→
330→model Invoice {
331→ id String @id @default(cuid())
332→ number String @unique // gapless, e.g. RE-2026-0001
333→ companyId String
334→ company Company @relation(fields: [companyId], references: [id])
335→ status String @default("draft") // draft|sent|paid|overdue|cancelled|storno
336→ type String @default("final") // final|partial|recurring|credit
337→ issueDate DateTime @default(now())
338→ dueDate DateTime
339→ paidDate DateTime?
340→ netCents Int @default(0)
341→ taxBp Int @default(1900) // 19% as basis points
342→ grossCents Int @default(0)
343→ // E-Rechnung / compliance
344→ zugferd Boolean @default(false)
345→ xrechnung Boolean @default(false)
346→ datevExported Boolean @default(false)
347→ projectId String?
348→ createdAt DateTime @default(now())
349→ updatedAt DateTime @updatedAt
350→ lines InvoiceLine[]
351→ dunningRuns DunningRun[]
352→}
353→
354→model InvoiceLine {
355→ id String @id @default(cuid())
356→ invoiceId String
357→ invoice Invoice @relation(fields: [invoiceId], references: [id])
358→ description String
359→ quantity Int @default(1)
360→ unitCents Int
361→ totalCents Int
362→}
363→
364→// ---------------------------------------------------------------------------
365→// Contracts / Verträge (Phase 5 — adapted from PerfEx Contracts, Germanized)
366→// German contract types: Werkvertrag, Dienstvertrag, Wartungsvertrag/SLA,
367→// AVV (Auftragsverarbeitungsvertrag), NDA, AVV
368→// ---------------------------------------------------------------------------
369→
370→model Contract {
371→ id String @id @default(cuid())
372→ number String @unique // gapless, e.g. VER-2026-0001
373→ companyId String
374→ company Company @relation(fields: [companyId], references: [id])
375→ type String // werkvertrag|dienstvertrag|wartung-sla|avv|nda|dpa
376→ title String
377→ status String @default("draft") // draft|sent|signed|active|expired|terminated
378→ valueCents Int @default(0)
379→ startDate DateTime?
380→ endDate DateTime? // null = unbefristet
381→ templateId String? // reference to a stored template
382→ dealId String? // optional link to originating deal
383→ projectId String? // optional link to project
384→ // E-signature hook (mock)
385→ signedAt DateTime?
386→ signedBy String? // signer name/email
387→ signedIp String?
388→ signToken String? // unique token for the signing link
389→ notes String?
390→ createdAt DateTime @default(now())
391→ updatedAt DateTime @updatedAt
392→}
393→
394→// ---------------------------------------------------------------------------
395→// Expenses / Ausgaben (adapted from PerfEx Expenses, Germanized for DATEV)
396→// ---------------------------------------------------------------------------
397→
398→model Expense {
399→ id String @id @default(cuid())
400→ number String @unique // gapless, e.g. AUS-2026-0001
401→ categoryId String // reise|software|hardware|fremdleistung|buero|marketing
402→ vendor String? // supplier / merchant
403→ description String
404→ amountCents Int // net amount
405→ taxBp Int @default(1900)
406→ grossCents Int @default(0)
407→ date DateTime @default(now())
408→ // linking
409→ companyId String? // customer this is billable to
410→ company Company? @relation(fields: [companyId], references: [id])
411→ projectId String? // project this relates to
412→ billable Boolean @default(false) // convert to customer invoice
413→ markupPct Int @default(0) // markup when re-billed
414→ // recurring
415→ recurring Boolean @default(false)
416→ recurringInterval String? // monthly|quarterly|yearly
417→ // receipt + export
418→ receiptHash String? // SHA-256 of stored receipt file (content-addressed)
419→ datevExported Boolean @default(false)
420→ // conversion
421→ convertedInvoiceId String? // link to the invoice created from this expense
422→ status String @default("recorded") // recorded|approved|invoiced|rejected
423→ createdAt DateTime @default(now())
424→ updatedAt DateTime @updatedAt
425→}
426→
427→// ---------------------------------------------------------------------------
428→// Support Tickets (PRD §6.7 + PerfEx Support Tickets + AI Integration)
429→// ---------------------------------------------------------------------------
430→
431→model Ticket {
432→ id String @id @default(cuid())
433→ number String @unique // gapless, e.g. TKT-2026-0001
434→ subject String
435→ status String @default("open") // open|pending|resolved|closed
436→ priority String @default("normal") // low|normal|high|urgent
437→ categoryId String // bug|anfrage|aenderung|konto|rechnung|hosting
438→ companyId String
439→ company Company @relation(fields: [companyId], references: [id])
440→ projectId String?
441→ licenseId String?
442→ // --- Ticket routing (agency model) ---
443→ // If the client belongs to an agency, the ticket is routed to that agency
444→ // (agencyCompanyId). The agency handles it; only if the agency escalates
445→ // does the ticket reach us (routedToAgency = false, escalatedToAdmin = true).
446→ agencyCompanyId String? // the reseller that owns this client (if any)
447→ routedToAgency Boolean @default(false) // ticket sent to agency inbox
448→ escalatedToAdmin Boolean @default(false) // agency escalated to us
449→ escalatedAt DateTime?
450→ // SLA
451→ slaResponseHours Int @default(8) // first response target
452→ slaResolveHours Int @default(48) // resolution target
453→ slaRespondedAt DateTime?
454→ slaResolvedAt DateTime?
455→ // AI
456→ aiSummary String? // LLM-generated summary of the conversation
457→ // assignee
458→ assignee String?
459→ createdAt DateTime @default(now())
460→ updatedAt DateTime @updatedAt
461→ messages TicketMessage[]
462→}
463→
464→model TicketMessage {
465→ id String @id @default(cuid())
466→ ticketId String
467→ ticket Ticket @relation(fields: [ticketId], references: [id])
468→ fromType String // customer|staff|system|agency
469→ fromName String
470→ body String
471→ createdAt DateTime @default(now())
472→}
473→
474→// ---------------------------------------------------------------------------
475→// Agency Applications (PRD §6.5 — reseller onboarding)
476→// ---------------------------------------------------------------------------
477→// When a company signs up, they may choose to become an agency (reseller).
478→// The application is stored here and awaits admin approval. On approval,
479→// the Company.isAgency flag is set, Company.agencyStatus = "approved", and
480→// an email is sent to the applicant with their discount percentage.
481→model AgencyApplication {
482→ id String @id @default(cuid())
483→ companyId String @unique
484→ company Company @relation(fields: [companyId], references: [id])
485→ status String @default("pending") // pending|approved|rejected
486→ // Discount the admin grants this agency (0-20%). Applied to all licenses
487→ // the agency buys from us. Admin-only decision.
488→ discountPct Int @default(0) // 0-20
489→ // Business justification fields (filled by the applicant)
490→ businessModel String? // e.g. "Webdesign agency", "IT consultant"
491→ expectedClientVolume String? // e.g. "10-20 clients"
492→ referredBy String? // how did they hear about us
493→ // Review
494→ reviewedBy String?
495→ reviewedAt DateTime?
496→ rejectReason String?
497→ appliedAt DateTime @default(now())
498→ createdAt DateTime @default(now())
499→ updatedAt DateTime @updatedAt
500→}
501→
502→// ---------------------------------------------------------------------------
503→// Estimates / Angebote (PRD §6.5 sales-docs — quotes that convert to invoice)
504→// ---------------------------------------------------------------------------
505→
506→model Estimate {
507→ id String @id @default(cuid())
508→ number String @unique // gapless, e.g. AN-2026-0001
509→ companyId String
510→ dealId String? // originating deal
511→ projectId String? // creates project when accepted
512→ subject String
513→ status String @default("draft") // draft|sent|accepted|rejected|expired
514→ validUntil DateTime?
515→ issueDate DateTime @default(now())
516→ acceptedAt DateTime?
517→ acceptedBy String?
518→ notes String?
519→ netCents Int @default(0)
520→ taxBp Int @default(1900)
521→ grossCents Int @default(0)
522→ // AI
523→ aiDraftFromBrief String? // flag: was this drafted from a brief via AI
524→ createdAt DateTime @default(now())
525→ updatedAt DateTime @updatedAt
526→ lines EstimateLine[]
527→}
528→
529→model EstimateLine {
530→ id String @id @default(cuid())
531→ estimateId String
532→ estimate Estimate @relation(fields: [estimateId], references: [id])
533→ description String
534→ quantity Int @default(1)
535→ unitCents Int
536→ totalCents Int
537→}
538→
539→// ---------------------------------------------------------------------------
540→// Accounting / Buchhaltung (adapted from Haben, German-law compliant)
541→// VAT return (Umsatzsteuer-Voranmeldung), EÜR, bank reconciliation, dunning.
542→// PRD §6.6: double-entry + ELSTER out of scope; this covers the agency-facing
543→// compliance layer (compute, validate, mock-submit) + bank matching.
544→// ---------------------------------------------------------------------------
545→
546→// Bank transactions imported from CAMT.053 or PSD2 (FinTS).
547→model BankTransaction {
548→ id String @id @default(cuid())
549→ amountCents Int // signed: + credit (incoming), - debit (outgoing)
550→ bookingDate DateTime
551→ valueDate DateTime
552→ partnerName String?
553→ partnerIban String?
554→ reference String?
555→ purpose String?
556→ matchedInvoiceId String?
557→ matchedStatus String @default("unmatched")
558→ matchReason String?
559→ importBatchId String?
560→ importedAt DateTime @default(now())
561→ createdAt DateTime @default(now())
562→}
563→
564→// VAT pre-registration periods (Umsatzsteuer-Voranmeldung)
565→model VatReturn {
566→ id String @id @default(cuid())
567→ periodYear Int
568→ periodMonth Int
569→ periodType String @default("monthly")
570→ status String @default("draft")
571→ k81StandardNetCents Int @default(0)
572→ k86ReducedNetCents Int @default(0)
573→ k66TaxFreeCents Int @default(0)
574→ k83TaxStandardCents Int @default(0)
575→ k65TaxReducedCents Int @default(0)
576→ k37InputTaxCents Int @default(0)
577→ outputTaxCents Int @default(0)
578→ inputTaxCents Int @default(0)
579→ payableCents Int @default(0)
580→ submittedAt DateTime?
581→ elsterRef String?
582→ acceptedAt DateTime?
583→ notes String?
584→ createdAt DateTime @default(now())
585→ updatedAt DateTime @updatedAt
586→}
587→
588→// Dunning levels for overdue invoices (Mahnwesen)
589→model DunningRun {
590→ id String @id @default(cuid())
591→ invoiceId String
592→ invoice Invoice @relation(fields: [invoiceId], references: [id])
593→ level Int
594→ status String @default("sent")
595→ feesCents Int @default(0)
596→ sentAt DateTime @default(now())
597→ createdAt DateTime @default(now())
598→}
599→
600→// ---------------------------------------------------------------------------
601→// Public API (PRD §5.5) — API tokens with scopes, webhooks with HMAC signing
602→// ---------------------------------------------------------------------------
603→
604→model ApiToken {
605→ id String @id @default(cuid())
606→ name String
607→ // The token shown to the user ONCE. We store only its SHA-256 hash so a
608→ // compromised DB does not leak usable tokens (like GitHub PATs).
609→ tokenHash String @unique
610→ tokenPrefix String // first 8 chars for identification (e.g. "aos_abc1…")
611→ scopes String // JSON array of scope strings, e.g. ["crm.company:read","finance.invoice:read"]
612→ lastUsedAt DateTime?
613→ expiresAt DateTime?
614→ revokedAt DateTime?
615→ createdAt DateTime @default(now())
616→}
617→
618→model Webhook {
619→ id String @id @default(cuid())
620→ url String // https endpoint to deliver to
621→ events String // JSON array, e.g. ["invoice.paid","contract.signed"]
622→ secret String // HMAC signing secret (stored, needed to verify signatures)
623→ active Boolean @default(true)
624→ // delivery stats
625→ sentCount Int @default(0)
626→ failCount Int @default(0)
627→ lastSentAt DateTime?
628→ lastStatus Int? // last HTTP status code received
629→ createdAt DateTime @default(now())
630→ updatedAt DateTime @updatedAt
631→}
632→
633→// ---------------------------------------------------------------------------
634→// Client Portal (PRD §6.3) — milestone approvals with evidence
635→// ---------------------------------------------------------------------------
636→
637→model PortalApproval {
638→ id String @id @default(cuid())
639→ projectId String
640→ project Project @relation(fields: [projectId], references: [id])
641→ title String // e.g. "Design-Entwürfe freigeben"
642→ description String?
643→ status String @default("pending") // pending|approved|rejected|changes-requested
644→ // evidence (PRD §6.3: "approvals with timestamp and IP evidence")
645→ decidedAt DateTime?
646→ decidedBy String? // client name
647→ decidedIp String? // real client IP at decision time
648→ decision String? // free-text reason/feedback
649→ // content hash for tamper-evidence
650→ contentHash String?
651→ createdAt DateTime @default(now())
652→ updatedAt DateTime @updatedAt
653→}
654→
655→// ---------------------------------------------------------------------------
656→// Knowledge Base (PRD §6.7) — internal + client-facing docs with versioning
657→// ---------------------------------------------------------------------------
658→
659→model Article {
660→ id String @id @default(cuid())
661→ slug String @unique
662→ title String
663→ category String // onboarding|launch-checklist|templates|handbuch|faq
664→ visibility String @default("internal") // internal|client|public
665→ excerpt String?
666→ content String // markdown
667→ tags String? // comma separated
668→ version Int @default(1)
669→ authorId String?
670→ authorName String?
671→ published Boolean @default(true)
672→ viewCount Int @default(0)
673→ createdAt DateTime @default(now())
674→ updatedAt DateTime @updatedAt
675→}
676→
677→// ---------------------------------------------------------------------------
678→// Haben-cloned features: Belege, Pauschalen, Anlagen/AfA, Kasse
679→// ---------------------------------------------------------------------------
680→
681→// Belege (receipts/documents) — content-addressed storage (PRD §5.5)
682→model Beleg {
683→ id String @id @default(cuid())
684→ number String @unique // gapless, e.g. BEL-2026-0001
685→ type String // rechnung-eingang|rechnung-ausgang|quittung|kassenbeleg|bankbeleg|sonstiges
686→ // content-addressed (SHA-256 hash of the file content)
687→ fileHash String @unique
688→ fileName String
689→ fileType String // application/pdf, image/jpeg, etc.
690→ fileSizeBytes Int
691→ // metadata extracted from the document
692→ vendor String? // Rechnungssteller
693→ invoiceNumber String? // Rechnungsnummer des Lieferanten
694→ amountCents Int? // Bruttobetrag auf dem Beleg
695→ amountNetCents Int? // Netto (falls extrahiert)
696→ taxBp Int? // USt %
697→ bookingDate DateTime?
698→ // linking
699→ expenseId String? // link to Expense if billable
700→ invoiceId String? // link to Invoice if outgoing
701→ // OCR / AI
702→ ocrText String? // extracted text (optional, via Jina/AI)
703→ // GoBD
704→ archivedAt DateTime?
705→ retentionUntil DateTime? // Aufbewahrung bis
706→ createdAt DateTime @default(now())
707→}
708→
709→// Pauschalen (flat-rate allowances) — Homeoffice, Kilometer, Verpflegung
710→model Pauschale {
711→ id String @id @default(cuid())
712→ type String // homeoffice|kilometer|verpflegung
713→ // homeoffice: €4.17/day (2026: €6.92/day up to €1.260/year)
714→ // kilometer: €0.30/km (car), €0.38/km (car w/ pass), or actual
715→ // verpflegung: €14 (8-14h), €24 (>14h, same day), €28 (overnight)
716→ date DateTime
717→ userId String? // employee
718→ amountCents Int // computed amount
719→ // details per type
720→ days Int? // homeoffice: 1; verpflegung: 1
721→ km Int? // kilometer: distance
722→ vehicleType String? // kilometer: auto|rad|fuss
723→ tripPurpose String? // kilometer: geschäftlich|fortbildung|umzug
724→ hoursAway Int? // verpflegung: hours away from office
725→ overnight Boolean @default(false) // verpflegung: overnight stay
726→ description String?
727→ projectId String? // link to project if billable
728→ expenseId String? // link to generated expense
729→ createdAt DateTime @default(now())
730→}
731→
732→// Anlagenverzeichnis & AfA (asset register + depreciation)
733→model Anlage {
734→ id String @id @default(cuid())
735→ number String @unique // e.g. ANL-2026-0001
736→ name String // "MacBook Pro 16"
737→ category String // hardware|software|mobiliar|fahrzeug|immobilie
738→ // acquisition
739→ purchaseDate DateTime
740→ purchaseCostCents Int // Anschaffungskosten (netto)
741→ taxBp Int @default(1900) // USt bei Anschaffung
742→ // AfA (depreciation)
743→ afaMethod String @default("linear") // linear|degressiv
744→ usefulLifeYears Int @default(3) // Nutzungsdauer (AfA-Tabelle)
745→ salvageValueCents Int @default(0) // Restwert
746→ // GWG (Geringwertiger Wirtschaftsgüter ≤ €800 net → Sofortabschreibung)
747→ isGWG Boolean @default(false) // GWG ≤ €800 → immediate expense
748→ // Sammelposten (€251-€1000 → Sammelabschreibung über 5 Jahre)
749→ isSammelposten Boolean @default(false)
750→ // computed
751→ annualAfaCents Int @default(0) // jährliche AfA
752→ accumulatedAfaCents Int @default(0) // kumulierte AfA
753→ bookValueCents Int @default(0) // Buchwert (purchase - accumulated AfA)
754→ // status
755→ status String @default("active") // active|sold|scrapped|fully-depreciated
756→ soldDate DateTime?
757→ soldPriceCents Int?
758→ createdAt DateTime @default(now())
759→ updatedAt DateTime @updatedAt
760→}
761→
762→// Kassenbuch (cash book for Barzahlungen — § 146 AO, § 22 UStG)
763→model KassenbuchEntry {
764→ id String @id @default(cuid())
765→ number String @unique // gapless, e.g. KAS-2026-0001
766→ date DateTime
767→ type String // einnahme|ausgabe
768→ description String
769→ amountCents Int // signed: + for einnahme, - for ausgabe
770→ taxBp Int? // USt % (if applicable)
771→ taxFree Boolean @default(false) // steuerfrei
772→ // receipt link
773→ belegId String? // link to Beleg (Kassenbeleg)
774→ // balance after this entry (laufender Kassenstand)
775→ balanceCents Int @default(0)
776→ // GoBD: Kassenbuch ist unveränderlich (elektronische Aufzeichnung)
777→ createdAt DateTime @default(now())
778→}
779→
780→
781→// ---------------------------------------------------------------------------
782→// SKR03/SKR04 Kontenrahmen + doppelte Buchführung (Haben clone)
783→// ---------------------------------------------------------------------------
784→
785→model Konto {
786→ id String @id @default(cuid())
787→ nummer String @unique
788→ name String
789→ rahmen String @default("skr04")
790→ art String // aktiv|passiv|ertrag|aufwand
791→ parentId String?
792→ locked Boolean @default(false)
793→ sollBuchungen Buchungssatz[] @relation("SollKonto")
794→ habenBuchungen Buchungssatz[] @relation("HabenKonto")
795→ createdAt DateTime @default(now())
796→}
797→
798→model Buchungssatz {
799→ id String @id @default(cuid())
800→ number String @unique
801→ belegdatum DateTime
802→ buchungsdatum DateTime @default(now())
803→ sollKontoId String
804→ sollKonto Konto @relation("SollKonto", fields: [sollKontoId], references: [id])
805→ habenKontoId String
806→ habenKonto Konto @relation("HabenKonto", fields: [habenKontoId], references: [id])
807→ betragCents Int
808→ buchungstext String
809→ belegNummer String?
810→ steuerBp Int?
811→ steuerKontoId String?
812→ festgeschrieben Boolean @default(false)
813→ festgeschriebenAt DateTime?
814→ festgeschriebenHash String?
815→ belegId String?
816→ invoiceId String?
817→ expenseId String?
818→ reverseCharge Boolean @default(false)
819→ par19UStG Boolean @default(false)
820→ createdAt DateTime @default(now())
821→}
822→
823→model EuerEntry {
824→ id String @id @default(cuid())
825→ jahr Int
826→ zeile String
827→ label String
828→ art String
829→ betragCents Int
830→ kontoNummern String?
831→ createdAt DateTime @default(now())
832→}
833→
834→model Jahreserklaerung {
835→ id String @id @default(cuid())
836→ jahr Int
837→ art String
838→ status String @default("draft")
839→ kennzahlen String?
840→ elsterRef String?
841→ submittedAt DateTime?
842→ acceptedAt DateTime?
843→ notes String?
844→ createdAt DateTime @default(now())
845→ updatedAt DateTime @updatedAt
846→}
847→
848→model Jahresarchiv {
849→ id String @id @default(cuid())
850→ jahr Int
851→ prevArchivId String?
852→ archivHash String @unique
853→ buchungenCount Int
854→ belegeCount Int
855→ rechnungenCount Int
856→ zipFileName String
857→ zipSizeBytes Int
858→ createdDate DateTime @default(now())
859→ createdBy String
860→ notes String?
861→}
862→
863→// ---------------------------------------------------------------------------
864→// Custom Fields (PRD §5.5: "Custom fields: JSONB with a schema table per entity")
865→// ---------------------------------------------------------------------------
866→
867→model CustomFieldDef {
868→ id String @id @default(cuid())
869→ entityType String // company|person|deal|project|invoice|ticket|contract
870→ key String // field key, e.g. "branche", "lead_source_detail"
871→ label String // display label, e.g. "Branche"
872→ type String // string|number|boolean|date|select|multiselect
873→ options String? // JSON array for select/multiselect
874→ required Boolean @default(false)
875→ sortOrder Int @default(0)
876→ values CustomFieldValue[]
877→ createdAt DateTime @default(now())
878→ updatedAt DateTime @updatedAt
879→
880→ @@unique([entityType, key])
881→}
882→
883→model CustomFieldValue {
884→ id String @id @default(cuid())
885→ fieldDefId String
886→ fieldDef CustomFieldDef @relation(fields: [fieldDefId], references: [id])
887→ entityType String // denormalized for fast lookup
888→ entityId String // the entity's ID
889→ value String? // JSON-encoded value (string, number, bool, array)
890→ createdAt DateTime @default(now())
891→ updatedAt DateTime @updatedAt
892→
893→ @@unique([entityType, entityId, fieldDefId])
894→}
895→