// 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[] } 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[] } 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 } model TimeEntry { id String @id @default(cuid()) projectId String project Project @relation(fields: [projectId], references: [id]) userId String? description String minutes Int billable Boolean @default(true) rateCentsPerHour Int? invoiced Boolean @default(false) 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[] } 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 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 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 }