1166 lines
45 KiB
Plaintext
1166 lines
45 KiB
Plaintext
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])
|
||
}
|