Files
AOS/prisma/schema.prisma
T
Z User 597bfbc3d8 feat: Recurring invoices + license suspend on dunning level 3 (PRD §6.4/§6.6)
- Recurring API: list contracts needing invoicing + run endpoint
- Auto-generate draft invoices for Wartung/SLA + Dienstvertrag contracts
- Automatic license suspension at dunning level 3 (PRD §6.4)
- Recurring billable expenses tracking
- MRR/ARR computation from contracts + expenses
- Prisma: added Contract.company + Expense.company relations
- Scheduler info (pg-boss monthly cron in production)
2026-10-27 16:51:58 +00:00

574 lines
20 KiB
Plaintext
Executable File

// 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
}