Datenschutz vollmacht fixed, two time counter added

This commit is contained in:
2026-03-21 16:42:31 +01:00
parent 0121c82412
commit 4f359df161
56 changed files with 4401 additions and 789 deletions
+2
View File
@@ -11,5 +11,7 @@ export declare function getSimCardCredentials(req: Request, res: Response): Prom
export declare function getInternetCredentials(req: Request, res: Response): Promise<void>;
export declare function getSipCredentials(req: Request, res: Response): Promise<void>;
export declare function getCockpit(req: AuthRequest, res: Response): Promise<void>;
export declare function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void>;
export declare function removeContractMeter(req: AuthRequest, res: Response): Promise<void>;
export declare function snoozeContract(req: Request, res: Response): Promise<void>;
//# sourceMappingURL=contract.controller.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDtF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
+67
View File
@@ -44,6 +44,8 @@ exports.getSimCardCredentials = getSimCardCredentials;
exports.getInternetCredentials = getInternetCredentials;
exports.getSipCredentials = getSipCredentials;
exports.getCockpit = getCockpit;
exports.addSuccessorMeter = addSuccessorMeter;
exports.removeContractMeter = removeContractMeter;
exports.snoozeContract = snoozeContract;
const client_1 = require("@prisma/client");
const contractService = __importStar(require("../services/contract.service.js"));
@@ -265,6 +267,71 @@ async function getCockpit(req, res) {
});
}
}
// ==================== FOLGEZÄHLER ====================
async function addSuccessorMeter(req, res) {
try {
const contractId = parseInt(req.params.id);
const { meterId, installedAt, finalReadingPrevious } = req.body;
const contract = await prisma.contract.findUnique({
where: { id: contractId },
include: { energyDetails: { include: { contractMeters: { orderBy: { position: 'asc' } } } } },
});
if (!contract?.energyDetails) {
res.status(404).json({ success: false, error: 'Energievertrag nicht gefunden' });
return;
}
const ecdId = contract.energyDetails.id;
const existingMeters = contract.energyDetails.contractMeters;
const nextPosition = existingMeters.length > 0
? Math.max(...existingMeters.map(m => m.position)) + 1
: 0;
// Vorherigen Zähler als gewechselt markieren
if (existingMeters.length > 0 && finalReadingPrevious !== undefined) {
const prevMeter = existingMeters[existingMeters.length - 1];
await prisma.contractMeter.update({
where: { id: prevMeter.id },
data: {
removedAt: installedAt ? new Date(installedAt) : new Date(),
finalReading: parseFloat(finalReadingPrevious),
},
});
}
const contractMeter = await prisma.contractMeter.create({
data: {
energyContractDetailsId: ecdId,
meterId: parseInt(meterId),
position: nextPosition,
installedAt: installedAt ? new Date(installedAt) : new Date(),
},
include: { meter: { include: { readings: true } } },
});
// Aktuellen Zähler am Vertrag aktualisieren
await prisma.energyContractDetails.update({
where: { id: ecdId },
data: { meterId: parseInt(meterId) },
});
res.json({ success: true, data: contractMeter });
}
catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
});
}
}
async function removeContractMeter(req, res) {
try {
const contractMeterId = parseInt(req.params.contractMeterId);
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
res.json({ success: true, data: null });
}
catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
});
}
}
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
async function snoozeContract(req, res) {
try {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
+33 -11
View File
@@ -351,7 +351,14 @@ async function getMeterReadings(req, res) {
}
async function addMeterReading(req, res) {
try {
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
const { readingDate, value, valueNt, unit, notes } = req.body;
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
readingDate: new Date(readingDate),
value: parseFloat(value),
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
unit,
notes,
});
res.status(201).json({ success: true, data: reading });
}
catch (error) {
@@ -363,7 +370,19 @@ async function addMeterReading(req, res) {
}
async function updateMeterReading(req, res) {
try {
const reading = await customerService.updateMeterReading(parseInt(req.params.meterId), parseInt(req.params.readingId), req.body);
const { readingDate, value, valueNt, unit, notes } = req.body;
const updateData = {};
if (readingDate !== undefined)
updateData.readingDate = new Date(readingDate);
if (value !== undefined)
updateData.value = parseFloat(value);
if (valueNt !== undefined)
updateData.valueNt = valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : null;
if (unit !== undefined)
updateData.unit = unit;
if (notes !== undefined)
updateData.notes = notes;
const reading = await customerService.updateMeterReading(parseInt(req.params.meterId), parseInt(req.params.readingId), updateData);
res.json({ success: true, data: reading });
}
catch (error) {
@@ -404,15 +423,18 @@ async function reportMeterReading(req, res) {
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' });
return;
}
const reading = await prisma.meterReading.create({
data: {
meterId,
value: parseFloat(value),
readingDate: readingDate ? new Date(readingDate) : new Date(),
notes,
reportedBy: user.email,
status: 'REPORTED',
},
const parsedDate = readingDate ? new Date(readingDate) : new Date();
const parsedValue = parseFloat(value);
// Validierung über den Service (monoton steigend)
const reading = await customerService.addMeterReading(meterId, {
readingDate: parsedDate,
value: parsedValue,
notes,
});
// Status auf REPORTED setzen
await prisma.meterReading.update({
where: { id: reading.id },
data: { reportedBy: user.email, status: 'REPORTED' },
});
res.status(201).json({ success: true, data: reading });
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8BxB,eAAe,MAAM,CAAC"}
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAkCxB,eAAe,MAAM,CAAC"}
+3
View File
@@ -48,6 +48,9 @@ router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('
router.post('/:id/follow-up', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createFollowUp);
// Snooze (Vertrag zurückstellen)
router.patch('/:id/snooze', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.snoozeContract);
// Folgezähler
router.post('/:id/successor-meter', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.addSuccessorMeter);
router.delete('/:id/contract-meter/:contractMeterId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.removeContractMeter);
// Get decrypted password
router.get('/:id/password', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContractPassword);
// Get decrypted SimCard PIN/PUK
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AACpG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEzG,2CAA2C;AAC3C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEzG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACtG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAC3G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAE9G,qBAAqB;AACrB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtH,iCAAiC;AACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEpH,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEvH,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;AAE3I,kCAAkC;AAClC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEtI,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CAAC,6CAA6C,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAEnJ,kBAAe,MAAM,CAAC"}
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AACpG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEzG,2CAA2C;AAC3C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEzG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACtG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAC3G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAE9G,qBAAqB;AACrB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtH,iCAAiC;AACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEpH,cAAc;AACd,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAC/H,MAAM,CAAC,MAAM,CAAC,sCAAsC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEnJ,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEvH,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;AAE3I,kCAAkC;AAClC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEtI,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CAAC,6CAA6C,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAEnJ,kBAAe,MAAM,CAAC"}
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2uBxB,eAAe,MAAM,CAAC"}
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2vBxB,eAAe,MAAM,CAAC"}
+14
View File
@@ -338,6 +338,15 @@ router.post('/customers/:id/privacy-policy', auth_js_1.authenticate, (0, auth_js
where: { id: customerId },
data: { privacyPolicyPath: relativePath },
});
// Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung)
const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
for (const consentType of consentTypes) {
await prisma.customerConsent.upsert({
where: { customerId_consentType: { customerId, consentType } },
update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' },
create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: req.user?.email || 'admin' },
});
}
res.json({
success: true,
data: {
@@ -376,6 +385,11 @@ router.delete('/customers/:id/privacy-policy', auth_js_1.authenticate, (0, auth_
where: { id: customerId },
data: { privacyPolicyPath: null },
});
// Nur Consents widerrufen die per Papier erteilt wurden
await prisma.customerConsent.updateMany({
where: { customerId, status: 'GRANTED', source: 'papier' },
data: { status: 'WITHDRAWN', withdrawnAt: new Date() },
});
res.json({ success: true });
}
catch (error) {
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+32 -2
View File
@@ -122,14 +122,14 @@ async function getContractById(id, decryptPassword = false) {
previousProvider: true,
previousContract: {
include: {
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
},
},
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
@@ -264,11 +264,41 @@ async function updateContract(id, data) {
});
// Update type-specific details
if (energyDetails) {
const existingEcd = await prisma.energyContractDetails.findUnique({
where: { contractId: id },
select: { id: true, meterId: true },
});
await prisma.energyContractDetails.upsert({
where: { contractId: id },
update: energyDetails,
create: { contractId: id, ...energyDetails },
});
// ContractMeter synchronisieren wenn sich der Zähler ändert
if (energyDetails.meterId !== undefined && existingEcd) {
const oldMeterId = existingEcd.meterId;
const newMeterId = energyDetails.meterId;
if (oldMeterId !== newMeterId) {
// Alle alten ContractMeter-Einträge entfernen
await prisma.contractMeter.deleteMany({
where: { energyContractDetailsId: existingEcd.id },
});
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
if (newMeterId) {
const contract = await prisma.contract.findUnique({
where: { id },
select: { startDate: true },
});
await prisma.contractMeter.create({
data: {
energyContractDetailsId: existingEcd.id,
meterId: newMeterId,
position: 0,
installedAt: contract?.startDate,
},
});
}
}
}
}
if (internetDetails) {
const { phoneNumbers, internetPassword, ...internetData } = internetDetails;
File diff suppressed because one or more lines are too long
+4
View File
@@ -78,6 +78,10 @@ export interface ReportedMeterReading {
customerNumber: string;
name: string;
};
contract?: {
id: number;
contractNumber: string;
};
providerPortal?: {
providerName: string;
portalUrl: string;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,cAAc,EACpC,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE;QACV,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IAEF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF,cAAc,CAAC,EAAE;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAyED,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAsjB7D"}
+5
View File
@@ -682,6 +682,7 @@ async function getReportedMeterReadings() {
contract: {
select: {
id: true,
contractNumber: true,
portalUsername: true,
provider: {
select: { id: true, name: true, portalUrl: true },
@@ -717,6 +718,10 @@ async function getReportedMeterReadings() {
customerNumber: r.meter.customer.customerNumber,
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
},
contract: contract ? {
id: contract.id,
contractNumber: contract.contractNumber,
} : undefined,
providerPortal: provider?.portalUrl ? {
providerName: provider.name,
portalUrl: provider.portalUrl,
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -206,17 +206,17 @@ export declare function getAllTasks(filters: AllTasksFilters): Promise<({
customerNumber: string;
companyName: string | null;
};
tariff: {
id: number;
name: string;
} | null;
contractNumber: string;
providerName: string | null;
tariffName: string | null;
provider: {
id: number;
name: string;
} | null;
tariffName: string | null;
providerName: string | null;
contractNumber: string;
tariff: {
id: number;
name: string;
} | null;
};
subtasks: {
id: number;
+29 -16
View File
@@ -107,13 +107,14 @@ export declare function getCustomerById(id: number): Promise<({
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}[];
} & {
id: number;
@@ -123,6 +124,7 @@ export declare function getCustomerById(id: number): Promise<({
updatedAt: Date;
type: import(".prisma/client").$Enums.MeterType;
meterNumber: string;
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
location: string | null;
})[];
stressfreiEmails: {
@@ -171,11 +173,8 @@ export declare function getCustomerById(id: number): Promise<({
notes: string | null;
portalPasswordEncrypted: string | null;
startDate: Date | null;
status: import(".prisma/client").$Enums.ContractStatus;
customerNumberAtProvider: string | null;
tariffName: string | null;
providerName: string | null;
contractNumber: string;
status: import(".prisma/client").$Enums.ContractStatus;
contractCategoryId: number | null;
addressId: number | null;
billingAddressId: number | null;
@@ -190,6 +189,9 @@ export declare function getCustomerById(id: number): Promise<({
previousContractNumber: string | null;
providerId: number | null;
tariffId: number | null;
providerName: string | null;
tariffName: string | null;
customerNumberAtProvider: string | null;
contractNumberAtProvider: string | null;
priceFirst12Months: string | null;
priceFrom13Months: string | null;
@@ -573,13 +575,14 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}[];
} & {
id: number;
@@ -589,6 +592,7 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
updatedAt: Date;
type: import(".prisma/client").$Enums.MeterType;
meterNumber: string;
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
location: string | null;
})[]>;
export declare function createMeter(customerId: number, data: {
@@ -603,6 +607,7 @@ export declare function createMeter(customerId: number, data: {
updatedAt: Date;
type: import(".prisma/client").$Enums.MeterType;
meterNumber: string;
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
location: string | null;
}>;
export declare function updateMeter(id: number, data: {
@@ -618,6 +623,7 @@ export declare function updateMeter(id: number, data: {
updatedAt: Date;
type: import(".prisma/client").$Enums.MeterType;
meterNumber: string;
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
location: string | null;
}>;
export declare function deleteMeter(id: number): Promise<{
@@ -628,11 +634,13 @@ export declare function deleteMeter(id: number): Promise<{
updatedAt: Date;
type: import(".prisma/client").$Enums.MeterType;
meterNumber: string;
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
location: string | null;
}>;
export declare function addMeterReading(meterId: number, data: {
readingDate: Date;
value: number;
valueNt?: number;
unit?: string;
notes?: string;
}): Promise<{
@@ -640,30 +648,33 @@ export declare function addMeterReading(meterId: number, data: {
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}>;
export declare function getMeterReadings(meterId: number): Promise<{
id: number;
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}[]>;
export declare function updateMeterReading(meterId: number, readingId: number, data: {
readingDate?: Date;
value?: number;
valueNt?: number | null;
unit?: string;
notes?: string;
}): Promise<{
@@ -671,26 +682,28 @@ export declare function updateMeterReading(meterId: number, readingId: number, d
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}>;
export declare function deleteMeterReading(meterId: number, readingId: number): Promise<{
id: number;
createdAt: Date;
notes: string | null;
readingDate: Date;
meterId: number;
status: import(".prisma/client").$Enums.MeterReadingStatus;
value: number;
valueNt: number | null;
unit: string;
reportedBy: string | null;
status: import(".prisma/client").$Enums.MeterReadingStatus;
transferredAt: Date | null;
transferredBy: string | null;
meterId: number;
}>;
export declare function updatePortalSettings(customerId: number, data: {
portalEnabled?: boolean;
File diff suppressed because one or more lines are too long
+65 -1
View File
@@ -321,13 +321,42 @@ async function updateMeter(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,
...data,
readingDate: data.readingDate,
value: data.value,
valueNt: data.valueNt,
unit: data.unit,
notes: data.notes,
},
});
}
@@ -345,11 +374,46 @@ async function updateMeterReading(meterId, readingId, data) {
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({
File diff suppressed because one or more lines are too long
+23 -3
View File
File diff suppressed because one or more lines are too long
+20
View File
@@ -361,6 +361,7 @@ exports.Prisma.MeterScalarFieldEnum = {
customerId: 'customerId',
meterNumber: 'meterNumber',
type: 'type',
tariffModel: 'tariffModel',
location: 'location',
isActive: 'isActive',
createdAt: 'createdAt',
@@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
meterId: 'meterId',
readingDate: 'readingDate',
value: 'value',
valueNt: 'valueNt',
unit: 'unit',
notes: 'notes',
reportedBy: 'reportedBy',
@@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
annualConsumptionKwh: 'annualConsumptionKwh',
basePrice: 'basePrice',
unitPrice: 'unitPrice',
unitPriceNt: 'unitPriceNt',
bonus: 'bonus',
previousProviderName: 'previousProviderName',
previousCustomerNumber: 'previousCustomerNumber'
};
exports.Prisma.ContractMeterScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
meterId: 'meterId',
position: 'position',
installedAt: 'installedAt',
removedAt: 'removedAt',
finalReading: 'finalReading',
createdAt: 'createdAt'
};
exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
@@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
GAS: 'GAS'
};
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
SINGLE: 'SINGLE',
DUAL: 'DUAL'
};
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
RECORDED: 'RECORDED',
REPORTED: 'REPORTED',
@@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask',
EnergyContractDetails: 'EnergyContractDetails',
ContractMeter: 'ContractMeter',
Invoice: 'Invoice',
InternetContractDetails: 'InternetContractDetails',
PhoneNumber: 'PhoneNumber',
+2068 -57
View File
File diff suppressed because it is too large Load Diff
+23 -3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-c6d54e22fa4d6137f643638da5d523e99ce84f9544cc793fd89163f1612953c6",
"name": "prisma-client-45a91d7556f300a75a0048d27fac6a72915779fc4e5c2234b54fe3547ddb1605",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
+43 -17
View File
@@ -411,18 +411,25 @@ enum MeterType {
GAS
}
enum MeterTariffModel {
SINGLE // Eintarifzähler (Standard)
DUAL // Zweitarifzähler (HT/NT)
}
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
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
meterNumber String
type MeterType
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
location String?
isActive Boolean @default(true)
readings MeterReading[]
energyDetails EnergyContractDetails[]
contractMeters ContractMeter[] @relation("ContractMeters")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MeterReading {
@@ -430,7 +437,8 @@ model MeterReading {
meterId Int
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
readingDate DateTime
value Float
value Float // Bei Eintarif: Gesamtwert. Bei Zweitarif: HT-Wert
valueNt Float? // Nur bei Zweitarif: NT-Wert (Niedertarif)
unit String @default("kWh")
notes String?
// Meldung & Übertragung
@@ -709,20 +717,38 @@ enum InvoiceType {
}
model EnergyContractDetails {
id Int @id @default(autoincrement())
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
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])
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)
unitPrice Float? // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
unitPriceNt Float? // €/kWh NT-Preis (nur bei Zweitarifzähler)
bonus Float?
previousProviderName String?
previousCustomerNumber String?
invoices Invoice[] // Rechnungen
contractMeters ContractMeter[] // Zähler-Zuordnungen (inkl. Folgezähler)
}
model ContractMeter {
id Int @id @default(autoincrement())
energyContractDetailsId Int
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
meterId Int
meter Meter @relation("ContractMeters", fields: [meterId], references: [id])
position Int @default(0) // 0 = Original, 1 = erster Folgezähler, etc.
installedAt DateTime? // Ab wann wird dieser Zähler am Vertrag genutzt?
removedAt DateTime? // Wann wurde der Zähler gewechselt? (null = aktuell)
finalReading Float? // Letzter Stand vor dem Wechsel
createdAt DateTime @default(now())
@@unique([energyContractDetailsId, meterId])
@@index([energyContractDetailsId])
}
model Invoice {
+20
View File
@@ -361,6 +361,7 @@ exports.Prisma.MeterScalarFieldEnum = {
customerId: 'customerId',
meterNumber: 'meterNumber',
type: 'type',
tariffModel: 'tariffModel',
location: 'location',
isActive: 'isActive',
createdAt: 'createdAt',
@@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
meterId: 'meterId',
readingDate: 'readingDate',
value: 'value',
valueNt: 'valueNt',
unit: 'unit',
notes: 'notes',
reportedBy: 'reportedBy',
@@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
annualConsumptionKwh: 'annualConsumptionKwh',
basePrice: 'basePrice',
unitPrice: 'unitPrice',
unitPriceNt: 'unitPriceNt',
bonus: 'bonus',
previousProviderName: 'previousProviderName',
previousCustomerNumber: 'previousCustomerNumber'
};
exports.Prisma.ContractMeterScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
meterId: 'meterId',
position: 'position',
installedAt: 'installedAt',
removedAt: 'removedAt',
finalReading: 'finalReading',
createdAt: 'createdAt'
};
exports.Prisma.InvoiceScalarFieldEnum = {
id: 'id',
energyContractDetailsId: 'energyContractDetailsId',
@@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
GAS: 'GAS'
};
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
SINGLE: 'SINGLE',
DUAL: 'DUAL'
};
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
RECORDED: 'RECORDED',
REPORTED: 'REPORTED',
@@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
ContractTask: 'ContractTask',
ContractTaskSubtask: 'ContractTaskSubtask',
EnergyContractDetails: 'EnergyContractDetails',
ContractMeter: 'ContractMeter',
Invoice: 'Invoice',
InternetContractDetails: 'InternetContractDetails',
PhoneNumber: 'PhoneNumber',
+34 -8
View File
@@ -411,17 +411,24 @@ enum MeterType {
GAS
}
enum MeterTariffModel {
SINGLE // Eintarifzähler (Standard)
DUAL // Zweitarifzähler (HT/NT)
}
model Meter {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
meterNumber String
type MeterType
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
location String?
isActive Boolean @default(true)
readings MeterReading[]
energyDetails EnergyContractDetails[]
createdAt DateTime @default(now())
isActive Boolean @default(true)
readings MeterReading[]
energyDetails EnergyContractDetails[]
contractMeters ContractMeter[] @relation("ContractMeters")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -430,7 +437,8 @@ model MeterReading {
meterId Int
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
readingDate DateTime
value Float
value Float // Bei Eintarif: Gesamtwert. Bei Zweitarif: HT-Wert
valueNt Float? // Nur bei Zweitarif: NT-Wert (Niedertarif)
unit String @default("kWh")
notes String?
// Meldung & Übertragung
@@ -718,11 +726,29 @@ model EnergyContractDetails {
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)
unitPrice Float? // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
unitPriceNt Float? // €/kWh NT-Preis (nur bei Zweitarifzähler)
bonus Float?
previousProviderName String?
previousCustomerNumber String?
invoices Invoice[] // Rechnungen
contractMeters ContractMeter[] // Zähler-Zuordnungen (inkl. Folgezähler)
}
model ContractMeter {
id Int @id @default(autoincrement())
energyContractDetailsId Int
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
meterId Int
meter Meter @relation("ContractMeters", fields: [meterId], references: [id])
position Int @default(0) // 0 = Original, 1 = erster Folgezähler, etc.
installedAt DateTime? // Ab wann wird dieser Zähler am Vertrag genutzt?
removedAt DateTime? // Wann wurde der Zähler gewechselt? (null = aktuell)
finalReading Float? // Letzter Stand vor dem Wechsel
createdAt DateTime @default(now())
@@unique([energyContractDetailsId, meterId])
@@index([energyContractDetailsId])
}
model Invoice {
@@ -244,6 +244,79 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
}
}
// ==================== FOLGEZÄHLER ====================
export async function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void> {
try {
const contractId = parseInt(req.params.id);
const { meterId, installedAt, finalReadingPrevious } = req.body;
const contract = await prisma.contract.findUnique({
where: { id: contractId },
include: { energyDetails: { include: { contractMeters: { orderBy: { position: 'asc' } } } } },
});
if (!contract?.energyDetails) {
res.status(404).json({ success: false, error: 'Energievertrag nicht gefunden' } as ApiResponse);
return;
}
const ecdId = contract.energyDetails.id;
const existingMeters = contract.energyDetails.contractMeters;
const nextPosition = existingMeters.length > 0
? Math.max(...existingMeters.map(m => m.position)) + 1
: 0;
// Vorherigen Zähler als gewechselt markieren
if (existingMeters.length > 0 && finalReadingPrevious !== undefined) {
const prevMeter = existingMeters[existingMeters.length - 1];
await prisma.contractMeter.update({
where: { id: prevMeter.id },
data: {
removedAt: installedAt ? new Date(installedAt) : new Date(),
finalReading: parseFloat(finalReadingPrevious),
},
});
}
const contractMeter = await prisma.contractMeter.create({
data: {
energyContractDetailsId: ecdId,
meterId: parseInt(meterId),
position: nextPosition,
installedAt: installedAt ? new Date(installedAt) : new Date(),
},
include: { meter: { include: { readings: true } } },
});
// Aktuellen Zähler am Vertrag aktualisieren
await prisma.energyContractDetails.update({
where: { id: ecdId },
data: { meterId: parseInt(meterId) },
});
res.json({ success: true, data: contractMeter } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
} as ApiResponse);
}
}
export async function removeContractMeter(req: AuthRequest, res: Response): Promise<void> {
try {
const contractMeterId = parseInt(req.params.contractMeterId);
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
} as ApiResponse);
}
}
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
export async function snoozeContract(req: Request, res: Response): Promise<void> {
+31 -11
View File
@@ -293,7 +293,14 @@ export async function getMeterReadings(req: Request, res: Response): Promise<voi
export async function addMeterReading(req: Request, res: Response): Promise<void> {
try {
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
const { readingDate, value, valueNt, unit, notes } = req.body;
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
readingDate: new Date(readingDate),
value: parseFloat(value),
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
unit,
notes,
});
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -305,10 +312,18 @@ export async function addMeterReading(req: Request, res: Response): Promise<void
export async function updateMeterReading(req: Request, res: Response): Promise<void> {
try {
const { readingDate, value, valueNt, unit, notes } = req.body;
const updateData: Record<string, unknown> = {};
if (readingDate !== undefined) updateData.readingDate = new Date(readingDate);
if (value !== undefined) updateData.value = parseFloat(value);
if (valueNt !== undefined) updateData.valueNt = valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : null;
if (unit !== undefined) updateData.unit = unit;
if (notes !== undefined) updateData.notes = notes;
const reading = await customerService.updateMeterReading(
parseInt(req.params.meterId),
parseInt(req.params.readingId),
req.body
updateData as any
);
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
@@ -358,15 +373,20 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
return;
}
const reading = await prisma.meterReading.create({
data: {
meterId,
value: parseFloat(value),
readingDate: readingDate ? new Date(readingDate) : new Date(),
notes,
reportedBy: user.email,
status: 'REPORTED',
},
const parsedDate = readingDate ? new Date(readingDate) : new Date();
const parsedValue = parseFloat(value);
// Validierung über den Service (monoton steigend)
const reading = await customerService.addMeterReading(meterId, {
readingDate: parsedDate,
value: parsedValue,
notes,
});
// Status auf REPORTED setzen
await prisma.meterReading.update({
where: { id: reading.id },
data: { reportedBy: user.email, status: 'REPORTED' },
});
res.status(201).json({ success: true, data: reading } as ApiResponse);
+239 -16
View File
@@ -24,6 +24,9 @@ const RESOURCE_MAPPING: Record<string, { type: string; extractId?: (req: AuthReq
'/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
'/api/meters': { type: 'Meter', extractId: (req) => req.params.id || req.params.meterId },
'/api/upload': { type: 'Upload' },
'/api/email-logs': { type: 'EmailLog' },
'/api/auth': { type: 'Authentication' },
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
'/api/gdpr': { type: 'GDPR' },
@@ -116,6 +119,240 @@ function getClientIp(req: AuthRequest): string {
return req.socket.remoteAddress || 'unknown';
}
// Menschenlesbare Bezeichnungen für Resource-Typen
const RESOURCE_TYPE_LABELS: Record<string, string> = {
Customer: 'Kunde',
Contract: 'Vertrag',
BankCard: 'Bankverbindung',
IdentityDocument: 'Ausweis',
Address: 'Adresse',
Meter: 'Zähler',
MeterReading: 'Zählerstand',
User: 'Benutzer',
Provider: 'Anbieter',
Tariff: 'Tarif',
SalesPlatform: 'Vertriebsplattform',
ContractCategory: 'Vertragskategorie',
CancellationPeriod: 'Kündigungsfrist',
ContractDuration: 'Vertragslaufzeit',
EmailProviderConfig: 'E-Mail-Provider',
AppSetting: 'Einstellung',
CustomerConsent: 'Einwilligung',
ContractTask: 'Aufgabe',
ContractHistoryEntry: 'Vertragshistorie',
GDPR: 'Datenschutz',
Authentication: 'Anmeldung',
AuditLog: 'Audit-Protokoll',
StressfreiEmail: 'Stressfrei-E-Mail',
CachedEmail: 'E-Mail',
};
const ACTION_LABELS: Record<string, string> = {
CREATE: 'erstellt',
READ: 'aufgerufen',
UPDATE: 'aktualisiert',
DELETE: 'gelöscht',
EXPORT: 'exportiert',
ANONYMIZE: 'anonymisiert',
LOGIN: 'angemeldet',
LOGOUT: 'abgemeldet',
LOGIN_FAILED: 'Anmeldung fehlgeschlagen',
};
/**
* Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag
*/
function generateHumanLabel(
action: AuditAction,
resourceType: string,
req: AuthRequest,
responseBody: unknown
): string {
const typeName = RESOURCE_TYPE_LABELS[resourceType] || resourceType;
const actionName = ACTION_LABELS[action] || action;
// Identifikator aus Response oder Request extrahieren
let identifier = '';
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
const data = (responseBody as { data: Record<string, unknown> }).data;
if (data) {
identifier =
(data.contractNumber as string) ||
(data.customerNumber as string) ||
(data.meterNumber as string) ||
(data.name as string) ||
(data.email as string) ||
(data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : '') ||
'';
}
}
// Spezial-Labels für bestimmte Endpunkte
const path = req.path;
// Auth
if (path.includes('/auth/login') || path.includes('/auth/customer-login')) {
const email = req.body?.email || '';
return action === 'LOGIN'
? `Benutzer ${email} hat sich angemeldet`
: `Anmeldung fehlgeschlagen für ${email}`;
}
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
// Kunden-Operationen
if (resourceType === 'Customer') {
if (action === 'CREATE') return `Kunde ${identifier} angelegt`;
if (action === 'UPDATE') return `Kundendaten ${identifier} aktualisiert`;
if (action === 'DELETE') return `Kunde ${identifier} gelöscht`;
if (action === 'READ' && req.params.id) return `Kundendaten ${identifier} aufgerufen`;
if (action === 'READ') return 'Kundenliste aufgerufen';
}
// Verträge
if (resourceType === 'Contract') {
if (path.includes('/cockpit')) return 'Vertrags-Cockpit aufgerufen';
if (path.includes('/follow-up')) return `Folgevertrag für ${identifier} erstellt`;
if (path.includes('/snooze')) return `Vertrag ${identifier} zurückgestellt`;
if (path.includes('/password')) return `Passwort für Vertrag ${identifier} abgerufen`;
if (path.includes('/sip-credentials')) return 'SIP-Zugangsdaten abgerufen';
if (path.includes('/internet-credentials')) return `Internet-Zugangsdaten für Vertrag ${identifier} abgerufen`;
if (path.includes('/successor-meter')) return `Folgezähler zu Vertrag ${identifier} hinzugefügt`;
if (action === 'CREATE') return `Vertrag ${identifier} angelegt`;
if (action === 'UPDATE') return `Vertrag ${identifier} aktualisiert`;
if (action === 'DELETE') return `Vertrag ${identifier} gelöscht`;
if (action === 'READ' && req.params.id) return `Vertrag ${identifier} aufgerufen`;
if (action === 'READ') return 'Vertragsliste aufgerufen';
}
// Bankverbindungen
if (resourceType === 'BankCard') {
if (action === 'CREATE') return `Bankverbindung hinzugefügt`;
if (action === 'UPDATE') return `Bankverbindung aktualisiert`;
if (action === 'DELETE') return `Bankverbindung gelöscht`;
}
// Ausweise
if (resourceType === 'IdentityDocument') {
if (action === 'CREATE') return `Ausweis ${identifier} hinzugefügt`;
if (action === 'UPDATE') return `Ausweis ${identifier} aktualisiert`;
if (action === 'DELETE') return `Ausweis gelöscht`;
}
// Adressen
if (resourceType === 'Address') {
if (action === 'CREATE') return `Adresse hinzugefügt`;
if (action === 'UPDATE') return `Adresse aktualisiert`;
if (action === 'DELETE') return `Adresse gelöscht`;
}
// Zähler
if (resourceType === 'Meter') {
if (action === 'CREATE') return `Zähler ${identifier} angelegt`;
if (action === 'UPDATE') return `Zähler ${identifier} aktualisiert`;
if (action === 'DELETE') return `Zähler gelöscht`;
}
// Einwilligungen
if (resourceType === 'CustomerConsent') {
const consentType = req.params.consentType || '';
const consentLabels: Record<string, string> = {
DATA_PROCESSING: 'Datenverarbeitung',
MARKETING_EMAIL: 'E-Mail-Marketing',
MARKETING_PHONE: 'Telefonmarketing',
DATA_SHARING_PARTNER: 'Datenweitergabe',
};
const consentName = consentLabels[consentType] || consentType;
if (action === 'UPDATE') {
const status = req.body?.status;
return status === 'GRANTED'
? `Einwilligung "${consentName}" erteilt`
: `Einwilligung "${consentName}" widerrufen`;
}
if (action === 'READ') return 'Einwilligungen abgerufen';
}
// Benutzer
if (resourceType === 'User') {
if (action === 'CREATE') return `Benutzer ${identifier} angelegt`;
if (action === 'UPDATE') return `Benutzer ${identifier} aktualisiert`;
if (action === 'DELETE') return `Benutzer ${identifier} gelöscht`;
if (action === 'READ' && req.params.id) return `Benutzerdaten ${identifier} aufgerufen`;
if (action === 'READ') return 'Benutzerliste aufgerufen';
}
// Aufgaben
if (resourceType === 'ContractTask') {
if (path.includes('/complete')) return `Aufgabe als erledigt markiert`;
if (action === 'CREATE') return `Aufgabe erstellt`;
if (action === 'UPDATE') return `Aufgabe aktualisiert`;
if (action === 'DELETE') return `Aufgabe gelöscht`;
}
// E-Mail-Provider
if (resourceType === 'EmailProviderConfig') {
if (path.includes('/test-connection')) return `E-Mail-Provider Verbindungstest`;
if (path.includes('/provision')) return `E-Mail-Adresse provisioniert`;
if (action === 'CREATE') return `E-Mail-Provider ${identifier} angelegt`;
if (action === 'UPDATE') return `E-Mail-Provider ${identifier} aktualisiert`;
if (action === 'DELETE') return `E-Mail-Provider ${identifier} gelöscht`;
}
// GDPR
if (resourceType === 'GDPR') {
if (path.includes('/dashboard')) return 'DSGVO-Dashboard aufgerufen';
if (path.includes('/export')) return 'Kundendaten exportiert (DSGVO Art. 15)';
if (path.includes('/privacy-policy')) {
return action === 'UPDATE' ? 'Datenschutzerklärung aktualisiert' : 'Datenschutzerklärung aufgerufen';
}
if (path.includes('/authorization-template')) {
return action === 'UPDATE' ? 'Vollmacht-Vorlage aktualisiert' : 'Vollmacht-Vorlage aufgerufen';
}
if (path.includes('/send-consent-link')) return 'Datenschutz-Link versendet';
if (path.includes('/authorizations') && path.includes('/send')) return 'Vollmacht-Anfrage versendet';
if (path.includes('/authorizations') && path.includes('/grant')) return 'Vollmacht erteilt';
if (path.includes('/authorizations') && path.includes('/withdraw')) return 'Vollmacht widerrufen';
if (path.includes('/authorizations') && path.includes('/upload')) return 'Vollmacht-PDF hochgeladen';
if (path.includes('/authorizations') && path.includes('/document') && action === 'DELETE') return 'Vollmacht-PDF gelöscht';
if (path.includes('/my-privacy')) return 'Eigene Datenschutzseite aufgerufen';
if (path.includes('/my-consent-status')) return 'Eigener Einwilligungsstatus geprüft';
if (path.includes('/my-authorizations')) return 'Eigene Vollmachten aufgerufen';
if (path.includes('/deletions')) {
if (action === 'CREATE') return 'Löschanfrage erstellt';
if (path.includes('/process')) return 'Löschanfrage bearbeitet';
return 'Löschanfragen aufgerufen';
}
if (path.includes('/consent-status')) return 'Einwilligungsstatus geprüft';
if (path.includes('/consents/overview')) return 'Einwilligungsübersicht aufgerufen';
}
// Einstellungen
if (resourceType === 'AppSetting') {
if (action === 'UPDATE') return `Einstellung "${req.params.key || ''}" geändert`;
if (action === 'READ') return 'Einstellungen aufgerufen';
}
// Zähler-Readings
if (path.includes('/readings')) {
if (path.includes('/report')) return 'Zählerstand vom Kunden gemeldet';
if (path.includes('/transfer')) return 'Zählerstand als übertragen markiert';
if (action === 'CREATE') return 'Zählerstand erfasst';
if (action === 'UPDATE') return 'Zählerstand aktualisiert';
if (action === 'DELETE') return 'Zählerstand gelöscht';
}
// Upload-Operationen
if (path.includes('/upload') || path.includes('/privacy-policy')) {
if (path.includes('/privacy-policy') && action === 'DELETE') return 'Datenschutzerklärung-PDF gelöscht';
if (path.includes('/privacy-policy')) return 'Datenschutzerklärung-PDF hochgeladen';
}
// Standard-Fallback
if (identifier) {
return `${typeName} ${identifier} ${actionName}`;
}
return `${typeName} ${actionName}`;
}
/**
* Audit Middleware - loggt alle API-Aufrufe asynchron
*/
@@ -162,22 +399,8 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
const auditContext = getAuditContext();
// Label für bessere Lesbarkeit generieren
let resourceLabel: string | undefined;
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
const data = (responseBody as { data: Record<string, unknown> }).data;
if (data) {
// Versuche verschiedene Label-Felder
resourceLabel =
(data.contractNumber as string) ||
(data.customerNumber as string) ||
(data.name as string) ||
(data.email as string) ||
(data.firstName && data.lastName
? `${data.firstName} ${data.lastName}`
: undefined);
}
}
// Menschenlesbares Label generieren
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
await createAuditLog({
userId: req.user?.userId,
+4
View File
@@ -20,6 +20,10 @@ router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'
// Snooze (Vertrag zurückstellen)
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
// Folgezähler
router.post('/:id/successor-meter', authenticate, requirePermission('contracts:update'), contractController.addSuccessorMeter);
router.delete('/:id/contract-meter/:contractMeterId', authenticate, requirePermission('contracts:update'), contractController.removeContractMeter);
// Get decrypted password
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
+16
View File
@@ -440,6 +440,16 @@ router.post(
data: { privacyPolicyPath: relativePath },
});
// Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung)
const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'] as const;
for (const consentType of consentTypes) {
await prisma.customerConsent.upsert({
where: { customerId_consentType: { customerId, consentType } },
update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' },
create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: (req as any).user?.email || 'admin' },
});
}
res.json({
success: true,
data: {
@@ -488,6 +498,12 @@ router.delete(
data: { privacyPolicyPath: null },
});
// Nur Consents widerrufen die per Papier erteilt wurden
await prisma.customerConsent.updateMany({
where: { customerId, status: 'GRANTED', source: 'papier' },
data: { status: 'WITHDRAWN', withdrawnAt: new Date() },
});
res.json({ success: true });
} catch (error) {
console.error('Delete error:', error);
+28 -6
View File
@@ -146,13 +146,35 @@ export async function updateAuthorizationDocument(
* Vollmacht-Dokument löschen
*/
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
// Prüfen ob die Vollmacht per Papier erteilt wurde
const auth = await prisma.representativeAuthorization.findUnique({
where: { customerId_representativeId: { customerId, representativeId } },
select: { source: true, documentPath: true },
});
if (!auth) throw new Error('Vollmacht nicht gefunden');
// Datei löschen
if (auth.documentPath) {
try {
const filePath = path.join(process.cwd(), auth.documentPath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (err) {
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
}
}
// Wenn per Papier erteilt → Vollmacht widerrufen
// Wenn per Portal/Online erteilt → nur PDF entfernen, Vollmacht bleibt
const withdrawData = auth.source === 'papier'
? { documentPath: null, isGranted: false, withdrawnAt: new Date() }
: { documentPath: null };
return prisma.representativeAuthorization.update({
where: {
customerId_representativeId: { customerId, representativeId },
},
data: {
documentPath: null,
},
where: { customerId_representativeId: { customerId, representativeId } },
data: withdrawData,
});
}
+36 -2
View File
@@ -125,14 +125,14 @@ export async function getContractById(id: number, decryptPassword = false) {
previousProvider: true,
previousContract: {
include: {
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
},
},
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
@@ -403,11 +403,45 @@ export async function updateContract(
// Update type-specific details
if (energyDetails) {
const existingEcd = await prisma.energyContractDetails.findUnique({
where: { contractId: id },
select: { id: true, meterId: true },
});
await prisma.energyContractDetails.upsert({
where: { contractId: id },
update: energyDetails,
create: { contractId: id, ...energyDetails },
});
// ContractMeter synchronisieren wenn sich der Zähler ändert
if (energyDetails.meterId !== undefined && existingEcd) {
const oldMeterId = existingEcd.meterId;
const newMeterId = energyDetails.meterId;
if (oldMeterId !== newMeterId) {
// Alle alten ContractMeter-Einträge entfernen
await prisma.contractMeter.deleteMany({
where: { energyContractDetailsId: existingEcd.id },
});
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
if (newMeterId) {
const contract = await prisma.contract.findUnique({
where: { id },
select: { startDate: true },
});
await prisma.contractMeter.create({
data: {
energyContractDetailsId: existingEcd.id,
meterId: newMeterId,
position: 0,
installedAt: contract?.startDate,
},
});
}
}
}
}
if (internetDetails) {
@@ -89,6 +89,11 @@ export interface ReportedMeterReading {
customerNumber: string;
name: string;
};
// Zugehöriger Vertrag
contract?: {
id: number;
contractNumber: string;
};
// Anbieter-Info für Quick-Login
providerPortal?: {
providerName: string;
@@ -810,6 +815,7 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
contract: {
select: {
id: true,
contractNumber: true,
portalUsername: true,
provider: {
select: { id: true, name: true, portalUrl: true },
@@ -847,6 +853,10 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
customerNumber: r.meter.customer.customerNumber,
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
},
contract: contract ? {
id: contract.id,
contractNumber: contract.contractNumber,
} : undefined,
providerPortal: provider?.portalUrl ? {
providerName: provider.name,
portalUrl: provider.portalUrl,
+90 -1
View File
@@ -443,6 +443,30 @@ export async function updateMeter(
}
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 } });
}
@@ -451,14 +475,25 @@ export async function addMeterReading(
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,
...data,
readingDate: data.readingDate,
value: data.value,
valueNt: data.valueNt,
unit: data.unit,
notes: data.notes,
},
});
}
@@ -476,6 +511,7 @@ export async function updateMeterReading(
data: {
readingDate?: Date;
value?: number;
valueNt?: number | null;
unit?: string;
notes?: string;
}
@@ -489,12 +525,65 @@ export async function updateMeterReading(
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({
+34
View File
@@ -0,0 +1,34 @@
Vertragliste bei Energie mit Anschlussadresse/Lieferadresse noch in der Liste
Bei Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
Bei Festnetz, die Anschlussadresse/Lieferadresse
Bei KFZ das Kennzeichen
#erledigt
Datenschutzerklärung wenn PDF hinterlegt wurde, alle Haken auf Grün setzten.
Und wenn von Kunde im Kundenportal ein Haken weg, pdf wieder löschen und gesperrt setzen. bis endweder alle haken wieder gesetzt sind, oder pdf erneut hochgeladen
Aktuell zählt das PDF als Alternative zu den Online-Haken. Du willst es so:
PDF hochgeladen → alle 4 Online-Consents automatisch auf GRANTED setzen
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
#erledigt
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen.
Auch in die Berechnung die Verbäuche dann darstellen
Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist
Jetzt : 1.1.2026
Und gewollt 01.01.2026
Die Auditmeldungen aussagekräftig
Email Log und system testen
Security System testen