Files
AOS/prisma/schema.prisma

987 lines
38 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|agency|self-signup
tags String? // comma separated
// --- B2B Agency / Reseller (PRD §6.5) ---
// A company may be a regular client OR an agency (reseller). Agencies
// onboard their own clients and license them; they cannot create invoices
// (they must use their own accounting system, e.g. lexoffice/sevdesk via REST API).
isAgency Boolean @default(false)
// Agency approval: pending (just signed up) → approved (admin allowed) → rejected.
agencyStatus String? // null when not an agency; "pending"|"approved"|"rejected"
agencyAppliedAt DateTime?
agencyApprovedAt DateTime?
agencyRejectedAt DateTime?
agencyRejectReason String?
// Admin-set discount percentage applied to all licenses this agency buys
// from us. Range 0-20 (admin decision only). Default 0.
agencyDiscountPct Int @default(0)
// Self-serve clients belong to an agency. NULL = direct client of ours.
parentAgencyId String? // FK to Company(id) — the agency that owns this client
parentAgency Company? @relation("AgencyClients", fields: [parentAgencyId], references: [id])
agencyClients Company[] @relation("AgencyClients")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
persons Person[]
deals Deal[]
leads Lead[]
licenses License[]
invoices Invoice[]
projects Project[]
contracts Contract[]
expenses Expense[]
tickets Ticket[]
agencyApplication AgencyApplication?
instances Instance[]
}
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, restov1, wp-plugin-xyz
plan String // starter|pro|enterprise
status String @default("active") // active|suspended|revoked|expired|cancelled
// environment: demo licenses are feature-limited / watermarked / time-boxed
// and can be promoted to production via the license server.
environment String @default("production") // demo|production
// Modular licensing: a license may be the "core" license, or a module add-on.
// Module licenses reference their parent core license via parentId.
parentId String? // null = core license; non-null = module add-on
parent License? @relation("LicenseModules", fields: [parentId], references: [id])
modules License[] @relation("LicenseModules")
// The module key this license grants (only when parentId is set).
// e.g. "online-order", "payment", "reservation", "delivery", "table-service"
module String? // null for core licenses
maxActivations Int @default(1)
validUntil DateTime?
token String? // Ed25519-signed token (mocked)
// Suspend/bloke reason + timestamp — set when status -> suspended.
suspendReason String?
suspendedAt DateTime?
// --- B2B Subscription & Billing (German law) ---
// Kündigung frist: 3 months (90 days) notice period before the next renewal.
// A client can cancel, but the cancellation only takes effect at the end of
// the notice period. Set on issuance, defaults to 90 days.
noticePeriodDays Int @default(90)
// Trial: first 14 days are a free trial. Subscription can be cancelled within
// these 14 days at any time with immediate effect (no charge). Day 15 the
// paid subscription starts.
trialEndsAt DateTime? // null = no trial (e.g. agency-issued)
subscriptionStartsAt DateTime? // when the paid subscription begins (trial + 1 day)
// Billing cycle: monthly or yearly. Yearly gets a discount on the total.
billingCycle String @default("monthly") // monthly|yearly
// The price in cents per billing cycle BEFORE any discount.
// For monthly: this is the monthly price (e.g. 5000 = 50 €).
// For yearly: this is the yearly total (e.g. 60000 = 600 € before yearly discount).
priceCents Int @default(0)
// Discount percentage applied to this license (0-100).
// For agencies: copied from Company.agencyDiscountPct at issue time.
// For yearly billing: a yearly discount (e.g. 10%) is applied on top.
discountPct Int @default(0)
// The effective amount actually charged (priceCents - all discounts).
// Computed at issue time and stored for audit/GoBD.
netPriceCents Int @default(0)
// Cancellation tracking (Kündigung).
cancelledAt DateTime? // when the client cancelled
cancellationEffectiveAt DateTime? // when the cancellation takes effect (notice period end)
cancellationReason String?
// Stripe integration (optional — populated when paid via Stripe).
stripeCustomerId String?
stripeSubscriptionId String?
stripePriceId String?
// Link to the tenant instance (1:1). Set when this license has a deployed
// instance on the Dokploy server.
instance Instance?
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
company Company @relation(fields: [companyId], references: [id])
projectId String?
licenseId String?
// --- Ticket routing (agency model) ---
// If the client belongs to an agency, the ticket is routed to that agency
// (agencyCompanyId). The agency handles it; only if the agency escalates
// does the ticket reach us (routedToAgency = false, escalatedToAdmin = true).
agencyCompanyId String? // the reseller that owns this client (if any)
routedToAgency Boolean @default(false) // ticket sent to agency inbox
escalatedToAdmin Boolean @default(false) // agency escalated to us
escalatedAt DateTime?
// 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|agency
fromName String
body String
createdAt DateTime @default(now())
}
// ---------------------------------------------------------------------------
// Agency Applications (PRD §6.5 — reseller onboarding)
// ---------------------------------------------------------------------------
// When a company signs up, they may choose to become an agency (reseller).
// The application is stored here and awaits admin approval. On approval,
// the Company.isAgency flag is set, Company.agencyStatus = "approved", and
// an email is sent to the applicant with their discount percentage.
model AgencyApplication {
id String @id @default(cuid())
companyId String @unique
company Company @relation(fields: [companyId], references: [id])
status String @default("pending") // pending|approved|rejected
// Discount the admin grants this agency (0-20%). Applied to all licenses
// the agency buys from us. Admin-only decision.
discountPct Int @default(0) // 0-20
// Business justification fields (filled by the applicant)
businessModel String? // e.g. "Webdesign agency", "IT consultant"
expectedClientVolume String? // e.g. "10-20 clients"
referredBy String? // how did they hear about us
// Review
reviewedBy String?
reviewedAt DateTime?
rejectReason String?
appliedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ---------------------------------------------------------------------------
// 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())
}
// ---------------------------------------------------------------------------
// SKR03/SKR04 Kontenrahmen + doppelte Buchführung (Haben clone)
// ---------------------------------------------------------------------------
model Konto {
id String @id @default(cuid())
nummer String @unique
name String
rahmen String @default("skr04")
art String // aktiv|passiv|ertrag|aufwand
parentId String?
locked Boolean @default(false)
sollBuchungen Buchungssatz[] @relation("SollKonto")
habenBuchungen Buchungssatz[] @relation("HabenKonto")
createdAt DateTime @default(now())
}
model Buchungssatz {
id String @id @default(cuid())
number String @unique
belegdatum DateTime
buchungsdatum DateTime @default(now())
sollKontoId String
sollKonto Konto @relation("SollKonto", fields: [sollKontoId], references: [id])
habenKontoId String
habenKonto Konto @relation("HabenKonto", fields: [habenKontoId], references: [id])
betragCents Int
buchungstext String
belegNummer String?
steuerBp Int?
steuerKontoId String?
festgeschrieben Boolean @default(false)
festgeschriebenAt DateTime?
festgeschriebenHash String?
belegId String?
invoiceId String?
expenseId String?
reverseCharge Boolean @default(false)
par19UStG Boolean @default(false)
createdAt DateTime @default(now())
}
model EuerEntry {
id String @id @default(cuid())
jahr Int
zeile String
label String
art String
betragCents Int
kontoNummern String?
createdAt DateTime @default(now())
}
model Jahreserklaerung {
id String @id @default(cuid())
jahr Int
art String
status String @default("draft")
kennzahlen String?
elsterRef String?
submittedAt DateTime?
acceptedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Jahresarchiv {
id String @id @default(cuid())
jahr Int
prevArchivId String?
archivHash String @unique
buchungenCount Int
belegeCount Int
rechnungenCount Int
zipFileName String
zipSizeBytes Int
createdDate DateTime @default(now())
createdBy String
notes String?
}
// ---------------------------------------------------------------------------
// Custom Fields (PRD §5.5: "Custom fields: JSONB with a schema table per entity")
// ---------------------------------------------------------------------------
model CustomFieldDef {
id String @id @default(cuid())
entityType String // company|person|deal|project|invoice|ticket|contract
key String // field key, e.g. "branche", "lead_source_detail"
label String // display label, e.g. "Branche"
type String // string|number|boolean|date|select|multiselect
options String? // JSON array for select/multiselect
required Boolean @default(false)
sortOrder Int @default(0)
values CustomFieldValue[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([entityType, key])
}
model CustomFieldValue {
id String @id @default(cuid())
fieldDefId String
fieldDef CustomFieldDef @relation(fields: [fieldDefId], references: [id])
entityType String // denormalized for fast lookup
entityId String // the entity's ID
value String? // JSON-encoded value (string, number, bool, array)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([entityType, entityId, fieldDefId])
}
// ---------------------------------------------------------------------------
// Tenant Instance Provisioning (PRD §6: SaaS demo + paid instances via Dokploy)
// ---------------------------------------------------------------------------
// AOS runs at dfmpanel.eu. Each license may have a tenant instance: an isolated
// deployment of restov1 (or another product) running on the Dokploy server,
// reachable under <random>.dfmpanel.eu (demo) or the customer's own domain.
//
// Lifecycle:
// requested -> provisioning -> demo_active --(subscribe)--> active
// | |
// (expired/cancelled) (sub cancelled)
// v v
// grace <-------------------+
// |
// (grace elapsed)
// v
// deleting -> deleted
// failed (retried, then alerts operator)
model Instance {
id String @id @default(cuid())
licenseId String? @unique // link to License (1:1, nullable for orphan cleanup)
license License? @relation(fields: [licenseId], references: [id])
companyId String // owning customer
company Company @relation(fields: [companyId], references: [id])
product String // 'restov1', 'aos-cms', ... (which template to deploy)
status String @default("requested") // requested|provisioning|demo_active|active|grace|deleting|deleted|failed
plan String @default("pro") // starter|pro|enterprise (resource limits)
// Subdomain on the platform domain (e.g. "k7f3a9" -> k7f3a9.dfmpanel.eu)
subdomain String @unique
// Dokploy server id (for future multi-server; v1 single server)
serverId String @default("default")
// Lifecycle timestamps
demoExpiresAt DateTime?
graceEndsAt DateTime?
deletedAt DateTime?
// Custom domain (customer's own domain, verified via TXT)
customDomain String?
domainVerificationToken String?
domainVerifiedAt DateTime?
// Dokploy resource IDs (returned by Dokploy API on creation)
dokployAppId String?
dokployDbId String?
dokployDbName String?
dokployDbUser String?
dokployDbPassword String? // stored encrypted in production; demo ok plaintext dev
// Hetzner DNS record ID (null if using wildcard DNS)
dnsRecordId String?
// Admin login link (one-time, demo only)
adminLoginToken String?
adminLoginExpiresAt DateTime?
// Provisioning log + metadata
metadata String? // JSON: { gitRepo, gitBranch, imageTag, resources }
lastError String?
lastErrorAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events InstanceEvent[]
jobs ProvisioningJob[]
}
model InstanceEvent {
id String @id @default(cuid())
instanceId String
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
type String // provision.start, provision.dokploy.created, provision.dns.created, provision.ok, delete.start, delete.dokploy.removed, delete.ok, suspend, reactivate, domain.verify, domain.attached, etc.
step String? // sub-step identifier
result String // ok|error|pending
details String? // JSON
createdAt DateTime @default(now())
}
model ProvisioningJob {
id String @id @default(cuid())
instanceId String
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
kind String // provision|delete|domain|suspend|reactivate
step String // current step in the sequence
attempts Int @default(0)
maxAttempts Int @default(5)
lastError String?
status String @default("pending") // pending|running|done|failed
nextRunAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}