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