895 lines
42 KiB
Plaintext
895 lines
42 KiB
Plaintext
1→// AOS — Agency Operating System
|
|
2→// One deployment = one agency (single tenant). SQLite for this build.
|
|
3→// Money is integer cents, tax rates are basis points. No floats for money.
|
|
4→
|
|
5→generator client {
|
|
6→ provider = "prisma-client-js"
|
|
7→}
|
|
8→
|
|
9→datasource db {
|
|
10→ provider = "sqlite"
|
|
11→ url = env("DATABASE_URL")
|
|
12→}
|
|
13→
|
|
14→// ---------------------------------------------------------------------------
|
|
15→// Platform
|
|
16→// ---------------------------------------------------------------------------
|
|
17→
|
|
18→model User {
|
|
19→ id String @id @default(cuid())
|
|
20→ email String @unique
|
|
21→ name String
|
|
22→ role String @default("owner") // owner|admin|sales|pm|developer|finance|readonly|client
|
|
23→ createdAt DateTime @default(now())
|
|
24→ updatedAt DateTime @updatedAt
|
|
25→}
|
|
26→
|
|
27→// Module registry — installed/enabled/disabled state of each domain module
|
|
28→model Module {
|
|
29→ id String @id // crm|leads|projects|licenses|sales-docs|finance|support|...
|
|
30→ version String @default("1.0.0")
|
|
31→ enabled Boolean @default(true)
|
|
32→ installed Boolean @default(true)
|
|
33→ nav String? // JSON: [{label,route,permission}]
|
|
34→ events String? // JSON: {emits:[],listens:[]}
|
|
35→ updatedAt DateTime @updatedAt
|
|
36→ createdAt DateTime @default(now())
|
|
37→}
|
|
38→
|
|
39→model Setting {
|
|
40→ key String @id
|
|
41→ value String // typed at app layer (encrypted secrets handled separately)
|
|
42→ updatedAt DateTime @updatedAt
|
|
43→}
|
|
44→
|
|
45→model AuditLog {
|
|
46→ id String @id @default(cuid())
|
|
47→ actor String? // user id or "system"
|
|
48→ action String // e.g. "crm.deal:update"
|
|
49→ entity String
|
|
50→ entityId String
|
|
51→ before String? // JSON
|
|
52→ after String? // JSON
|
|
53→ hash String? // hash chain
|
|
54→ createdAt DateTime @default(now())
|
|
55→}
|
|
56→
|
|
57→// ---------------------------------------------------------------------------
|
|
58→// CRM (Phase 1)
|
|
59→// ---------------------------------------------------------------------------
|
|
60→
|
|
61→model Company {
|
|
62→ id String @id @default(cuid())
|
|
63→ name String
|
|
64→ legalForm String? // GmbH, UG, e.K., GbR, AG, Einzelunternehmen...
|
|
65→ vatId String? // USt-IdNr
|
|
66→ registerCourt String?
|
|
67→ registerNumber String? // HRB ...
|
|
68→ website String?
|
|
69→ email String?
|
|
70→ phone String?
|
|
71→ city String?
|
|
72→ zip String?
|
|
73→ street String?
|
|
74→ source String? // lead-finder|import|manual|referral|agency|self-signup
|
|
75→ tags String? // comma separated
|
|
76→
|
|
77→ // --- B2B Agency / Reseller (PRD §6.5) ---
|
|
78→ // A company may be a regular client OR an agency (reseller). Agencies
|
|
79→ // onboard their own clients and license them; they cannot create invoices
|
|
80→ // (they must use their own accounting system, e.g. lexoffice/sevdesk via REST API).
|
|
81→ isAgency Boolean @default(false)
|
|
82→ // Agency approval: pending (just signed up) → approved (admin allowed) → rejected.
|
|
83→ agencyStatus String? // null when not an agency; "pending"|"approved"|"rejected"
|
|
84→ agencyAppliedAt DateTime?
|
|
85→ agencyApprovedAt DateTime?
|
|
86→ agencyRejectedAt DateTime?
|
|
87→ agencyRejectReason String?
|
|
88→ // Admin-set discount percentage applied to all licenses this agency buys
|
|
89→ // from us. Range 0-20 (admin decision only). Default 0.
|
|
90→ agencyDiscountPct Int @default(0)
|
|
91→ // Self-serve clients belong to an agency. NULL = direct client of ours.
|
|
92→ parentAgencyId String? // FK to Company(id) — the agency that owns this client
|
|
93→ parentAgency Company? @relation("AgencyClients", fields: [parentAgencyId], references: [id])
|
|
94→ agencyClients Company[] @relation("AgencyClients")
|
|
95→
|
|
96→ createdAt DateTime @default(now())
|
|
97→ updatedAt DateTime @updatedAt
|
|
98→ persons Person[]
|
|
99→ deals Deal[]
|
|
100→ leads Lead[]
|
|
101→ licenses License[]
|
|
102→ invoices Invoice[]
|
|
103→ projects Project[]
|
|
104→ contracts Contract[]
|
|
105→ expenses Expense[]
|
|
106→ tickets Ticket[]
|
|
107→ agencyApplication AgencyApplication?
|
|
108→}
|
|
109→
|
|
110→model Person {
|
|
111→ id String @id @default(cuid())
|
|
112→ companyId String?
|
|
113→ company Company? @relation(fields: [companyId], references: [id])
|
|
114→ firstName String
|
|
115→ lastName String
|
|
116→ email String?
|
|
117→ phone String?
|
|
118→ role String? // Geschäftsführer, Marketing, IT...
|
|
119→ createdAt DateTime @default(now())
|
|
120→ updatedAt DateTime @updatedAt
|
|
121→}
|
|
122→
|
|
123→model Deal {
|
|
124→ id String @id @default(cuid())
|
|
125→ companyId String
|
|
126→ company Company @relation(fields: [companyId], references: [id])
|
|
127→ title String
|
|
128→ valueCents Int @default(0)
|
|
129→ probability Int @default(0) // 0-100
|
|
130→ stage String @default("lead") // pipeline stage key
|
|
131→ expectedClose DateTime?
|
|
132→ lossReason String?
|
|
133→ ownerId String?
|
|
134→ createdAt DateTime @default(now())
|
|
135→ updatedAt DateTime @updatedAt
|
|
136→}
|
|
137→
|
|
138→// ---------------------------------------------------------------------------
|
|
139→// Leads Finder (Phase 2)
|
|
140→// ---------------------------------------------------------------------------
|
|
141→
|
|
142→model Lead {
|
|
143→ id String @id @default(cuid())
|
|
144→ // discovered company info (may become a Company when approved)
|
|
145→ companyName String
|
|
146→ website String?
|
|
147→ city String?
|
|
148→ branch String?
|
|
149→ // audit + scoring
|
|
150→ score Int @default(0) // 0-100 weighted rule score
|
|
151→ scoreLabel String? // new-site|redesign|seo|aeo|shop|maintenance|accessibility
|
|
152→ evidence String? // JSON array of {id,check,status,detail}
|
|
153→ explanation String? // LLM draft "why this lead" — every claim links to an evidence id
|
|
154→ suggestedAngle String? // proposed outreach angle
|
|
155→ status String @default("pending") // pending|approved|rejected|snoozed|converted
|
|
156→ reviewReason String?
|
|
157→ source String? // serpapi|tavily|apify|firecrawl
|
|
158→ radiusKm Int?
|
|
159→ // when converted, link to CRM company + deal
|
|
160→ companyId String?
|
|
161→ company Company? @relation(fields: [companyId], references: [id])
|
|
162→ createdAt DateTime @default(now())
|
|
163→ reviewedAt DateTime?
|
|
164→ updatedAt DateTime @updatedAt
|
|
165→}
|
|
166→
|
|
167→// ---------------------------------------------------------------------------
|
|
168→// Projects (Phase 3)
|
|
169→// ---------------------------------------------------------------------------
|
|
170→
|
|
171→model Project {
|
|
172→ id String @id @default(cuid())
|
|
173→ companyId String
|
|
174→ company Company @relation(fields: [companyId], references: [id])
|
|
175→ name String
|
|
176→ type String // website|shop|app|retainer
|
|
177→ status String @default("planning") // planning|active|review|done|on-hold|cancelled
|
|
178→ budgetType String @default("fixed") // fixed|time-materials|retainer
|
|
179→ budgetHours Int?
|
|
180→ spentHours Int @default(0)
|
|
181→ fixedPriceCents Int?
|
|
182→ startDate DateTime?
|
|
183→ dueDate DateTime?
|
|
184→ progress Int @default(0) // 0-100
|
|
185→ createdAt DateTime @default(now())
|
|
186→ updatedAt DateTime @updatedAt
|
|
187→ tasks Task[]
|
|
188→ timeEntries TimeEntry[]
|
|
189→ approvals PortalApproval[]
|
|
190→}
|
|
191→
|
|
192→model Task {
|
|
193→ id String @id @default(cuid())
|
|
194→ projectId String
|
|
195→ project Project @relation(fields: [projectId], references: [id])
|
|
196→ title String
|
|
197→ description String?
|
|
198→ status String @default("todo") // todo|in-progress|review|done
|
|
199→ assignee String?
|
|
200→ estimateMin Int?
|
|
201→ priority String @default("medium") // low|medium|high
|
|
202→ createdAt DateTime @default(now())
|
|
203→ updatedAt DateTime @updatedAt
|
|
204→ comments TaskComment[]
|
|
205→}
|
|
206→
|
|
207→model TaskComment {
|
|
208→ id String @id @default(cuid())
|
|
209→ taskId String
|
|
210→ task Task @relation(fields: [taskId], references: [id])
|
|
211→ author String
|
|
212→ body String
|
|
213→ createdAt DateTime @default(now())
|
|
214→}
|
|
215→
|
|
216→model TimeEntry {
|
|
217→ id String @id @default(cuid())
|
|
218→ projectId String
|
|
219→ project Project @relation(fields: [projectId], references: [id])
|
|
220→ taskId String? // optional link to a specific task
|
|
221→ userId String?
|
|
222→ description String
|
|
223→ minutes Int
|
|
224→ billable Boolean @default(true)
|
|
225→ rateCentsPerHour Int?
|
|
226→ invoiced Boolean @default(false)
|
|
227→ locked Boolean @default(false) // locked after PM approval
|
|
228→ date DateTime @default(now())
|
|
229→ createdAt DateTime @default(now())
|
|
230→}
|
|
231→
|
|
232→// ---------------------------------------------------------------------------
|
|
233→// Licenses (Phase 4)
|
|
234→// ---------------------------------------------------------------------------
|
|
235→
|
|
236→model License {
|
|
237→ id String @id @default(cuid())
|
|
238→ key String @unique
|
|
239→ companyId String
|
|
240→ company Company @relation(fields: [companyId], references: [id])
|
|
241→ product String // e.g. aos-cms, restov1, wp-plugin-xyz
|
|
242→ plan String // starter|pro|enterprise
|
|
243→ status String @default("active") // active|suspended|revoked|expired|cancelled
|
|
244→ // environment: demo licenses are feature-limited / watermarked / time-boxed
|
|
245→ // and can be promoted to production via the license server.
|
|
246→ environment String @default("production") // demo|production
|
|
247→ // Modular licensing: a license may be the "core" license, or a module add-on.
|
|
248→ // Module licenses reference their parent core license via parentId.
|
|
249→ parentId String? // null = core license; non-null = module add-on
|
|
250→ parent License? @relation("LicenseModules", fields: [parentId], references: [id])
|
|
251→ modules License[] @relation("LicenseModules")
|
|
252→ // The module key this license grants (only when parentId is set).
|
|
253→ // e.g. "online-order", "payment", "reservation", "delivery", "table-service"
|
|
254→ module String? // null for core licenses
|
|
255→ maxActivations Int @default(1)
|
|
256→ validUntil DateTime?
|
|
257→ token String? // Ed25519-signed token (mocked)
|
|
258→ // Suspend/bloke reason + timestamp — set when status -> suspended.
|
|
259→ suspendReason String?
|
|
260→ suspendedAt DateTime?
|
|
261→
|
|
262→ // --- B2B Subscription & Billing (German law) ---
|
|
263→ // Kündigung frist: 3 months (90 days) notice period before the next renewal.
|
|
264→ // A client can cancel, but the cancellation only takes effect at the end of
|
|
265→ // the notice period. Set on issuance, defaults to 90 days.
|
|
266→ noticePeriodDays Int @default(90)
|
|
267→ // Trial: first 14 days are a free trial. Subscription can be cancelled within
|
|
268→ // these 14 days at any time with immediate effect (no charge). Day 15 the
|
|
269→ // paid subscription starts.
|
|
270→ trialEndsAt DateTime? // null = no trial (e.g. agency-issued)
|
|
271→ subscriptionStartsAt DateTime? // when the paid subscription begins (trial + 1 day)
|
|
272→ // Billing cycle: monthly or yearly. Yearly gets a discount on the total.
|
|
273→ billingCycle String @default("monthly") // monthly|yearly
|
|
274→ // The price in cents per billing cycle BEFORE any discount.
|
|
275→ // For monthly: this is the monthly price (e.g. 5000 = 50 €).
|
|
276→ // For yearly: this is the yearly total (e.g. 60000 = 600 € before yearly discount).
|
|
277→ priceCents Int @default(0)
|
|
278→ // Discount percentage applied to this license (0-100).
|
|
279→ // For agencies: copied from Company.agencyDiscountPct at issue time.
|
|
280→ // For yearly billing: a yearly discount (e.g. 10%) is applied on top.
|
|
281→ discountPct Int @default(0)
|
|
282→ // The effective amount actually charged (priceCents - all discounts).
|
|
283→ // Computed at issue time and stored for audit/GoBD.
|
|
284→ netPriceCents Int @default(0)
|
|
285→ // Cancellation tracking (Kündigung).
|
|
286→ cancelledAt DateTime? // when the client cancelled
|
|
287→ cancellationEffectiveAt DateTime? // when the cancellation takes effect (notice period end)
|
|
288→ cancellationReason String?
|
|
289→ // Stripe integration (optional — populated when paid via Stripe).
|
|
290→ stripeCustomerId String?
|
|
291→ stripeSubscriptionId String?
|
|
292→ stripePriceId String?
|
|
293→
|
|
294→ createdAt DateTime @default(now())
|
|
295→ activations LicenseActivation[]
|
|
296→}
|
|
297→
|
|
298→model LicenseActivation {
|
|
299→ id String @id @default(cuid())
|
|
300→ licenseId String
|
|
301→ license License @relation(fields: [licenseId], references: [id])
|
|
302→ domain String
|
|
303→ fingerprint String?
|
|
304→ environment String @default("production") // production|staging
|
|
305→ version String?
|
|
306→ lastCheck DateTime @default(now())
|
|
307→ createdAt DateTime @default(now())
|
|
308→}
|
|
309→
|
|
310→// Asset & renewal register (what the agency manages/pays for on behalf of clients)
|
|
311→model Asset {
|
|
312→ id String @id @default(cuid())
|
|
313→ name String
|
|
314→ type String // domain|hosting|ssl|plugin-seat|saas
|
|
315→ provider String? // Ionos, Strato, Cloudflare...
|
|
316→ companyId String? // may be internal asset
|
|
317→ costCentsPerMonth Int?
|
|
318→ renewalDate DateTime
|
|
319→ rebill Boolean @default(false)
|
|
320→ markupPct Int @default(0)
|
|
321→ status String @default("active") // active|expiring|expired|cancelled
|
|
322→ createdAt DateTime @default(now())
|
|
323→ updatedAt DateTime @updatedAt
|
|
324→}
|
|
325→
|
|
326→// ---------------------------------------------------------------------------
|
|
327→// Finance (Phase 5)
|
|
328→// ---------------------------------------------------------------------------
|
|
329→
|
|
330→model Invoice {
|
|
331→ id String @id @default(cuid())
|
|
332→ number String @unique // gapless, e.g. RE-2026-0001
|
|
333→ companyId String
|
|
334→ company Company @relation(fields: [companyId], references: [id])
|
|
335→ status String @default("draft") // draft|sent|paid|overdue|cancelled|storno
|
|
336→ type String @default("final") // final|partial|recurring|credit
|
|
337→ issueDate DateTime @default(now())
|
|
338→ dueDate DateTime
|
|
339→ paidDate DateTime?
|
|
340→ netCents Int @default(0)
|
|
341→ taxBp Int @default(1900) // 19% as basis points
|
|
342→ grossCents Int @default(0)
|
|
343→ // E-Rechnung / compliance
|
|
344→ zugferd Boolean @default(false)
|
|
345→ xrechnung Boolean @default(false)
|
|
346→ datevExported Boolean @default(false)
|
|
347→ projectId String?
|
|
348→ createdAt DateTime @default(now())
|
|
349→ updatedAt DateTime @updatedAt
|
|
350→ lines InvoiceLine[]
|
|
351→ dunningRuns DunningRun[]
|
|
352→}
|
|
353→
|
|
354→model InvoiceLine {
|
|
355→ id String @id @default(cuid())
|
|
356→ invoiceId String
|
|
357→ invoice Invoice @relation(fields: [invoiceId], references: [id])
|
|
358→ description String
|
|
359→ quantity Int @default(1)
|
|
360→ unitCents Int
|
|
361→ totalCents Int
|
|
362→}
|
|
363→
|
|
364→// ---------------------------------------------------------------------------
|
|
365→// Contracts / Verträge (Phase 5 — adapted from PerfEx Contracts, Germanized)
|
|
366→// German contract types: Werkvertrag, Dienstvertrag, Wartungsvertrag/SLA,
|
|
367→// AVV (Auftragsverarbeitungsvertrag), NDA, AVV
|
|
368→// ---------------------------------------------------------------------------
|
|
369→
|
|
370→model Contract {
|
|
371→ id String @id @default(cuid())
|
|
372→ number String @unique // gapless, e.g. VER-2026-0001
|
|
373→ companyId String
|
|
374→ company Company @relation(fields: [companyId], references: [id])
|
|
375→ type String // werkvertrag|dienstvertrag|wartung-sla|avv|nda|dpa
|
|
376→ title String
|
|
377→ status String @default("draft") // draft|sent|signed|active|expired|terminated
|
|
378→ valueCents Int @default(0)
|
|
379→ startDate DateTime?
|
|
380→ endDate DateTime? // null = unbefristet
|
|
381→ templateId String? // reference to a stored template
|
|
382→ dealId String? // optional link to originating deal
|
|
383→ projectId String? // optional link to project
|
|
384→ // E-signature hook (mock)
|
|
385→ signedAt DateTime?
|
|
386→ signedBy String? // signer name/email
|
|
387→ signedIp String?
|
|
388→ signToken String? // unique token for the signing link
|
|
389→ notes String?
|
|
390→ createdAt DateTime @default(now())
|
|
391→ updatedAt DateTime @updatedAt
|
|
392→}
|
|
393→
|
|
394→// ---------------------------------------------------------------------------
|
|
395→// Expenses / Ausgaben (adapted from PerfEx Expenses, Germanized for DATEV)
|
|
396→// ---------------------------------------------------------------------------
|
|
397→
|
|
398→model Expense {
|
|
399→ id String @id @default(cuid())
|
|
400→ number String @unique // gapless, e.g. AUS-2026-0001
|
|
401→ categoryId String // reise|software|hardware|fremdleistung|buero|marketing
|
|
402→ vendor String? // supplier / merchant
|
|
403→ description String
|
|
404→ amountCents Int // net amount
|
|
405→ taxBp Int @default(1900)
|
|
406→ grossCents Int @default(0)
|
|
407→ date DateTime @default(now())
|
|
408→ // linking
|
|
409→ companyId String? // customer this is billable to
|
|
410→ company Company? @relation(fields: [companyId], references: [id])
|
|
411→ projectId String? // project this relates to
|
|
412→ billable Boolean @default(false) // convert to customer invoice
|
|
413→ markupPct Int @default(0) // markup when re-billed
|
|
414→ // recurring
|
|
415→ recurring Boolean @default(false)
|
|
416→ recurringInterval String? // monthly|quarterly|yearly
|
|
417→ // receipt + export
|
|
418→ receiptHash String? // SHA-256 of stored receipt file (content-addressed)
|
|
419→ datevExported Boolean @default(false)
|
|
420→ // conversion
|
|
421→ convertedInvoiceId String? // link to the invoice created from this expense
|
|
422→ status String @default("recorded") // recorded|approved|invoiced|rejected
|
|
423→ createdAt DateTime @default(now())
|
|
424→ updatedAt DateTime @updatedAt
|
|
425→}
|
|
426→
|
|
427→// ---------------------------------------------------------------------------
|
|
428→// Support Tickets (PRD §6.7 + PerfEx Support Tickets + AI Integration)
|
|
429→// ---------------------------------------------------------------------------
|
|
430→
|
|
431→model Ticket {
|
|
432→ id String @id @default(cuid())
|
|
433→ number String @unique // gapless, e.g. TKT-2026-0001
|
|
434→ subject String
|
|
435→ status String @default("open") // open|pending|resolved|closed
|
|
436→ priority String @default("normal") // low|normal|high|urgent
|
|
437→ categoryId String // bug|anfrage|aenderung|konto|rechnung|hosting
|
|
438→ companyId String
|
|
439→ company Company @relation(fields: [companyId], references: [id])
|
|
440→ projectId String?
|
|
441→ licenseId String?
|
|
442→ // --- Ticket routing (agency model) ---
|
|
443→ // If the client belongs to an agency, the ticket is routed to that agency
|
|
444→ // (agencyCompanyId). The agency handles it; only if the agency escalates
|
|
445→ // does the ticket reach us (routedToAgency = false, escalatedToAdmin = true).
|
|
446→ agencyCompanyId String? // the reseller that owns this client (if any)
|
|
447→ routedToAgency Boolean @default(false) // ticket sent to agency inbox
|
|
448→ escalatedToAdmin Boolean @default(false) // agency escalated to us
|
|
449→ escalatedAt DateTime?
|
|
450→ // SLA
|
|
451→ slaResponseHours Int @default(8) // first response target
|
|
452→ slaResolveHours Int @default(48) // resolution target
|
|
453→ slaRespondedAt DateTime?
|
|
454→ slaResolvedAt DateTime?
|
|
455→ // AI
|
|
456→ aiSummary String? // LLM-generated summary of the conversation
|
|
457→ // assignee
|
|
458→ assignee String?
|
|
459→ createdAt DateTime @default(now())
|
|
460→ updatedAt DateTime @updatedAt
|
|
461→ messages TicketMessage[]
|
|
462→}
|
|
463→
|
|
464→model TicketMessage {
|
|
465→ id String @id @default(cuid())
|
|
466→ ticketId String
|
|
467→ ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
468→ fromType String // customer|staff|system|agency
|
|
469→ fromName String
|
|
470→ body String
|
|
471→ createdAt DateTime @default(now())
|
|
472→}
|
|
473→
|
|
474→// ---------------------------------------------------------------------------
|
|
475→// Agency Applications (PRD §6.5 — reseller onboarding)
|
|
476→// ---------------------------------------------------------------------------
|
|
477→// When a company signs up, they may choose to become an agency (reseller).
|
|
478→// The application is stored here and awaits admin approval. On approval,
|
|
479→// the Company.isAgency flag is set, Company.agencyStatus = "approved", and
|
|
480→// an email is sent to the applicant with their discount percentage.
|
|
481→model AgencyApplication {
|
|
482→ id String @id @default(cuid())
|
|
483→ companyId String @unique
|
|
484→ company Company @relation(fields: [companyId], references: [id])
|
|
485→ status String @default("pending") // pending|approved|rejected
|
|
486→ // Discount the admin grants this agency (0-20%). Applied to all licenses
|
|
487→ // the agency buys from us. Admin-only decision.
|
|
488→ discountPct Int @default(0) // 0-20
|
|
489→ // Business justification fields (filled by the applicant)
|
|
490→ businessModel String? // e.g. "Webdesign agency", "IT consultant"
|
|
491→ expectedClientVolume String? // e.g. "10-20 clients"
|
|
492→ referredBy String? // how did they hear about us
|
|
493→ // Review
|
|
494→ reviewedBy String?
|
|
495→ reviewedAt DateTime?
|
|
496→ rejectReason String?
|
|
497→ appliedAt DateTime @default(now())
|
|
498→ createdAt DateTime @default(now())
|
|
499→ updatedAt DateTime @updatedAt
|
|
500→}
|
|
501→
|
|
502→// ---------------------------------------------------------------------------
|
|
503→// Estimates / Angebote (PRD §6.5 sales-docs — quotes that convert to invoice)
|
|
504→// ---------------------------------------------------------------------------
|
|
505→
|
|
506→model Estimate {
|
|
507→ id String @id @default(cuid())
|
|
508→ number String @unique // gapless, e.g. AN-2026-0001
|
|
509→ companyId String
|
|
510→ dealId String? // originating deal
|
|
511→ projectId String? // creates project when accepted
|
|
512→ subject String
|
|
513→ status String @default("draft") // draft|sent|accepted|rejected|expired
|
|
514→ validUntil DateTime?
|
|
515→ issueDate DateTime @default(now())
|
|
516→ acceptedAt DateTime?
|
|
517→ acceptedBy String?
|
|
518→ notes String?
|
|
519→ netCents Int @default(0)
|
|
520→ taxBp Int @default(1900)
|
|
521→ grossCents Int @default(0)
|
|
522→ // AI
|
|
523→ aiDraftFromBrief String? // flag: was this drafted from a brief via AI
|
|
524→ createdAt DateTime @default(now())
|
|
525→ updatedAt DateTime @updatedAt
|
|
526→ lines EstimateLine[]
|
|
527→}
|
|
528→
|
|
529→model EstimateLine {
|
|
530→ id String @id @default(cuid())
|
|
531→ estimateId String
|
|
532→ estimate Estimate @relation(fields: [estimateId], references: [id])
|
|
533→ description String
|
|
534→ quantity Int @default(1)
|
|
535→ unitCents Int
|
|
536→ totalCents Int
|
|
537→}
|
|
538→
|
|
539→// ---------------------------------------------------------------------------
|
|
540→// Accounting / Buchhaltung (adapted from Haben, German-law compliant)
|
|
541→// VAT return (Umsatzsteuer-Voranmeldung), EÜR, bank reconciliation, dunning.
|
|
542→// PRD §6.6: double-entry + ELSTER out of scope; this covers the agency-facing
|
|
543→// compliance layer (compute, validate, mock-submit) + bank matching.
|
|
544→// ---------------------------------------------------------------------------
|
|
545→
|
|
546→// Bank transactions imported from CAMT.053 or PSD2 (FinTS).
|
|
547→model BankTransaction {
|
|
548→ id String @id @default(cuid())
|
|
549→ amountCents Int // signed: + credit (incoming), - debit (outgoing)
|
|
550→ bookingDate DateTime
|
|
551→ valueDate DateTime
|
|
552→ partnerName String?
|
|
553→ partnerIban String?
|
|
554→ reference String?
|
|
555→ purpose String?
|
|
556→ matchedInvoiceId String?
|
|
557→ matchedStatus String @default("unmatched")
|
|
558→ matchReason String?
|
|
559→ importBatchId String?
|
|
560→ importedAt DateTime @default(now())
|
|
561→ createdAt DateTime @default(now())
|
|
562→}
|
|
563→
|
|
564→// VAT pre-registration periods (Umsatzsteuer-Voranmeldung)
|
|
565→model VatReturn {
|
|
566→ id String @id @default(cuid())
|
|
567→ periodYear Int
|
|
568→ periodMonth Int
|
|
569→ periodType String @default("monthly")
|
|
570→ status String @default("draft")
|
|
571→ k81StandardNetCents Int @default(0)
|
|
572→ k86ReducedNetCents Int @default(0)
|
|
573→ k66TaxFreeCents Int @default(0)
|
|
574→ k83TaxStandardCents Int @default(0)
|
|
575→ k65TaxReducedCents Int @default(0)
|
|
576→ k37InputTaxCents Int @default(0)
|
|
577→ outputTaxCents Int @default(0)
|
|
578→ inputTaxCents Int @default(0)
|
|
579→ payableCents Int @default(0)
|
|
580→ submittedAt DateTime?
|
|
581→ elsterRef String?
|
|
582→ acceptedAt DateTime?
|
|
583→ notes String?
|
|
584→ createdAt DateTime @default(now())
|
|
585→ updatedAt DateTime @updatedAt
|
|
586→}
|
|
587→
|
|
588→// Dunning levels for overdue invoices (Mahnwesen)
|
|
589→model DunningRun {
|
|
590→ id String @id @default(cuid())
|
|
591→ invoiceId String
|
|
592→ invoice Invoice @relation(fields: [invoiceId], references: [id])
|
|
593→ level Int
|
|
594→ status String @default("sent")
|
|
595→ feesCents Int @default(0)
|
|
596→ sentAt DateTime @default(now())
|
|
597→ createdAt DateTime @default(now())
|
|
598→}
|
|
599→
|
|
600→// ---------------------------------------------------------------------------
|
|
601→// Public API (PRD §5.5) — API tokens with scopes, webhooks with HMAC signing
|
|
602→// ---------------------------------------------------------------------------
|
|
603→
|
|
604→model ApiToken {
|
|
605→ id String @id @default(cuid())
|
|
606→ name String
|
|
607→ // The token shown to the user ONCE. We store only its SHA-256 hash so a
|
|
608→ // compromised DB does not leak usable tokens (like GitHub PATs).
|
|
609→ tokenHash String @unique
|
|
610→ tokenPrefix String // first 8 chars for identification (e.g. "aos_abc1…")
|
|
611→ scopes String // JSON array of scope strings, e.g. ["crm.company:read","finance.invoice:read"]
|
|
612→ lastUsedAt DateTime?
|
|
613→ expiresAt DateTime?
|
|
614→ revokedAt DateTime?
|
|
615→ createdAt DateTime @default(now())
|
|
616→}
|
|
617→
|
|
618→model Webhook {
|
|
619→ id String @id @default(cuid())
|
|
620→ url String // https endpoint to deliver to
|
|
621→ events String // JSON array, e.g. ["invoice.paid","contract.signed"]
|
|
622→ secret String // HMAC signing secret (stored, needed to verify signatures)
|
|
623→ active Boolean @default(true)
|
|
624→ // delivery stats
|
|
625→ sentCount Int @default(0)
|
|
626→ failCount Int @default(0)
|
|
627→ lastSentAt DateTime?
|
|
628→ lastStatus Int? // last HTTP status code received
|
|
629→ createdAt DateTime @default(now())
|
|
630→ updatedAt DateTime @updatedAt
|
|
631→}
|
|
632→
|
|
633→// ---------------------------------------------------------------------------
|
|
634→// Client Portal (PRD §6.3) — milestone approvals with evidence
|
|
635→// ---------------------------------------------------------------------------
|
|
636→
|
|
637→model PortalApproval {
|
|
638→ id String @id @default(cuid())
|
|
639→ projectId String
|
|
640→ project Project @relation(fields: [projectId], references: [id])
|
|
641→ title String // e.g. "Design-Entwürfe freigeben"
|
|
642→ description String?
|
|
643→ status String @default("pending") // pending|approved|rejected|changes-requested
|
|
644→ // evidence (PRD §6.3: "approvals with timestamp and IP evidence")
|
|
645→ decidedAt DateTime?
|
|
646→ decidedBy String? // client name
|
|
647→ decidedIp String? // real client IP at decision time
|
|
648→ decision String? // free-text reason/feedback
|
|
649→ // content hash for tamper-evidence
|
|
650→ contentHash String?
|
|
651→ createdAt DateTime @default(now())
|
|
652→ updatedAt DateTime @updatedAt
|
|
653→}
|
|
654→
|
|
655→// ---------------------------------------------------------------------------
|
|
656→// Knowledge Base (PRD §6.7) — internal + client-facing docs with versioning
|
|
657→// ---------------------------------------------------------------------------
|
|
658→
|
|
659→model Article {
|
|
660→ id String @id @default(cuid())
|
|
661→ slug String @unique
|
|
662→ title String
|
|
663→ category String // onboarding|launch-checklist|templates|handbuch|faq
|
|
664→ visibility String @default("internal") // internal|client|public
|
|
665→ excerpt String?
|
|
666→ content String // markdown
|
|
667→ tags String? // comma separated
|
|
668→ version Int @default(1)
|
|
669→ authorId String?
|
|
670→ authorName String?
|
|
671→ published Boolean @default(true)
|
|
672→ viewCount Int @default(0)
|
|
673→ createdAt DateTime @default(now())
|
|
674→ updatedAt DateTime @updatedAt
|
|
675→}
|
|
676→
|
|
677→// ---------------------------------------------------------------------------
|
|
678→// Haben-cloned features: Belege, Pauschalen, Anlagen/AfA, Kasse
|
|
679→// ---------------------------------------------------------------------------
|
|
680→
|
|
681→// Belege (receipts/documents) — content-addressed storage (PRD §5.5)
|
|
682→model Beleg {
|
|
683→ id String @id @default(cuid())
|
|
684→ number String @unique // gapless, e.g. BEL-2026-0001
|
|
685→ type String // rechnung-eingang|rechnung-ausgang|quittung|kassenbeleg|bankbeleg|sonstiges
|
|
686→ // content-addressed (SHA-256 hash of the file content)
|
|
687→ fileHash String @unique
|
|
688→ fileName String
|
|
689→ fileType String // application/pdf, image/jpeg, etc.
|
|
690→ fileSizeBytes Int
|
|
691→ // metadata extracted from the document
|
|
692→ vendor String? // Rechnungssteller
|
|
693→ invoiceNumber String? // Rechnungsnummer des Lieferanten
|
|
694→ amountCents Int? // Bruttobetrag auf dem Beleg
|
|
695→ amountNetCents Int? // Netto (falls extrahiert)
|
|
696→ taxBp Int? // USt %
|
|
697→ bookingDate DateTime?
|
|
698→ // linking
|
|
699→ expenseId String? // link to Expense if billable
|
|
700→ invoiceId String? // link to Invoice if outgoing
|
|
701→ // OCR / AI
|
|
702→ ocrText String? // extracted text (optional, via Jina/AI)
|
|
703→ // GoBD
|
|
704→ archivedAt DateTime?
|
|
705→ retentionUntil DateTime? // Aufbewahrung bis
|
|
706→ createdAt DateTime @default(now())
|
|
707→}
|
|
708→
|
|
709→// Pauschalen (flat-rate allowances) — Homeoffice, Kilometer, Verpflegung
|
|
710→model Pauschale {
|
|
711→ id String @id @default(cuid())
|
|
712→ type String // homeoffice|kilometer|verpflegung
|
|
713→ // homeoffice: €4.17/day (2026: €6.92/day up to €1.260/year)
|
|
714→ // kilometer: €0.30/km (car), €0.38/km (car w/ pass), or actual
|
|
715→ // verpflegung: €14 (8-14h), €24 (>14h, same day), €28 (overnight)
|
|
716→ date DateTime
|
|
717→ userId String? // employee
|
|
718→ amountCents Int // computed amount
|
|
719→ // details per type
|
|
720→ days Int? // homeoffice: 1; verpflegung: 1
|
|
721→ km Int? // kilometer: distance
|
|
722→ vehicleType String? // kilometer: auto|rad|fuss
|
|
723→ tripPurpose String? // kilometer: geschäftlich|fortbildung|umzug
|
|
724→ hoursAway Int? // verpflegung: hours away from office
|
|
725→ overnight Boolean @default(false) // verpflegung: overnight stay
|
|
726→ description String?
|
|
727→ projectId String? // link to project if billable
|
|
728→ expenseId String? // link to generated expense
|
|
729→ createdAt DateTime @default(now())
|
|
730→}
|
|
731→
|
|
732→// Anlagenverzeichnis & AfA (asset register + depreciation)
|
|
733→model Anlage {
|
|
734→ id String @id @default(cuid())
|
|
735→ number String @unique // e.g. ANL-2026-0001
|
|
736→ name String // "MacBook Pro 16"
|
|
737→ category String // hardware|software|mobiliar|fahrzeug|immobilie
|
|
738→ // acquisition
|
|
739→ purchaseDate DateTime
|
|
740→ purchaseCostCents Int // Anschaffungskosten (netto)
|
|
741→ taxBp Int @default(1900) // USt bei Anschaffung
|
|
742→ // AfA (depreciation)
|
|
743→ afaMethod String @default("linear") // linear|degressiv
|
|
744→ usefulLifeYears Int @default(3) // Nutzungsdauer (AfA-Tabelle)
|
|
745→ salvageValueCents Int @default(0) // Restwert
|
|
746→ // GWG (Geringwertiger Wirtschaftsgüter ≤ €800 net → Sofortabschreibung)
|
|
747→ isGWG Boolean @default(false) // GWG ≤ €800 → immediate expense
|
|
748→ // Sammelposten (€251-€1000 → Sammelabschreibung über 5 Jahre)
|
|
749→ isSammelposten Boolean @default(false)
|
|
750→ // computed
|
|
751→ annualAfaCents Int @default(0) // jährliche AfA
|
|
752→ accumulatedAfaCents Int @default(0) // kumulierte AfA
|
|
753→ bookValueCents Int @default(0) // Buchwert (purchase - accumulated AfA)
|
|
754→ // status
|
|
755→ status String @default("active") // active|sold|scrapped|fully-depreciated
|
|
756→ soldDate DateTime?
|
|
757→ soldPriceCents Int?
|
|
758→ createdAt DateTime @default(now())
|
|
759→ updatedAt DateTime @updatedAt
|
|
760→}
|
|
761→
|
|
762→// Kassenbuch (cash book for Barzahlungen — § 146 AO, § 22 UStG)
|
|
763→model KassenbuchEntry {
|
|
764→ id String @id @default(cuid())
|
|
765→ number String @unique // gapless, e.g. KAS-2026-0001
|
|
766→ date DateTime
|
|
767→ type String // einnahme|ausgabe
|
|
768→ description String
|
|
769→ amountCents Int // signed: + for einnahme, - for ausgabe
|
|
770→ taxBp Int? // USt % (if applicable)
|
|
771→ taxFree Boolean @default(false) // steuerfrei
|
|
772→ // receipt link
|
|
773→ belegId String? // link to Beleg (Kassenbeleg)
|
|
774→ // balance after this entry (laufender Kassenstand)
|
|
775→ balanceCents Int @default(0)
|
|
776→ // GoBD: Kassenbuch ist unveränderlich (elektronische Aufzeichnung)
|
|
777→ createdAt DateTime @default(now())
|
|
778→}
|
|
779→
|
|
780→
|
|
781→// ---------------------------------------------------------------------------
|
|
782→// SKR03/SKR04 Kontenrahmen + doppelte Buchführung (Haben clone)
|
|
783→// ---------------------------------------------------------------------------
|
|
784→
|
|
785→model Konto {
|
|
786→ id String @id @default(cuid())
|
|
787→ nummer String @unique
|
|
788→ name String
|
|
789→ rahmen String @default("skr04")
|
|
790→ art String // aktiv|passiv|ertrag|aufwand
|
|
791→ parentId String?
|
|
792→ locked Boolean @default(false)
|
|
793→ sollBuchungen Buchungssatz[] @relation("SollKonto")
|
|
794→ habenBuchungen Buchungssatz[] @relation("HabenKonto")
|
|
795→ createdAt DateTime @default(now())
|
|
796→}
|
|
797→
|
|
798→model Buchungssatz {
|
|
799→ id String @id @default(cuid())
|
|
800→ number String @unique
|
|
801→ belegdatum DateTime
|
|
802→ buchungsdatum DateTime @default(now())
|
|
803→ sollKontoId String
|
|
804→ sollKonto Konto @relation("SollKonto", fields: [sollKontoId], references: [id])
|
|
805→ habenKontoId String
|
|
806→ habenKonto Konto @relation("HabenKonto", fields: [habenKontoId], references: [id])
|
|
807→ betragCents Int
|
|
808→ buchungstext String
|
|
809→ belegNummer String?
|
|
810→ steuerBp Int?
|
|
811→ steuerKontoId String?
|
|
812→ festgeschrieben Boolean @default(false)
|
|
813→ festgeschriebenAt DateTime?
|
|
814→ festgeschriebenHash String?
|
|
815→ belegId String?
|
|
816→ invoiceId String?
|
|
817→ expenseId String?
|
|
818→ reverseCharge Boolean @default(false)
|
|
819→ par19UStG Boolean @default(false)
|
|
820→ createdAt DateTime @default(now())
|
|
821→}
|
|
822→
|
|
823→model EuerEntry {
|
|
824→ id String @id @default(cuid())
|
|
825→ jahr Int
|
|
826→ zeile String
|
|
827→ label String
|
|
828→ art String
|
|
829→ betragCents Int
|
|
830→ kontoNummern String?
|
|
831→ createdAt DateTime @default(now())
|
|
832→}
|
|
833→
|
|
834→model Jahreserklaerung {
|
|
835→ id String @id @default(cuid())
|
|
836→ jahr Int
|
|
837→ art String
|
|
838→ status String @default("draft")
|
|
839→ kennzahlen String?
|
|
840→ elsterRef String?
|
|
841→ submittedAt DateTime?
|
|
842→ acceptedAt DateTime?
|
|
843→ notes String?
|
|
844→ createdAt DateTime @default(now())
|
|
845→ updatedAt DateTime @updatedAt
|
|
846→}
|
|
847→
|
|
848→model Jahresarchiv {
|
|
849→ id String @id @default(cuid())
|
|
850→ jahr Int
|
|
851→ prevArchivId String?
|
|
852→ archivHash String @unique
|
|
853→ buchungenCount Int
|
|
854→ belegeCount Int
|
|
855→ rechnungenCount Int
|
|
856→ zipFileName String
|
|
857→ zipSizeBytes Int
|
|
858→ createdDate DateTime @default(now())
|
|
859→ createdBy String
|
|
860→ notes String?
|
|
861→}
|
|
862→
|
|
863→// ---------------------------------------------------------------------------
|
|
864→// Custom Fields (PRD §5.5: "Custom fields: JSONB with a schema table per entity")
|
|
865→// ---------------------------------------------------------------------------
|
|
866→
|
|
867→model CustomFieldDef {
|
|
868→ id String @id @default(cuid())
|
|
869→ entityType String // company|person|deal|project|invoice|ticket|contract
|
|
870→ key String // field key, e.g. "branche", "lead_source_detail"
|
|
871→ label String // display label, e.g. "Branche"
|
|
872→ type String // string|number|boolean|date|select|multiselect
|
|
873→ options String? // JSON array for select/multiselect
|
|
874→ required Boolean @default(false)
|
|
875→ sortOrder Int @default(0)
|
|
876→ values CustomFieldValue[]
|
|
877→ createdAt DateTime @default(now())
|
|
878→ updatedAt DateTime @updatedAt
|
|
879→
|
|
880→ @@unique([entityType, key])
|
|
881→}
|
|
882→
|
|
883→model CustomFieldValue {
|
|
884→ id String @id @default(cuid())
|
|
885→ fieldDefId String
|
|
886→ fieldDef CustomFieldDef @relation(fields: [fieldDefId], references: [id])
|
|
887→ entityType String // denormalized for fast lookup
|
|
888→ entityId String // the entity's ID
|
|
889→ value String? // JSON-encoded value (string, number, bool, array)
|
|
890→ createdAt DateTime @default(now())
|
|
891→ updatedAt DateTime @updatedAt
|
|
892→
|
|
893→ @@unique([entityType, entityId, fieldDefId])
|
|
894→}
|
|
895→ |