first commit

This commit is contained in:
Stefan Hacker
2026-01-29 01:16:54 +01:00
commit e209e9bbca
12105 changed files with 2480672 additions and 0 deletions
+682
View File
@@ -0,0 +1,682 @@
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<string, unknown> = {};
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<string, unknown> = { 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<string, unknown> = { 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<string, unknown> = { 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) {
return prisma.meter.delete({ where: { id } });
}
export async function addMeterReading(
meterId: number,
data: {
readingDate: Date;
value: number;
unit?: string;
notes?: string;
}
) {
return prisma.meterReading.create({
data: {
meterId,
...data,
},
});
}
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;
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');
}
return prisma.meterReading.update({
where: { id: readingId },
data,
});
}
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,
});
}