opencrm/backend/prisma/schema.prisma

1166 lines
45 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ==================== PDF TEMPLATES (Auftragsvorlagen) ====================
model PdfTemplate {
id Int @id @default(autoincrement())
name String @unique // z.B. "EWE Auftragsformular"
description String? // Beschreibung
providerName String? // Zugehöriger Anbieter (z.B. "EWE")
templatePath String // Pfad zur PDF-Vorlage
originalName String // Originaler Dateiname
// Feld-Mapping: JSON-Objekt { pdfFieldName: crmFieldPath }
// z.B. { "Vorname": "customer.firstName", "PLZ": "customer.addresses[0].postalCode" }
fieldMapping String @db.LongText // JSON
// Rufnummern-Konfiguration
phoneFieldPrefix String? // Prefix für Rufnummern-Felder (z.B. "Rufnummer")
maxPhoneFields Int? @default(8) // Max. Rufnummern-Felder im PDF
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== EMAIL LOG ====================
model EmailLog {
id Int @id @default(autoincrement())
// Absender & Empfänger
fromAddress String // Absender-E-Mail
toAddress String // Empfänger-E-Mail
subject String // Betreff
// Versand-Kontext
context String // z.B. "consent-link", "authorization-request", "customer-email"
customerId Int? // Zugehöriger Kunde (falls vorhanden)
triggeredBy String? // Wer hat den Versand ausgelöst (User-Email)
// SMTP-Details
smtpServer String // SMTP-Server
smtpPort Int // SMTP-Port
smtpEncryption String // SSL, STARTTLS, NONE
smtpUser String // SMTP-Benutzername
// Ergebnis
success Boolean // Erfolgreich?
messageId String? // Message-ID aus SMTP-Antwort
errorMessage String? @db.Text // Fehlermeldung bei Fehler
smtpResponse String? @db.Text // SMTP-Server-Antwort
// Zeitstempel
sentAt DateTime @default(now())
@@index([sentAt])
@@index([customerId])
@@index([success])
}
// ==================== APP SETTINGS ====================
model AppSetting {
id Int @id @default(autoincrement())
key String @unique
value String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== USERS & AUTH ====================
model User {
id Int @id @default(autoincrement())
email String @unique
password String
firstName String
lastName String
isActive Boolean @default(true)
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
// Passwort-Reset
passwordResetToken String? @unique
passwordResetExpiresAt DateTime?
// Messaging-Kanäle (für Datenschutz-Link-Versand)
whatsappNumber String?
telegramUsername String?
signalNumber String?
customerId Int? @unique
customer Customer? @relation(fields: [customerId], references: [id])
roles UserRole[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Role {
id Int @id @default(autoincrement())
name String @unique
description String?
permissions RolePermission[]
users UserRole[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Permission {
id Int @id @default(autoincrement())
resource String
action String
roles RolePermission[]
@@unique([resource, action])
}
model RolePermission {
roleId Int
permissionId Int
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@id([roleId, permissionId])
}
model UserRole {
userId Int
roleId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([userId, roleId])
}
// ==================== CUSTOMERS ====================
enum CustomerType {
PRIVATE
BUSINESS
}
model Customer {
id Int @id @default(autoincrement())
customerNumber String @unique
type CustomerType @default(PRIVATE)
salutation String?
firstName String
lastName String
companyName String?
foundingDate DateTime? // Gründungsdatum (für Firmen)
birthDate DateTime?
birthPlace String?
email String?
phone String?
mobile String?
taxNumber String?
businessRegistrationPath String? // PDF-Pfad zur Gewerbeanmeldung
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
commercialRegisterNumber String? // Handelsregisternummer (Text)
privacyPolicyPath String? // PDF-Pfad zur Datenschutzerklärung (für alle Kunden)
consentHash String? @unique // Permanenter Hash für öffentlichen Einwilligungslink /datenschutz/<hash>
notes String? @db.Text
// ===== Portal-Zugangsdaten =====
portalEnabled Boolean @default(false) // Portal aktiviert?
portalEmail String? @unique // Portal-Login E-Mail
portalPasswordHash String? // Gehashtes Passwort (für Login)
portalPasswordEncrypted String? // Verschlüsseltes Passwort (für Anzeige)
portalLastLogin DateTime? // Letzte Anmeldung
// Portal Passwort-Reset
portalPasswordResetToken String? @unique
portalPasswordResetExpiresAt DateTime?
// Portal Session-Invalidation (nach Passwort-Reset / Rechte-Änderung)
portalTokenInvalidatedAt DateTime?
// Geburtstagsmodal: Jahr in dem dem Kunden der Geburtstagsgruß gezeigt wurde (vermeidet mehrfaches Anzeigen)
lastBirthdayGreetingYear Int?
// Anrede-Verhältnis: true = Du (informell), false = Sie (formell, Default)
useInformalAddress Boolean @default(false)
// Automatischer Geburtstagsgruß-Versand
autoBirthdayGreeting Boolean @default(false)
autoBirthdayChannel String? // "email", "whatsapp", "telegram", "signal"
user User?
addresses Address[]
bankCards BankCard[]
identityDocuments IdentityDocument[]
meters Meter[]
stressfreiEmails StressfreiEmail[]
contracts Contract[]
// Vertreter-Beziehungen (Kunde kann für andere Kunden handeln)
representingFor CustomerRepresentative[] @relation("RepresentativeCustomer")
representedBy CustomerRepresentative[] @relation("RepresentedCustomer")
// Vollmachten
authorizationsGiven RepresentativeAuthorization[] @relation("AuthorizationCustomer")
authorizationsReceived RepresentativeAuthorization[] @relation("AuthorizationRepresentative")
// DSGVO: Einwilligungen
consents CustomerConsent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CUSTOMER REPRESENTATIVES ====================
// Vertretungsbeziehung: Ein Kunde kann die Verträge eines anderen Kunden einsehen
// z.B. Sohn (representativeId) kann Verträge der Mutter (customerId) sehen
model CustomerRepresentative {
id Int @id @default(autoincrement())
customerId Int // Der Kunde, dessen Verträge eingesehen werden (z.B. Mutter)
customer Customer @relation("RepresentedCustomer", fields: [customerId], references: [id], onDelete: Cascade)
representativeId Int // Der Kunde, der einsehen darf (z.B. Sohn)
representative Customer @relation("RepresentativeCustomer", fields: [representativeId], references: [id], onDelete: Cascade)
notes String? // Notizen zur Vertretung
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([customerId, representativeId]) // Keine doppelten Einträge
}
// ==================== VOLLMACHTEN ====================
// Vollmacht: Kunde B erteilt Kunde A die Vollmacht, seine Daten einzusehen
// Ohne Vollmacht kann der Vertreter die Verträge des Kunden NICHT sehen
model RepresentativeAuthorization {
id Int @id @default(autoincrement())
customerId Int // Der Kunde, der die Vollmacht erteilt (z.B. Mutter)
customer Customer @relation("AuthorizationCustomer", fields: [customerId], references: [id], onDelete: Cascade)
representativeId Int // Der Vertreter, der Zugriff bekommt (z.B. Sohn)
representative Customer @relation("AuthorizationRepresentative", fields: [representativeId], references: [id], onDelete: Cascade)
isGranted Boolean @default(false) // Vollmacht erteilt?
grantedAt DateTime? // Wann erteilt
withdrawnAt DateTime? // Wann widerrufen
source String? // Quelle: 'portal', 'papier', 'crm-backend'
documentPath String? // PDF-Upload der unterschriebenen Vollmacht
notes String? @db.Text // Notizen
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([customerId, representativeId]) // Eine Vollmacht pro Paar
}
// ==================== ADDRESSES ====================
enum AddressType {
DELIVERY_RESIDENCE
BILLING
}
model Address {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
type AddressType @default(DELIVERY_RESIDENCE)
street String
houseNumber String
postalCode String
city String
country String @default("Deutschland")
isDefault Boolean @default(false)
// Eigentümer (leer = Kunde ist selbst Eigentümer)
ownerCompany String?
ownerFirstName String?
ownerLastName String?
ownerStreet String?
ownerHouseNumber String?
ownerPostalCode String?
ownerCity String?
ownerPhone String?
ownerMobile String?
ownerEmail String?
contractsAsDelivery Contract[] @relation("DeliveryAddress")
contractsAsBilling Contract[] @relation("BillingAddress")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== BANK CARDS ====================
model BankCard {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
accountHolder String
iban String
bic String?
bankName String?
expiryDate DateTime?
documentPath String? // Pfad zur hochgeladenen PDF
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== IDENTITY DOCUMENTS ====================
enum DocumentType {
ID_CARD
PASSPORT
DRIVERS_LICENSE
OTHER
}
model IdentityDocument {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
type DocumentType @default(ID_CARD)
documentNumber String
issuingAuthority String?
issueDate DateTime?
expiryDate DateTime?
documentPath String? // Pfad zur hochgeladenen PDF
isActive Boolean @default(true)
// Führerschein-spezifische Felder
licenseClasses String? // z.B. "B, BE, AM, L" - kommasepariert
licenseIssueDate DateTime? // Datum des Führerscheinerwerbs (Klasse B)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== EMAIL PROVIDER CONFIG (Plesk, cPanel etc.) ====================
enum EmailProviderType {
PLESK
CPANEL
DIRECTADMIN
}
// Verschlüsselungstyp für E-Mail-Verbindungen
enum MailEncryption {
SSL // Implicit SSL/TLS (Ports 465/993) - Verschlüsselung von Anfang an
STARTTLS // STARTTLS (Ports 587/143) - Startet unverschlüsselt, dann Upgrade
NONE // Keine Verschlüsselung (Ports 25/143)
}
model EmailProviderConfig {
id Int @id @default(autoincrement())
name String @unique // z.B. "Plesk Hauptserver"
type EmailProviderType
apiUrl String // API-URL (z.B. https://server.de:8443)
apiKey String? // API-Key (verschlüsselt)
username String? // Benutzername für API
passwordEncrypted String? // Passwort (verschlüsselt)
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
// IMAP/SMTP-Server für E-Mail-Client (optional, default: mail.{domain})
imapServer String? // z.B. "mail.stressfrei-wechseln.de"
imapPort Int? @default(993)
smtpServer String?
smtpPort Int? @default(465)
// Verschlüsselungs-Einstellungen
imapEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
// System-E-Mail für automatisierte Nachrichten (z.B. DSGVO Consent-Links)
systemEmailAddress String? // z.B. "info@stressfrei-wechseln.de"
systemEmailPasswordEncrypted String? // Passwort (verschlüsselt)
// Label für Kunden-E-Mail-Adressen in der UI (z.B. "Stressfrei-Wechseln")
// Wenn leer, wird automatisch aus der Domain abgeleitet (z.B. "stressfrei-wechseln.de" → "Stressfrei-Wechseln")
customerEmailLabel String?
isActive Boolean @default(true)
isDefault Boolean @default(false) // Standard-Provider
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
model StressfreiEmail {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
email String // Die Weiterleitungs-E-Mail-Adresse
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
notes String? @db.Text // Optionale Notizen
isActive Boolean @default(true)
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
provisionedAt DateTime? // Wann wurde provisioniert?
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
hasMailbox Boolean @default(false) // Hat echte Mailbox (nicht nur Weiterleitung)?
emailPasswordEncrypted String? // Verschlüsseltes Mailbox-Passwort (AES-256-GCM)
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
cachedEmails CachedEmail[] // Gecachte E-Mails aus dieser Mailbox
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CACHED EMAILS (E-Mail-Client) ====================
enum EmailFolder {
INBOX
SENT
}
model CachedEmail {
id Int @id @default(autoincrement())
stressfreiEmailId Int
stressfreiEmail StressfreiEmail @relation(fields: [stressfreiEmailId], references: [id], onDelete: Cascade)
// Ordner (Posteingang oder Gesendet)
folder EmailFolder @default(INBOX)
// IMAP-Identifikation
messageId String // RFC 5322 Message-ID
uid Int // IMAP UID (für Synchronisierung, bei SENT = 0)
// E-Mail-Metadaten
subject String?
fromAddress String
fromName String?
toAddresses String @db.Text // JSON Array
ccAddresses String? @db.Text // JSON Array
receivedAt DateTime
// Inhalt
textBody String? @db.LongText
htmlBody String? @db.LongText
hasAttachments Boolean @default(false)
attachmentNames String? @db.Text // JSON Array
// Vertragszuordnung
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: SetNull)
assignedAt DateTime?
assignedBy Int? // User ID der die Zuordnung gemacht hat
isAutoAssigned Boolean @default(false) // true = automatisch beim Senden aus Vertrag
// Flags
isRead Boolean @default(false)
isStarred Boolean @default(false)
// Papierkorb
isDeleted Boolean @default(false) // Im Papierkorb?
deletedAt DateTime? // Wann gelöscht?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([stressfreiEmailId, messageId, folder]) // Folder hinzugefügt: gleiche MessageID kann in INBOX und SENT existieren
@@index([contractId])
@@index([stressfreiEmailId, folder, receivedAt])
@@index([stressfreiEmailId, isDeleted]) // Für Papierkorb-Abfragen
}
// ==================== METERS (Energy) ====================
enum MeterType {
ELECTRICITY
GAS
}
enum MeterTariffModel {
SINGLE // Eintarifzähler (Standard)
DUAL // Zweitarifzähler (HT/NT)
}
model Meter {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
meterNumber String
type MeterType
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
location String?
isActive Boolean @default(true)
readings MeterReading[]
energyDetails EnergyContractDetails[]
contractMeters ContractMeter[] @relation("ContractMeters")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MeterReading {
id Int @id @default(autoincrement())
meterId Int
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
readingDate DateTime
value Float // Bei Eintarif: Gesamtwert. Bei Zweitarif: HT-Wert
valueNt Float? // Nur bei Zweitarif: NT-Wert (Niedertarif)
unit String @default("kWh")
notes String?
// Meldung & Übertragung
reportedBy String? // Wer hat gemeldet? (E-Mail des Portal-Kunden oder Mitarbeiter)
status MeterReadingStatus @default(RECORDED)
transferredAt DateTime? // Wann wurde der Stand an den Anbieter übertragen?
transferredBy String? // Wer hat übertragen?
createdAt DateTime @default(now())
}
enum MeterReadingStatus {
RECORDED // Erfasst (vom Mitarbeiter)
REPORTED // Vom Kunden gemeldet (Portal)
TRANSFERRED // An Anbieter übertragen
}
// ==================== SALES PLATFORMS ====================
model SalesPlatform {
id Int @id @default(autoincrement())
name String @unique
contactInfo String? @db.Text
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CANCELLATION PERIODS ====================
model CancellationPeriod {
id Int @id @default(autoincrement())
code String @unique // z.B. "14T", "1M", "3M", "12M", "1J"
description String // z.B. "14 Tage", "1 Monat", "3 Monate"
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CONTRACT DURATIONS ====================
model ContractDuration {
id Int @id @default(autoincrement())
code String @unique // z.B. "12M", "24M", "1J", "2J"
description String // z.B. "12 Monate", "24 Monate", "1 Jahr"
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== PROVIDERS (Anbieter) ====================
model Provider {
id Int @id @default(autoincrement())
name String @unique // Anbietername
portalUrl String? // Kundenkontourl (Login-Seite)
usernameFieldName String? // Benutzernamefeld (z.B. "email", "username")
passwordFieldName String? // Kennwortfeld (z.B. "password", "pwd")
isActive Boolean @default(true)
tariffs Tariff[]
contracts Contract[]
previousContracts Contract[] @relation("PreviousProvider") // Verträge wo dieser Provider Altanbieter ist
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== TARIFFS (Tarife) ====================
model Tariff {
id Int @id @default(autoincrement())
providerId Int
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
name String // Tarifname
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([providerId, name]) // Eindeutiger Tarif pro Anbieter
}
// ==================== CONTRACT CATEGORIES ====================
model ContractCategory {
id Int @id @default(autoincrement())
code String @unique // Technischer Code (z.B. ELECTRICITY, GAS)
name String // Anzeigename (z.B. Strom, Gas)
icon String? // Icon-Name für UI (z.B. "Zap", "Flame")
color String? // Farbe für UI (z.B. "#FFC107")
sortOrder Int @default(0)
isActive Boolean @default(true)
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CONTRACTS ====================
// Legacy Enum - wird durch ContractCategory ersetzt
enum ContractType {
ELECTRICITY
GAS
DSL
CABLE
FIBER
MOBILE
TV
CAR_INSURANCE
}
enum ContractStatus {
DRAFT
PENDING
ACTIVE
CANCELLED
EXPIRED
DEACTIVATED
}
model Contract {
id Int @id @default(autoincrement())
contractNumber String @unique
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
type ContractType
status ContractStatus @default(DRAFT)
// Neue konfigurierbare Kategorie (ersetzt langfristig das type-Enum)
contractCategoryId Int?
contractCategory ContractCategory? @relation(fields: [contractCategoryId], references: [id])
// Lieferadresse
addressId Int?
address Address? @relation("DeliveryAddress", fields: [addressId], references: [id])
// Rechnungsadresse (falls leer, wird Lieferadresse verwendet)
billingAddressId Int?
billingAddress Address? @relation("BillingAddress", fields: [billingAddressId], references: [id])
bankCardId Int?
bankCard BankCard? @relation(fields: [bankCardId], references: [id])
identityDocumentId Int?
identityDocument IdentityDocument? @relation(fields: [identityDocumentId], references: [id])
salesPlatformId Int?
salesPlatform SalesPlatform? @relation(fields: [salesPlatformId], references: [id])
cancellationPeriodId Int?
cancellationPeriod CancellationPeriod? @relation(fields: [cancellationPeriodId], references: [id])
contractDurationId Int?
contractDuration ContractDuration? @relation(fields: [contractDurationId], references: [id])
previousContractId Int? @unique
previousContract Contract? @relation("ContractHistory", fields: [previousContractId], references: [id])
followUpContract Contract? @relation("ContractHistory")
// Altanbieter-Daten (nur wenn kein Vorgängervertrag existiert)
previousProviderId Int?
previousProvider Provider? @relation("PreviousProvider", fields: [previousProviderId], references: [id])
previousCustomerNumber String? // Kundennummer beim Altanbieter
previousContractNumber String? // Vertragsnummer beim Altanbieter
// Anbieter & Tarif (neue Verknüpfung)
providerId Int?
provider Provider? @relation(fields: [providerId], references: [id])
tariffId Int?
tariff Tariff? @relation(fields: [tariffId], references: [id])
// Legacy-Felder (für Abwärtskompatibilität)
providerName String?
tariffName String?
customerNumberAtProvider String?
contractNumberAtProvider String? // Vertragsnummer beim Anbieter
priceFirst12Months String? // Preis erste 12 Monate
priceFrom13Months String? // Preis ab 13. Monat
priceAfter24Months String? // Preis nach 24 Monaten
startDate DateTime?
endDate DateTime? // Wird aus startDate + contractDuration berechnet
commission Float?
// Kündigungsdokumente
cancellationLetterPath String? // Kündigungsschreiben PDF
cancellationConfirmationPath String? // Kündigungsbestätigung PDF
cancellationLetterOptionsPath String? // Kündigungsschreiben Optionen PDF
cancellationConfirmationOptionsPath String? // Kündigungsbestätigung Optionen PDF
// Kündigungsdaten
cancellationConfirmationDate DateTime? // Kündigungsbestätigungsdatum
cancellationConfirmationOptionsDate DateTime? // Kündigungsbestätigungsoptionendatum
wasSpecialCancellation Boolean @default(false) // Wurde sondergekündigt?
portalUsername String?
portalPasswordEncrypted String?
// Stressfrei-Wechseln E-Mail als Benutzername (Alternative zu portalUsername)
stressfreiEmailId Int?
stressfreiEmail StressfreiEmail? @relation(fields: [stressfreiEmailId], references: [id])
// Snooze: Vertrag zurückstellen bis Datum (für Cockpit)
nextReviewDate DateTime? // Erneute Prüfung am
notes String? @db.Text
energyDetails EnergyContractDetails?
internetDetails InternetContractDetails?
mobileDetails MobileContractDetails?
tvDetails TvContractDetails?
carInsuranceDetails CarInsuranceDetails?
tasks ContractTask[]
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
historyEntries ContractHistoryEntry[]
documents ContractDocument[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== CONTRACT DOCUMENTS ====================
model ContractDocument {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
documentType String // Auftragsformular, Lieferbestätigung, etc.
documentPath String // Dateipfad
originalName String // Originaler Dateiname
notes String? @db.Text
uploadedBy String? // Wer hat hochgeladen
createdAt DateTime @default(now())
@@index([contractId])
}
// ==================== CONTRACT HISTORY ====================
model ContractHistoryEntry {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
title String // Kurzbeschreibung (z.B. "Folgevertrag erstellt", "kWh auf 18000 erhöht")
description String? @db.Text // Längere Beschreibung (optional)
isAutomatic Boolean @default(false) // true = automatisch erstellt, false = manuell
createdBy String // E-Mail des Erstellers
createdAt DateTime @default(now())
}
// ==================== CONTRACT TASKS ====================
enum ContractTaskStatus {
OPEN
COMPLETED
}
model ContractTask {
id Int @id @default(autoincrement())
contractId Int
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
title String
description String? @db.Text
status ContractTaskStatus @default(OPEN)
visibleInPortal Boolean @default(false)
createdBy String? // Name des Erstellers
completedAt DateTime?
subtasks ContractTaskSubtask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ContractTaskSubtask {
id Int @id @default(autoincrement())
taskId Int
task ContractTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
title String
status ContractTaskStatus @default(OPEN)
createdBy String?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== ENERGY CONTRACT DETAILS ====================
enum InvoiceType {
INTERIM // Zwischenrechnung
FINAL // Schlussrechnung
NOT_AVAILABLE // Rechnung nicht mehr zu bekommen
}
model EnergyContractDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
meterId Int?
meter Meter? @relation(fields: [meterId], references: [id])
maloId String? // Marktlokations-ID
annualConsumption Float? // kWh für Strom, m³ für Gas
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
basePrice Float? // €/Monat
unitPrice Float? // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
unitPriceNt Float? // €/kWh NT-Preis (nur bei Zweitarifzähler)
bonus Float?
previousProviderName String?
previousCustomerNumber String?
invoices Invoice[] // Rechnungen
contractMeters ContractMeter[] // Zähler-Zuordnungen (inkl. Folgezähler)
}
model ContractMeter {
id Int @id @default(autoincrement())
energyContractDetailsId Int
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
meterId Int
meter Meter @relation("ContractMeters", fields: [meterId], references: [id])
position Int @default(0) // 0 = Original, 1 = erster Folgezähler, etc.
installedAt DateTime? // Ab wann wird dieser Zähler am Vertrag genutzt?
removedAt DateTime? // Wann wurde der Zähler gewechselt? (null = aktuell)
finalReading Float? // Letzter Stand vor dem Wechsel
createdAt DateTime @default(now())
@@unique([energyContractDetailsId, meterId])
@@index([energyContractDetailsId])
}
model Invoice {
id Int @id @default(autoincrement())
energyContractDetailsId Int?
energyContractDetails EnergyContractDetails? @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
contractId Int?
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
invoiceDate DateTime
invoiceType InvoiceType
documentPath String? // Pflicht, außer bei NOT_AVAILABLE
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([energyContractDetailsId])
@@index([contractId])
}
// ==================== INTERNET CONTRACT DETAILS ====================
model InternetContractDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
downloadSpeed Int?
uploadSpeed Int?
routerModel String?
routerSerialNumber String?
installationDate DateTime?
// Internet-Zugangsdaten
internetUsername String?
internetPasswordEncrypted String? // Verschlüsselt gespeichert
// Objekt & Lage
propertyType String? // Objekttyp (Mehrparteienhaus, Freistehendes Haus, etc.)
propertyLocation String? // Lage (Erdgeschoss, OG1, etc.)
connectionLocation String? // Lage des Anschlusses (Flur, HWR, etc.)
// Glasfaser-spezifisch
homeId String?
// Vodafone DSL/Kabel spezifisch
activationCode String?
phoneNumbers PhoneNumber[]
}
model PhoneNumber {
id Int @id @default(autoincrement())
internetContractDetailsId Int
internetDetails InternetContractDetails @relation(fields: [internetContractDetailsId], references: [id], onDelete: Cascade)
phoneNumber String
isMain Boolean @default(false)
// SIP-Zugangsdaten
sipUsername String?
sipPasswordEncrypted String? // Verschlüsselt gespeichert
sipServer String?
}
// ==================== MOBILE CONTRACT DETAILS ====================
model MobileContractDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
requiresMultisim Boolean @default(false) // Multisim erforderlich?
dataVolume Float?
includedMinutes Int?
includedSMS Int?
deviceModel String?
deviceImei String?
simCards SimCard[]
// Legacy-Felder (für Abwärtskompatibilität, werden durch simCards ersetzt)
phoneNumber String?
simCardNumber String?
}
model SimCard {
id Int @id @default(autoincrement())
mobileDetailsId Int
mobileDetails MobileContractDetails @relation(fields: [mobileDetailsId], references: [id], onDelete: Cascade)
phoneNumber String? // Rufnummer
simCardNumber String? // SIM-Kartennummer
pin String? // PIN (verschlüsselt gespeichert)
puk String? // PUK (verschlüsselt gespeichert)
isMultisim Boolean @default(false) // Ist dies eine Multisim-Karte?
isMain Boolean @default(false) // Ist dies die Hauptkarte?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ==================== TV CONTRACT DETAILS ====================
model TvContractDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
receiverModel String?
smartcardNumber String?
package String?
}
// ==================== CAR INSURANCE DETAILS ====================
enum InsuranceType {
LIABILITY
PARTIAL
FULL
}
model CarInsuranceDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
licensePlate String?
hsn String?
tsn String?
vin String?
vehicleType String?
firstRegistration DateTime?
noClaimsClass String?
insuranceType InsuranceType @default(LIABILITY)
deductiblePartial Float?
deductibleFull Float?
policyNumber String?
previousInsurer String?
}
// ==================== AUDIT LOGGING (DSGVO) ====================
enum AuditAction {
CREATE
READ
UPDATE
DELETE
EXPORT // DSGVO-Datenexport
ANONYMIZE // Recht auf Vergessenwerden
LOGIN
LOGOUT
LOGIN_FAILED
}
enum AuditSensitivity {
LOW // Einstellungen, Plattformen
MEDIUM // Verträge, Tarife
HIGH // Kundendaten, Bankdaten
CRITICAL // Authentifizierung, Ausweisdokumente
}
model AuditLog {
id Int @id @default(autoincrement())
// Wer
userId Int? // Staff User (null bei Kundenportal/System)
userEmail String
userRole String? @db.Text // Rolle zum Zeitpunkt der Aktion
customerId Int? // Bei Kundenportal-Zugriff
isCustomerPortal Boolean @default(false)
// Was
action AuditAction
sensitivity AuditSensitivity @default(MEDIUM)
// Welche Ressource
resourceType String // Prisma Model Name
resourceId String? // ID des Datensatzes
resourceLabel String? // Lesbare Bezeichnung
// Kontext
endpoint String // API-Pfad
httpMethod String // GET, POST, PUT, DELETE
ipAddress String
userAgent String? @db.Text
// Änderungen (JSON, bei sensiblen Daten verschlüsselt)
changesBefore String? @db.LongText
changesAfter String? @db.LongText
changesEncrypted Boolean @default(false)
// DSGVO
dataSubjectId Int? // Betroffene Person (für Reports)
legalBasis String? // Rechtsgrundlage
// Status
success Boolean @default(true)
errorMessage String? @db.Text
durationMs Int?
// Unveränderlichkeit (Hash-Kette)
createdAt DateTime @default(now())
hash String? // SHA-256 Hash des Eintrags
previousHash String? // Hash des vorherigen Eintrags
@@index([userId])
@@index([customerId])
@@index([resourceType, resourceId])
@@index([dataSubjectId])
@@index([action])
@@index([createdAt])
@@index([sensitivity])
}
// ==================== CONSENT MANAGEMENT (DSGVO) ====================
enum ConsentType {
DATA_PROCESSING // Grundlegende Datenverarbeitung
MARKETING_EMAIL // E-Mail-Marketing
MARKETING_PHONE // Telefon-Marketing
DATA_SHARING_PARTNER // Weitergabe an Partner
}
enum ConsentStatus {
GRANTED
WITHDRAWN
PENDING
}
model CustomerConsent {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
consentType ConsentType
status ConsentStatus @default(PENDING)
grantedAt DateTime?
withdrawnAt DateTime?
source String? // "portal", "telefon", "papier", "email"
documentPath String? // Unterschriebenes Dokument
version String? // Version der Datenschutzerklärung
ipAddress String?
createdBy String // User der die Einwilligung erfasst hat
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([customerId, consentType])
@@index([customerId])
@@index([consentType])
@@index([status])
}
// ==================== DATA DELETION REQUESTS (DSGVO) ====================
enum DeletionRequestStatus {
PENDING // Anfrage eingegangen
IN_PROGRESS // Wird bearbeitet
COMPLETED // Abgeschlossen
PARTIALLY_COMPLETED // Teildaten behalten (rechtliche Gründe)
REJECTED // Abgelehnt
}
model DataDeletionRequest {
id Int @id @default(autoincrement())
customerId Int
status DeletionRequestStatus @default(PENDING)
requestedAt DateTime @default(now())
requestSource String // "email", "portal", "brief"
requestedBy String // Wer hat angefragt
processedAt DateTime?
processedBy String? // Mitarbeiter der bearbeitet hat
deletedData String? @db.LongText // JSON: Was wurde gelöscht
retainedData String? @db.LongText // JSON: Was wurde behalten + Grund
retentionReason String? @db.Text // Begründung für Aufbewahrung
proofDocument String? // Pfad zum Löschnachweis-PDF
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([status])
@@index([requestedAt])
}
// ==================== AUDIT RETENTION POLICIES ====================
model AuditRetentionPolicy {
id Int @id @default(autoincrement())
resourceType String // "*" für Standard, oder spezifischer Model-Name
sensitivity AuditSensitivity?
retentionDays Int // Aufbewahrungsfrist in Tagen (z.B. 3650 = 10 Jahre)
description String?
legalBasis String? // Gesetzliche Grundlage (z.B. "AO §147", "HGB §257")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([resourceType, sensitivity])
}
// ==================== SECURITY MONITORING ====================
// Sicherheitsrelevante Events für Realtime-Alerting + Forensik.
// Im Gegensatz zum AuditLog (forensisch, hash-gekettet) ist das hier
// optimiert für schnelles Filtern + Alerting (nicht-tamper-evident, dafür
// effizient querybar). Threshold-Detection läuft per Cron.
enum SecurityEventType {
LOGIN_FAILED // falsches Passwort / unbekannter User
LOGIN_SUCCESS // erfolgreicher Login (informativ)
RATE_LIMIT_HIT // express-rate-limit hat zugeschlagen
ACCESS_DENIED // 403 von canAccess* (versuchter IDOR)
SSRF_BLOCKED // ssrfGuard hat geblockte Adresse abgefangen
PASSWORD_RESET_REQUEST // Reset-Mail angefordert
PASSWORD_RESET_CONFIRM // Reset abgeschlossen
LOGOUT // expliziter Logout
TOKEN_REJECTED // ungültiger / abgelaufener / manipulierter JWT
PERMISSION_CHANGED // Admin hat Rolle/Permission geändert
SUSPICIOUS // generischer Catch-All
}
enum SecuritySeverity {
INFO // Login-Success, Logout
LOW // Einzelner failed Login, einzelner 403
MEDIUM // Rate-Limit-Hit, mehrere 403er
HIGH // SSRF-Block, JWT-Manipulation
CRITICAL // Threshold überschritten (>10 failed login/h, >5 403/min)
}
model SecurityEvent {
id Int @id @default(autoincrement())
type SecurityEventType
severity SecuritySeverity
message String @db.Text
ipAddress String?
userId Int? // Mitarbeiter (falls eingeloggt)
customerId Int? // Portal-Kunde (falls eingeloggt)
userEmail String? // beste Schätzung auch bei nicht eingeloggt
endpoint String? // betroffener Endpoint
details Json? // strukturierte Zusatzinfo
alerted Boolean @default(false) // schon per Email versendet?
alertedAt DateTime?
createdAt DateTime @default(now())
@@index([type, createdAt])
@@index([severity, createdAt])
@@index([ipAddress, createdAt])
@@index([alerted, severity])
}