opencrm/backend/dist/services/customer.service.js

580 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAllCustomers = getAllCustomers;
exports.getCustomerById = getCustomerById;
exports.getCustomersByIds = getCustomersByIds;
exports.createCustomer = createCustomer;
exports.updateCustomer = updateCustomer;
exports.deleteCustomer = deleteCustomer;
exports.getCustomerAddresses = getCustomerAddresses;
exports.createAddress = createAddress;
exports.updateAddress = updateAddress;
exports.deleteAddress = deleteAddress;
exports.getCustomerBankCards = getCustomerBankCards;
exports.createBankCard = createBankCard;
exports.updateBankCard = updateBankCard;
exports.deleteBankCard = deleteBankCard;
exports.getCustomerDocuments = getCustomerDocuments;
exports.createDocument = createDocument;
exports.updateDocument = updateDocument;
exports.deleteDocument = deleteDocument;
exports.getCustomerMeters = getCustomerMeters;
exports.createMeter = createMeter;
exports.updateMeter = updateMeter;
exports.deleteMeter = deleteMeter;
exports.addMeterReading = addMeterReading;
exports.getMeterReadings = getMeterReadings;
exports.updateMeterReading = updateMeterReading;
exports.deleteMeterReading = deleteMeterReading;
exports.updatePortalSettings = updatePortalSettings;
exports.getPortalSettings = getPortalSettings;
exports.getCustomerRepresentatives = getCustomerRepresentatives;
exports.getRepresentedByList = getRepresentedByList;
exports.addRepresentative = addRepresentative;
exports.removeRepresentative = removeRepresentative;
exports.searchCustomersForRepresentative = searchCustomersForRepresentative;
const client_1 = require("@prisma/client");
const helpers_js_1 = require("../utils/helpers.js");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const prisma = new client_1.PrismaClient();
// Helper zum Löschen von Dateien
function deleteFileIfExists(filePath) {
if (!filePath)
return;
const absolutePath = path_1.default.join(process.cwd(), filePath);
if (fs_1.default.existsSync(absolutePath)) {
try {
fs_1.default.unlinkSync(absolutePath);
}
catch (error) {
console.error('Fehler beim Löschen der Datei:', absolutePath, error);
}
}
}
async function getAllCustomers(filters) {
const { search, type, page = 1, limit = 20 } = filters;
const { skip, take } = (0, helpers_js_1.paginate)(page, limit);
const where = {};
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: (0, helpers_js_1.buildPaginationResponse)(page, limit, total),
};
}
async function getCustomerById(id) {
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: client_1.ContractStatus.DEACTIVATED },
},
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
include: {
address: true,
salesPlatform: true,
},
},
},
});
}
async function getCustomersByIds(ids) {
return prisma.customer.findMany({
where: { id: { in: ids } },
select: {
id: true,
portalEmail: true,
},
});
}
async function createCustomer(data) {
return prisma.customer.create({
data: {
...data,
customerNumber: (0, helpers_js_1.generateCustomerNumber)(),
},
});
}
async function updateCustomer(id, data) {
return prisma.customer.update({
where: { id },
data,
});
}
async function deleteCustomer(id) {
// 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
async function getCustomerAddresses(customerId) {
return prisma.address.findMany({
where: { customerId },
orderBy: [{ isDefault: 'desc' }, { createdAt: 'desc' }],
});
}
async function createAddress(customerId, data) {
// 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,
},
});
}
async function updateAddress(id, data) {
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,
});
}
async function deleteAddress(id) {
return prisma.address.delete({ where: { id } });
}
// Bank card operations
async function getCustomerBankCards(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.bankCard.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createBankCard(customerId, data) {
return prisma.bankCard.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateBankCard(id, data) {
return prisma.bankCard.update({
where: { id },
data,
});
}
async function deleteBankCard(id) {
// 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
async function getCustomerDocuments(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.identityDocument.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createDocument(customerId, data) {
return prisma.identityDocument.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateDocument(id, data) {
return prisma.identityDocument.update({
where: { id },
data,
});
}
async function deleteDocument(id) {
// 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
async function getCustomerMeters(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.meter.findMany({
where,
include: {
readings: {
orderBy: { readingDate: 'desc' },
take: 5,
},
},
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createMeter(customerId, data) {
return prisma.meter.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateMeter(id, data) {
return prisma.meter.update({
where: { id },
data,
});
}
async function deleteMeter(id) {
// 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 } });
}
async function addMeterReading(meterId, data) {
// 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,
},
});
}
async function getMeterReadings(meterId) {
return prisma.meterReading.findMany({
where: { meterId },
orderBy: { readingDate: 'desc' },
});
}
async function updateMeterReading(meterId, readingId, data) {
// 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, readingDate, value, excludeReadingId, tariffLabel = 'HT') {
const existing = await prisma.meterReading.findMany({
where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) },
orderBy: { readingDate: 'asc' },
});
const fmtDate = (d) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
const fmtVal = (v) => v.toLocaleString('de-DE');
const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand';
// Vergleichswert aus bestehendem Reading extrahieren
const getVal = (r) => 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))})`);
}
}
async function deleteMeterReading(meterId, readingId) {
// 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 ====================
async function updatePortalSettings(customerId, data) {
// 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,
},
});
}
async function getPortalSettings(customerId) {
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 ====================
async function getCustomerRepresentatives(customerId) {
// 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' },
});
}
async function getRepresentedByList(customerId) {
// 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' },
});
}
async function addRepresentative(customerId, // Der Kunde, dessen Verträge eingesehen werden dürfen
representativeId, // Der Kunde, der einsehen darf
notes) {
// 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,
},
},
},
});
}
async function removeRepresentative(customerId, representativeId) {
// Anstatt zu löschen, setzen wir isActive auf false
return prisma.customerRepresentative.update({
where: {
customerId_representativeId: { customerId, representativeId },
},
data: { isActive: false },
});
}
async function searchCustomersForRepresentative(search, excludeCustomerId) {
// 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,
});
}
//# sourceMappingURL=customer.service.js.map