generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } // ==================== 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) 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) 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 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") 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 } // ==================== 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) 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 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 } model Meter { id Int @id @default(autoincrement()) customerId Int customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) meterNumber String type MeterType location String? isActive Boolean @default(true) readings MeterReading[] energyDetails EnergyContractDetails[] 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 unit String @default("kWh") notes String? createdAt DateTime @default(now()) } // ==================== 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[] 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") // 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? 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]) 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 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // ==================== 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) bonus Float? previousProviderName String? previousCustomerNumber String? invoices Invoice[] // Rechnungen } model Invoice { id Int @id @default(autoincrement()) energyContractDetailsId Int energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], 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]) } // ==================== 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 // 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? }