import { PrismaClient, CustomerType, ContractStatus } from '@prisma/client'; import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js'; import fs from 'fs'; import path from 'path'; const prisma = new PrismaClient(); // Helper zum Löschen von Dateien function deleteFileIfExists(filePath: string | null) { if (!filePath) return; const absolutePath = path.join(process.cwd(), filePath); if (fs.existsSync(absolutePath)) { try { fs.unlinkSync(absolutePath); } catch (error) { console.error('Fehler beim Löschen der Datei:', absolutePath, error); } } } export interface CustomerFilters { search?: string; type?: CustomerType; page?: number; limit?: number; } export async function getAllCustomers(filters: CustomerFilters) { const { search, type, page = 1, limit = 20 } = filters; const { skip, take } = paginate(page, limit); const where: Record = {}; if (type) { where.type = type; } if (search) { where.OR = [ { firstName: { contains: search } }, { lastName: { contains: search } }, { companyName: { contains: search } }, { email: { contains: search } }, { customerNumber: { contains: search } }, ]; } const [customers, total] = await Promise.all([ prisma.customer.findMany({ where, skip, take, orderBy: { createdAt: 'desc' }, include: { addresses: { where: { isDefault: true }, take: 1 }, _count: { select: { contracts: true }, }, }, }), prisma.customer.count({ where }), ]); return { customers, pagination: buildPaginationResponse(page, limit, total), }; } export async function getCustomerById(id: number) { return prisma.customer.findUnique({ where: { id }, include: { addresses: true, bankCards: { orderBy: { isActive: 'desc' } }, identityDocuments: { orderBy: { isActive: 'desc' } }, meters: { orderBy: { isActive: 'desc' }, include: { readings: { orderBy: { readingDate: 'desc' }, }, }, }, stressfreiEmails: { orderBy: { isActive: 'desc' } }, contracts: { where: { // Deaktivierte Verträge ausblenden status: { not: ContractStatus.DEACTIVATED }, }, orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }], include: { address: true, salesPlatform: true, }, }, }, }); } export async function getCustomersByIds(ids: number[]) { return prisma.customer.findMany({ where: { id: { in: ids } }, select: { id: true, portalEmail: true, }, }); } export async function createCustomer(data: { type?: CustomerType; salutation?: string; firstName: string; lastName: string; companyName?: string; birthDate?: Date; birthPlace?: string; email?: string; phone?: string; mobile?: string; taxNumber?: string; businessRegistration?: string; commercialRegister?: string; notes?: string; }) { return prisma.customer.create({ data: { ...data, customerNumber: generateCustomerNumber(), }, }); } export async function updateCustomer( id: number, data: { type?: CustomerType; salutation?: string; firstName?: string; lastName?: string; companyName?: string; birthDate?: Date; birthPlace?: string; email?: string; phone?: string; mobile?: string; taxNumber?: string; businessRegistration?: string; commercialRegister?: string; notes?: string; } ) { return prisma.customer.update({ where: { id }, data, }); } export async function deleteCustomer(id: number) { // Vor dem Löschen: Alle Dokumente (Dateien) des Kunden löschen const customer = await prisma.customer.findUnique({ where: { id }, select: { businessRegistrationPath: true, commercialRegisterPath: true, privacyPolicyPath: true }, }); const bankCards = await prisma.bankCard.findMany({ where: { customerId: id }, select: { documentPath: true }, }); const identityDocs = await prisma.identityDocument.findMany({ where: { customerId: id }, select: { documentPath: true }, }); // Kundendokumente löschen if (customer) { deleteFileIfExists(customer.businessRegistrationPath); deleteFileIfExists(customer.commercialRegisterPath); deleteFileIfExists(customer.privacyPolicyPath); } // Bankkarten- und Ausweisdokumente löschen for (const card of bankCards) { deleteFileIfExists(card.documentPath); } for (const doc of identityDocs) { deleteFileIfExists(doc.documentPath); } // Jetzt DB-Eintrag löschen (Cascade löscht die verknüpften Einträge) return prisma.customer.delete({ where: { id }, }); } // Address operations export async function getCustomerAddresses(customerId: number) { return prisma.address.findMany({ where: { customerId }, orderBy: [{ isDefault: 'desc' }, { createdAt: 'desc' }], }); } export async function createAddress( customerId: number, data: { type: 'DELIVERY_RESIDENCE' | 'BILLING'; street: string; houseNumber: string; postalCode: string; city: string; country?: string; isDefault?: boolean; } ) { // If this is set as default, unset other defaults of same type if (data.isDefault) { await prisma.address.updateMany({ where: { customerId, type: data.type }, data: { isDefault: false }, }); } return prisma.address.create({ data: { customerId, ...data, }, }); } export async function updateAddress( id: number, data: { type?: 'DELIVERY_RESIDENCE' | 'BILLING'; street?: string; houseNumber?: string; postalCode?: string; city?: string; country?: string; isDefault?: boolean; } ) { const address = await prisma.address.findUnique({ where: { id } }); if (!address) throw new Error('Adresse nicht gefunden'); if (data.isDefault) { await prisma.address.updateMany({ where: { customerId: address.customerId, type: data.type || address.type, id: { not: id }, }, data: { isDefault: false }, }); } return prisma.address.update({ where: { id }, data, }); } export async function deleteAddress(id: number) { return prisma.address.delete({ where: { id } }); } // Bank card operations export async function getCustomerBankCards( customerId: number, showInactive: boolean = false ) { const where: Record = { customerId }; if (!showInactive) { where.isActive = true; } return prisma.bankCard.findMany({ where, orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }], }); } export async function createBankCard( customerId: number, data: { accountHolder: string; iban: string; bic?: string; bankName?: string; expiryDate?: Date; } ) { return prisma.bankCard.create({ data: { customerId, ...data, isActive: true, }, }); } export async function updateBankCard( id: number, data: { accountHolder?: string; iban?: string; bic?: string; bankName?: string; expiryDate?: Date; isActive?: boolean; } ) { return prisma.bankCard.update({ where: { id }, data, }); } export async function deleteBankCard(id: number) { // Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen const bankCard = await prisma.bankCard.findUnique({ where: { id } }); if (bankCard?.documentPath) { deleteFileIfExists(bankCard.documentPath); } return prisma.bankCard.delete({ where: { id } }); } // Identity document operations export async function getCustomerDocuments( customerId: number, showInactive: boolean = false ) { const where: Record = { customerId }; if (!showInactive) { where.isActive = true; } return prisma.identityDocument.findMany({ where, orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }], }); } export async function createDocument( customerId: number, data: { type: 'ID_CARD' | 'PASSPORT' | 'DRIVERS_LICENSE' | 'OTHER'; documentNumber: string; issuingAuthority?: string; issueDate?: Date; expiryDate?: Date; licenseClasses?: string; licenseIssueDate?: Date; } ) { return prisma.identityDocument.create({ data: { customerId, ...data, isActive: true, }, }); } export async function updateDocument( id: number, data: { type?: 'ID_CARD' | 'PASSPORT' | 'DRIVERS_LICENSE' | 'OTHER'; documentNumber?: string; issuingAuthority?: string; issueDate?: Date; expiryDate?: Date; licenseClasses?: string; licenseIssueDate?: Date; isActive?: boolean; } ) { return prisma.identityDocument.update({ where: { id }, data, }); } export async function deleteDocument(id: number) { // Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen const document = await prisma.identityDocument.findUnique({ where: { id } }); if (document?.documentPath) { deleteFileIfExists(document.documentPath); } return prisma.identityDocument.delete({ where: { id } }); } // Meter operations export async function getCustomerMeters( customerId: number, showInactive: boolean = false ) { const where: Record = { customerId }; if (!showInactive) { where.isActive = true; } return prisma.meter.findMany({ where, include: { readings: { orderBy: { readingDate: 'desc' }, take: 5, }, }, orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }], }); } export async function createMeter( customerId: number, data: { meterNumber: string; type: 'ELECTRICITY' | 'GAS'; location?: string; } ) { return prisma.meter.create({ data: { customerId, ...data, isActive: true, }, }); } export async function updateMeter( id: number, data: { meterNumber?: string; type?: 'ELECTRICITY' | 'GAS'; location?: string; isActive?: boolean; } ) { return prisma.meter.update({ where: { id }, data, }); } export async function deleteMeter(id: number) { // Prüfen ob der Zähler noch an Verträgen hängt const linkedContracts = await prisma.contractMeter.findMany({ where: { meterId: id }, include: { energyContractDetails: { include: { contract: { select: { contractNumber: true } } } } }, }); if (linkedContracts.length > 0) { const contractNumbers = linkedContracts .map(cm => cm.energyContractDetails.contract.contractNumber) .join(', '); throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`); } // Auch direkte meterId-Referenz auf EnergyContractDetails prüfen const directLinks = await prisma.energyContractDetails.findMany({ where: { meterId: id }, include: { contract: { select: { contractNumber: true } } }, }); if (directLinks.length > 0) { const contractNumbers = directLinks.map(d => d.contract.contractNumber).join(', '); throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`); } return prisma.meter.delete({ where: { id } }); } export async function addMeterReading( meterId: number, data: { readingDate: Date; value: number; valueNt?: number; unit?: string; notes?: string; } ) { // Validierung: Zählerstand muss monoton steigend sein await validateReadingValue(meterId, data.readingDate, data.value, undefined, 'HT'); if (data.valueNt !== undefined) { await validateReadingValue(meterId, data.readingDate, data.valueNt, undefined, 'NT'); } return prisma.meterReading.create({ data: { meterId, readingDate: data.readingDate, value: data.value, valueNt: data.valueNt, unit: data.unit, notes: data.notes, }, }); } export async function getMeterReadings(meterId: number) { return prisma.meterReading.findMany({ where: { meterId }, orderBy: { readingDate: 'desc' }, }); } export async function updateMeterReading( meterId: number, readingId: number, data: { readingDate?: Date; value?: number; valueNt?: number | null; unit?: string; notes?: string; } ) { // Verify the reading belongs to the meter const reading = await prisma.meterReading.findFirst({ where: { id: readingId, meterId }, }); if (!reading) { throw new Error('Zählerstand nicht gefunden'); } // Validierung bei Wertänderung if (data.value !== undefined || data.readingDate !== undefined) { await validateReadingValue( meterId, data.readingDate || reading.readingDate, data.value ?? reading.value, readingId, 'HT' ); } if (data.valueNt !== undefined || data.readingDate !== undefined) { const ntVal = data.valueNt ?? reading.valueNt; if (ntVal !== undefined && ntVal !== null) { await validateReadingValue( meterId, data.readingDate || reading.readingDate, ntVal, readingId, 'NT' ); } } return prisma.meterReading.update({ where: { id: readingId }, data, }); } /** * Validiert, dass ein Zählerstand monoton steigend ist. * tariffLabel: 'HT' für Hochtarif/Eintarif, 'NT' für Niedertarif */ async function validateReadingValue(meterId: number, readingDate: Date, value: number, excludeReadingId?: number, tariffLabel: 'HT' | 'NT' = 'HT') { const existing = await prisma.meterReading.findMany({ where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) }, orderBy: { readingDate: 'asc' }, }); const fmtDate = (d: Date) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); const fmtVal = (v: number) => v.toLocaleString('de-DE'); const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand'; // Vergleichswert aus bestehendem Reading extrahieren const getVal = (r: typeof existing[0]) => tariffLabel === 'NT' ? (r.valueNt ?? 0) : r.value; // Stand vor dem neuen Datum const before = [...existing].filter(r => r.readingDate <= readingDate).pop(); if (before && value < getVal(before)) { throw new Error(`${label} (${fmtVal(value)}) darf nicht kleiner sein als der Stand vom ${fmtDate(before.readingDate)} (${fmtVal(getVal(before))})`); } // Stand nach dem neuen Datum const after = existing.find(r => r.readingDate > readingDate); if (after && value > getVal(after)) { throw new Error(`${label} (${fmtVal(value)}) darf nicht größer sein als der spätere Stand vom ${fmtDate(after.readingDate)} (${fmtVal(getVal(after))})`); } } export async function deleteMeterReading(meterId: number, readingId: number) { // Verify the reading belongs to the meter const reading = await prisma.meterReading.findFirst({ where: { id: readingId, meterId }, }); if (!reading) { throw new Error('Zählerstand nicht gefunden'); } return prisma.meterReading.delete({ where: { id: readingId }, }); } // ==================== PORTAL SETTINGS ==================== export async function updatePortalSettings( customerId: number, data: { portalEnabled?: boolean; portalEmail?: string | null; } ) { // Wenn Portal deaktiviert wird, Passwort-Hash nicht löschen (für spätere Reaktivierung) return prisma.customer.update({ where: { id: customerId }, data: { portalEnabled: data.portalEnabled, portalEmail: data.portalEmail, }, select: { id: true, portalEnabled: true, portalEmail: true, portalLastLogin: true, }, }); } export async function getPortalSettings(customerId: number) { return prisma.customer.findUnique({ where: { id: customerId }, select: { id: true, portalEnabled: true, portalEmail: true, portalLastLogin: true, portalPasswordHash: true, // Nur um zu prüfen ob Passwort gesetzt (wird als boolean zurückgegeben) }, }); } // ==================== REPRESENTATIVE MANAGEMENT ==================== export async function getCustomerRepresentatives(customerId: number) { // Holt alle Kunden, die der angegebene Kunde vertreten kann (dieser ist der Vertreter) return prisma.customerRepresentative.findMany({ where: { representativeId: customerId, isActive: true }, include: { customer: { select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, type: true, }, }, }, orderBy: { createdAt: 'desc' }, }); } export async function getRepresentedByList(customerId: number) { // Holt alle Kunden, die den angegebenen Kunden vertreten können return prisma.customerRepresentative.findMany({ where: { customerId: customerId, isActive: true }, include: { representative: { select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, type: true, }, }, }, orderBy: { createdAt: 'desc' }, }); } export async function addRepresentative( customerId: number, // Der Kunde, dessen Verträge eingesehen werden dürfen representativeId: number, // Der Kunde, der einsehen darf notes?: string ) { // Prüfen, ob beide Kunden existieren const [customer, representative] = await Promise.all([ prisma.customer.findUnique({ where: { id: customerId } }), prisma.customer.findUnique({ where: { id: representativeId } }), ]); if (!customer) { throw new Error('Kunde nicht gefunden'); } if (!representative) { throw new Error('Vertreter-Kunde nicht gefunden'); } if (customerId === representativeId) { throw new Error('Ein Kunde kann sich nicht selbst vertreten'); } // Prüfen ob der Vertreter ein Portal-Konto hat if (!representative.portalEnabled) { throw new Error('Der Vertreter-Kunde muss ein aktiviertes Portal-Konto haben'); } return prisma.customerRepresentative.upsert({ where: { customerId_representativeId: { customerId, representativeId }, }, create: { customerId, representativeId, notes, isActive: true, }, update: { isActive: true, notes, }, include: { representative: { select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, type: true, }, }, }, }); } export async function removeRepresentative(customerId: number, representativeId: number) { // Anstatt zu löschen, setzen wir isActive auf false return prisma.customerRepresentative.update({ where: { customerId_representativeId: { customerId, representativeId }, }, data: { isActive: false }, }); } export async function searchCustomersForRepresentative(search: string, excludeCustomerId: number) { // Sucht Kunden, die als Vertreter hinzugefügt werden können // Nur Kunden mit aktiviertem Portal return prisma.customer.findMany({ where: { id: { not: excludeCustomerId }, portalEnabled: true, OR: [ { firstName: { contains: search } }, { lastName: { contains: search } }, { companyName: { contains: search } }, { customerNumber: { contains: search } }, ], }, select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, type: true, }, take: 10, }); }