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 prisma_js_1 = __importDefault(require("../lib/prisma.js"));
const helpers_js_1 = require("../utils/helpers.js");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
// 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_js_1.default.customer.findMany({
where,
skip,
take,
orderBy: { createdAt: 'desc' },
include: {
addresses: { where: { isDefault: true }, take: 1 },
_count: {
select: { contracts: true },
},
},
}),
prisma_js_1.default.customer.count({ where }),
]);
return {
customers,
pagination: (0, helpers_js_1.buildPaginationResponse)(page, limit, total),
};
}
async function getCustomerById(id) {
return prisma_js_1.default.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_js_1.default.customer.findMany({
where: { id: { in: ids } },
select: {
id: true,
portalEmail: true,
},
});
}
async function createCustomer(data) {
return prisma_js_1.default.customer.create({
data: {
...data,
customerNumber: (0, helpers_js_1.generateCustomerNumber)(),
},
});
}
async function updateCustomer(id, data) {
return prisma_js_1.default.customer.update({
where: { id },
data,
});
}
async function deleteCustomer(id) {
// Vor dem Löschen: Alle Dokumente (Dateien) des Kunden löschen
const customer = await prisma_js_1.default.customer.findUnique({
where: { id },
select: { businessRegistrationPath: true, commercialRegisterPath: true, privacyPolicyPath: true },
});
const bankCards = await prisma_js_1.default.bankCard.findMany({
where: { customerId: id },
select: { documentPath: true },
});
const identityDocs = await prisma_js_1.default.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_js_1.default.customer.delete({
where: { id },
});
}
// Address operations
async function getCustomerAddresses(customerId) {
return prisma_js_1.default.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_js_1.default.address.updateMany({
where: { customerId, type: data.type },
data: { isDefault: false },
});
}
return prisma_js_1.default.address.create({
data: {
customerId,
...data,
},
});
}
async function updateAddress(id, data) {
const address = await prisma_js_1.default.address.findUnique({ where: { id } });
if (!address)
throw new Error('Adresse nicht gefunden');
if (data.isDefault) {
await prisma_js_1.default.address.updateMany({
where: {
customerId: address.customerId,
type: data.type || address.type,
id: { not: id },
},
data: { isDefault: false },
});
}
return prisma_js_1.default.address.update({
where: { id },
data,
});
}
async function deleteAddress(id) {
return prisma_js_1.default.address.delete({ where: { id } });
}
// Bank card operations
async function getCustomerBankCards(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma_js_1.default.bankCard.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createBankCard(customerId, data) {
return prisma_js_1.default.bankCard.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateBankCard(id, data) {
return prisma_js_1.default.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_js_1.default.bankCard.findUnique({ where: { id } });
if (bankCard?.documentPath) {
deleteFileIfExists(bankCard.documentPath);
}
return prisma_js_1.default.bankCard.delete({ where: { id } });
}
// Identity document operations
async function getCustomerDocuments(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma_js_1.default.identityDocument.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createDocument(customerId, data) {
return prisma_js_1.default.identityDocument.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateDocument(id, data) {
return prisma_js_1.default.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_js_1.default.identityDocument.findUnique({ where: { id } });
if (document?.documentPath) {
deleteFileIfExists(document.documentPath);
}
return prisma_js_1.default.identityDocument.delete({ where: { id } });
}
// Meter operations
async function getCustomerMeters(customerId, showInactive = false) {
const where = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma_js_1.default.meter.findMany({
where,
include: {
readings: {
orderBy: { readingDate: 'desc' },
take: 5,
},
},
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
async function createMeter(customerId, data) {
return prisma_js_1.default.meter.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
async function updateMeter(id, data) {
return prisma_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.meterReading.findMany({
where: { meterId },
orderBy: { readingDate: 'desc' },
});
}
async function updateMeterReading(meterId, readingId, data) {
// Verify the reading belongs to the meter
const reading = await prisma_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.meterReading.findFirst({
where: { id: readingId, meterId },
});
if (!reading) {
throw new Error('Zählerstand nicht gefunden');
}
return prisma_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.customer.findUnique({ where: { id: customerId } }),
prisma_js_1.default.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_js_1.default.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_js_1.default.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_js_1.default.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