// AOS — Agency Operating System // One deployment = one agency (single tenant). SQLite for this build. // Money is integer cents, tax rates are basis points. No floats for money. generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } // --------------------------------------------------------------------------- // Platform // --------------------------------------------------------------------------- model User { id String @id @default(cuid()) email String @unique name String role String @default("owner") // owner|admin|sales|pm|developer|finance|readonly|client createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Module registry — installed/enabled/disabled state of each domain module model Module { id String @id // crm|leads|projects|licenses|sales-docs|finance|support|... version String @default("1.0.0") enabled Boolean @default(true) installed Boolean @default(true) nav String? // JSON: [{label,route,permission}] events String? // JSON: {emits:[],listens:[]} updatedAt DateTime @updatedAt createdAt DateTime @default(now()) } model Setting { key String @id value String // typed at app layer (encrypted secrets handled separately) updatedAt DateTime @updatedAt } model AuditLog { id String @id @default(cuid()) actor String? // user id or "system" action String // e.g. "crm.deal:update" entity String entityId String before String? // JSON after String? // JSON hash String? // hash chain createdAt DateTime @default(now()) } // --------------------------------------------------------------------------- // CRM (Phase 1) // --------------------------------------------------------------------------- model Company { id String @id @default(cuid()) name String legalForm String? // GmbH, UG, e.K., GbR, AG, Einzelunternehmen... vatId String? // USt-IdNr registerCourt String? registerNumber String? // HRB ... website String? email String? phone String? city String? zip String? street String? source String? // lead-finder|import|manual|referral tags String? // comma separated createdAt DateTime @default(now()) updatedAt DateTime @updatedAt persons Person[] deals Deal[] leads Lead[] licenses License[] invoices Invoice[] projects Project[] contracts Contract[] expenses Expense[] } model Person { id String @id @default(cuid()) companyId String? company Company? @relation(fields: [companyId], references: [id]) firstName String lastName String email String? phone String? role String? // Geschäftsführer, Marketing, IT... createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Deal { id String @id @default(cuid()) companyId String company Company @relation(fields: [companyId], references: [id]) title String valueCents Int @default(0) probability Int @default(0) // 0-100 stage String @default("lead") // pipeline stage key expectedClose DateTime? lossReason String? ownerId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Leads Finder (Phase 2) // --------------------------------------------------------------------------- model Lead { id String @id @default(cuid()) // discovered company info (may become a Company when approved) companyName String website String? city String? branch String? // audit + scoring score Int @default(0) // 0-100 weighted rule score scoreLabel String? // new-site|redesign|seo|aeo|shop|maintenance|accessibility evidence String? // JSON array of {id,check,status,detail} explanation String? // LLM draft "why this lead" — every claim links to an evidence id suggestedAngle String? // proposed outreach angle status String @default("pending") // pending|approved|rejected|snoozed|converted reviewReason String? source String? // serpapi|tavily|apify|firecrawl radiusKm Int? // when converted, link to CRM company + deal companyId String? company Company? @relation(fields: [companyId], references: [id]) createdAt DateTime @default(now()) reviewedAt DateTime? updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Projects (Phase 3) // --------------------------------------------------------------------------- model Project { id String @id @default(cuid()) companyId String company Company @relation(fields: [companyId], references: [id]) name String type String // website|shop|app|retainer status String @default("planning") // planning|active|review|done|on-hold|cancelled budgetType String @default("fixed") // fixed|time-materials|retainer budgetHours Int? spentHours Int @default(0) fixedPriceCents Int? startDate DateTime? dueDate DateTime? progress Int @default(0) // 0-100 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tasks Task[] timeEntries TimeEntry[] approvals PortalApproval[] } model Task { id String @id @default(cuid()) projectId String project Project @relation(fields: [projectId], references: [id]) title String description String? status String @default("todo") // todo|in-progress|review|done assignee String? estimateMin Int? priority String @default("medium") // low|medium|high createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments TaskComment[] } model TaskComment { id String @id @default(cuid()) taskId String task Task @relation(fields: [taskId], references: [id]) author String body String createdAt DateTime @default(now()) } model TimeEntry { id String @id @default(cuid()) projectId String project Project @relation(fields: [projectId], references: [id]) taskId String? // optional link to a specific task userId String? description String minutes Int billable Boolean @default(true) rateCentsPerHour Int? invoiced Boolean @default(false) locked Boolean @default(false) // locked after PM approval date DateTime @default(now()) createdAt DateTime @default(now()) } // --------------------------------------------------------------------------- // Licenses (Phase 4) // --------------------------------------------------------------------------- model License { id String @id @default(cuid()) key String @unique companyId String company Company @relation(fields: [companyId], references: [id]) product String // e.g. aos-cms, wp-plugin-xyz plan String // starter|pro|enterprise status String @default("active") // active|suspended|revoked|expired maxActivations Int @default(1) validUntil DateTime? token String? // Ed25519-signed token (mocked) createdAt DateTime @default(now()) activations LicenseActivation[] } model LicenseActivation { id String @id @default(cuid()) licenseId String license License @relation(fields: [licenseId], references: [id]) domain String fingerprint String? environment String @default("production") // production|staging version String? lastCheck DateTime @default(now()) createdAt DateTime @default(now()) } // Asset & renewal register (what the agency manages/pays for on behalf of clients) model Asset { id String @id @default(cuid()) name String type String // domain|hosting|ssl|plugin-seat|saas provider String? // Ionos, Strato, Cloudflare... companyId String? // may be internal asset costCentsPerMonth Int? renewalDate DateTime rebill Boolean @default(false) markupPct Int @default(0) status String @default("active") // active|expiring|expired|cancelled createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Finance (Phase 5) // --------------------------------------------------------------------------- model Invoice { id String @id @default(cuid()) number String @unique // gapless, e.g. RE-2026-0001 companyId String company Company @relation(fields: [companyId], references: [id]) status String @default("draft") // draft|sent|paid|overdue|cancelled|storno type String @default("final") // final|partial|recurring|credit issueDate DateTime @default(now()) dueDate DateTime paidDate DateTime? netCents Int @default(0) taxBp Int @default(1900) // 19% as basis points grossCents Int @default(0) // E-Rechnung / compliance zugferd Boolean @default(false) xrechnung Boolean @default(false) datevExported Boolean @default(false) projectId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lines InvoiceLine[] dunningRuns DunningRun[] } model InvoiceLine { id String @id @default(cuid()) invoiceId String invoice Invoice @relation(fields: [invoiceId], references: [id]) description String quantity Int @default(1) unitCents Int totalCents Int } // --------------------------------------------------------------------------- // Contracts / Verträge (Phase 5 — adapted from PerfEx Contracts, Germanized) // German contract types: Werkvertrag, Dienstvertrag, Wartungsvertrag/SLA, // AVV (Auftragsverarbeitungsvertrag), NDA, AVV // --------------------------------------------------------------------------- model Contract { id String @id @default(cuid()) number String @unique // gapless, e.g. VER-2026-0001 companyId String company Company @relation(fields: [companyId], references: [id]) type String // werkvertrag|dienstvertrag|wartung-sla|avv|nda|dpa title String status String @default("draft") // draft|sent|signed|active|expired|terminated valueCents Int @default(0) startDate DateTime? endDate DateTime? // null = unbefristet templateId String? // reference to a stored template dealId String? // optional link to originating deal projectId String? // optional link to project // E-signature hook (mock) signedAt DateTime? signedBy String? // signer name/email signedIp String? signToken String? // unique token for the signing link notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Expenses / Ausgaben (adapted from PerfEx Expenses, Germanized for DATEV) // --------------------------------------------------------------------------- model Expense { id String @id @default(cuid()) number String @unique // gapless, e.g. AUS-2026-0001 categoryId String // reise|software|hardware|fremdleistung|buero|marketing vendor String? // supplier / merchant description String amountCents Int // net amount taxBp Int @default(1900) grossCents Int @default(0) date DateTime @default(now()) // linking companyId String? // customer this is billable to company Company? @relation(fields: [companyId], references: [id]) projectId String? // project this relates to billable Boolean @default(false) // convert to customer invoice markupPct Int @default(0) // markup when re-billed // recurring recurring Boolean @default(false) recurringInterval String? // monthly|quarterly|yearly // receipt + export receiptHash String? // SHA-256 of stored receipt file (content-addressed) datevExported Boolean @default(false) // conversion convertedInvoiceId String? // link to the invoice created from this expense status String @default("recorded") // recorded|approved|invoiced|rejected createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Support Tickets (PRD §6.7 + PerfEx Support Tickets + AI Integration) // --------------------------------------------------------------------------- model Ticket { id String @id @default(cuid()) number String @unique // gapless, e.g. TKT-2026-0001 subject String status String @default("open") // open|pending|resolved|closed priority String @default("normal") // low|normal|high|urgent categoryId String // bug|anfrage|aenderung|konto|rechnung|hosting companyId String projectId String? licenseId String? // SLA slaResponseHours Int @default(8) // first response target slaResolveHours Int @default(48) // resolution target slaRespondedAt DateTime? slaResolvedAt DateTime? // AI aiSummary String? // LLM-generated summary of the conversation // assignee assignee String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt messages TicketMessage[] } model TicketMessage { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) fromType String // customer|staff|system fromName String body String createdAt DateTime @default(now()) } // --------------------------------------------------------------------------- // Estimates / Angebote (PRD §6.5 sales-docs — quotes that convert to invoice) // --------------------------------------------------------------------------- model Estimate { id String @id @default(cuid()) number String @unique // gapless, e.g. AN-2026-0001 companyId String dealId String? // originating deal projectId String? // creates project when accepted subject String status String @default("draft") // draft|sent|accepted|rejected|expired validUntil DateTime? issueDate DateTime @default(now()) acceptedAt DateTime? acceptedBy String? notes String? netCents Int @default(0) taxBp Int @default(1900) grossCents Int @default(0) // AI aiDraftFromBrief String? // flag: was this drafted from a brief via AI createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lines EstimateLine[] } model EstimateLine { id String @id @default(cuid()) estimateId String estimate Estimate @relation(fields: [estimateId], references: [id]) description String quantity Int @default(1) unitCents Int totalCents Int } // --------------------------------------------------------------------------- // Accounting / Buchhaltung (adapted from Haben, German-law compliant) // VAT return (Umsatzsteuer-Voranmeldung), EÜR, bank reconciliation, dunning. // PRD §6.6: double-entry + ELSTER out of scope; this covers the agency-facing // compliance layer (compute, validate, mock-submit) + bank matching. // --------------------------------------------------------------------------- // Bank transactions imported from CAMT.053 or PSD2 (FinTS). model BankTransaction { id String @id @default(cuid()) amountCents Int // signed: + credit (incoming), - debit (outgoing) bookingDate DateTime valueDate DateTime partnerName String? partnerIban String? reference String? purpose String? matchedInvoiceId String? matchedStatus String @default("unmatched") matchReason String? importBatchId String? importedAt DateTime @default(now()) createdAt DateTime @default(now()) } // VAT pre-registration periods (Umsatzsteuer-Voranmeldung) model VatReturn { id String @id @default(cuid()) periodYear Int periodMonth Int periodType String @default("monthly") status String @default("draft") k81StandardNetCents Int @default(0) k86ReducedNetCents Int @default(0) k66TaxFreeCents Int @default(0) k83TaxStandardCents Int @default(0) k65TaxReducedCents Int @default(0) k37InputTaxCents Int @default(0) outputTaxCents Int @default(0) inputTaxCents Int @default(0) payableCents Int @default(0) submittedAt DateTime? elsterRef String? acceptedAt DateTime? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Dunning levels for overdue invoices (Mahnwesen) model DunningRun { id String @id @default(cuid()) invoiceId String invoice Invoice @relation(fields: [invoiceId], references: [id]) level Int status String @default("sent") feesCents Int @default(0) sentAt DateTime @default(now()) createdAt DateTime @default(now()) } // --------------------------------------------------------------------------- // Public API (PRD §5.5) — API tokens with scopes, webhooks with HMAC signing // --------------------------------------------------------------------------- model ApiToken { id String @id @default(cuid()) name String // The token shown to the user ONCE. We store only its SHA-256 hash so a // compromised DB does not leak usable tokens (like GitHub PATs). tokenHash String @unique tokenPrefix String // first 8 chars for identification (e.g. "aos_abc1…") scopes String // JSON array of scope strings, e.g. ["crm.company:read","finance.invoice:read"] lastUsedAt DateTime? expiresAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) } model Webhook { id String @id @default(cuid()) url String // https endpoint to deliver to events String // JSON array, e.g. ["invoice.paid","contract.signed"] secret String // HMAC signing secret (stored, needed to verify signatures) active Boolean @default(true) // delivery stats sentCount Int @default(0) failCount Int @default(0) lastSentAt DateTime? lastStatus Int? // last HTTP status code received createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Client Portal (PRD §6.3) — milestone approvals with evidence // --------------------------------------------------------------------------- model PortalApproval { id String @id @default(cuid()) projectId String project Project @relation(fields: [projectId], references: [id]) title String // e.g. "Design-Entwürfe freigeben" description String? status String @default("pending") // pending|approved|rejected|changes-requested // evidence (PRD §6.3: "approvals with timestamp and IP evidence") decidedAt DateTime? decidedBy String? // client name decidedIp String? // real client IP at decision time decision String? // free-text reason/feedback // content hash for tamper-evidence contentHash String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Knowledge Base (PRD §6.7) — internal + client-facing docs with versioning // --------------------------------------------------------------------------- model Article { id String @id @default(cuid()) slug String @unique title String category String // onboarding|launch-checklist|templates|handbuch|faq visibility String @default("internal") // internal|client|public excerpt String? content String // markdown tags String? // comma separated version Int @default(1) authorId String? authorName String? published Boolean @default(true) viewCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --------------------------------------------------------------------------- // Haben-cloned features: Belege, Pauschalen, Anlagen/AfA, Kasse // --------------------------------------------------------------------------- // Belege (receipts/documents) — content-addressed storage (PRD §5.5) model Beleg { id String @id @default(cuid()) number String @unique // gapless, e.g. BEL-2026-0001 type String // rechnung-eingang|rechnung-ausgang|quittung|kassenbeleg|bankbeleg|sonstiges // content-addressed (SHA-256 hash of the file content) fileHash String @unique fileName String fileType String // application/pdf, image/jpeg, etc. fileSizeBytes Int // metadata extracted from the document vendor String? // Rechnungssteller invoiceNumber String? // Rechnungsnummer des Lieferanten amountCents Int? // Bruttobetrag auf dem Beleg amountNetCents Int? // Netto (falls extrahiert) taxBp Int? // USt % bookingDate DateTime? // linking expenseId String? // link to Expense if billable invoiceId String? // link to Invoice if outgoing // OCR / AI ocrText String? // extracted text (optional, via Jina/AI) // GoBD archivedAt DateTime? retentionUntil DateTime? // Aufbewahrung bis createdAt DateTime @default(now()) } // Pauschalen (flat-rate allowances) — Homeoffice, Kilometer, Verpflegung model Pauschale { id String @id @default(cuid()) type String // homeoffice|kilometer|verpflegung // homeoffice: €4.17/day (2026: €6.92/day up to €1.260/year) // kilometer: €0.30/km (car), €0.38/km (car w/ pass), or actual // verpflegung: €14 (8-14h), €24 (>14h, same day), €28 (overnight) date DateTime userId String? // employee amountCents Int // computed amount // details per type days Int? // homeoffice: 1; verpflegung: 1 km Int? // kilometer: distance vehicleType String? // kilometer: auto|rad|fuss tripPurpose String? // kilometer: geschäftlich|fortbildung|umzug hoursAway Int? // verpflegung: hours away from office overnight Boolean @default(false) // verpflegung: overnight stay description String? projectId String? // link to project if billable expenseId String? // link to generated expense createdAt DateTime @default(now()) } // Anlagenverzeichnis & AfA (asset register + depreciation) model Anlage { id String @id @default(cuid()) number String @unique // e.g. ANL-2026-0001 name String // "MacBook Pro 16" category String // hardware|software|mobiliar|fahrzeug|immobilie // acquisition purchaseDate DateTime purchaseCostCents Int // Anschaffungskosten (netto) taxBp Int @default(1900) // USt bei Anschaffung // AfA (depreciation) afaMethod String @default("linear") // linear|degressiv usefulLifeYears Int @default(3) // Nutzungsdauer (AfA-Tabelle) salvageValueCents Int @default(0) // Restwert // GWG (Geringwertiger Wirtschaftsgüter ≤ €800 net → Sofortabschreibung) isGWG Boolean @default(false) // GWG ≤ €800 → immediate expense // Sammelposten (€251-€1000 → Sammelabschreibung über 5 Jahre) isSammelposten Boolean @default(false) // computed annualAfaCents Int @default(0) // jährliche AfA accumulatedAfaCents Int @default(0) // kumulierte AfA bookValueCents Int @default(0) // Buchwert (purchase - accumulated AfA) // status status String @default("active") // active|sold|scrapped|fully-depreciated soldDate DateTime? soldPriceCents Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Kassenbuch (cash book for Barzahlungen — § 146 AO, § 22 UStG) model KassenbuchEntry { id String @id @default(cuid()) number String @unique // gapless, e.g. KAS-2026-0001 date DateTime type String // einnahme|ausgabe description String amountCents Int // signed: + for einnahme, - for ausgabe taxBp Int? // USt % (if applicable) taxFree Boolean @default(false) // steuerfrei // receipt link belegId String? // link to Beleg (Kassenbeleg) // balance after this entry (laufender Kassenstand) balanceCents Int @default(0) // GoBD: Kassenbuch ist unveränderlich (elektronische Aufzeichnung) createdAt DateTime @default(now()) }