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→