534 lines
19 KiB
Plaintext
534 lines
19 KiB
Plaintext
// 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[]
|
|
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
|
|
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
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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
|
|
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
|
|
}
|
|
|
|
|
|
|
|
|