     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→