Endstand alter Zähler fließt in Verbrauchsberechnung ein

Bisher wurde "Letzter Stand alter Zähler" zwar in
ContractMeter.finalReading gespeichert, aber nirgends ausgewertet.

Neuer Helper recordPredecessorFinalReading legt am Wechseldatum
einen regulären MeterReading-Eintrag für den Vorgänger an
(idempotent, mit Validierung gegen vorhandene Stände). Aufgerufen
aus addSuccessorMeter (Vertragsansicht) und createMeter mit
successorOf (Kundenakte).

Folge: Der Endstand erscheint in der Zählerstände-Liste des alten
Zählers und fließt automatisch über calculateMultiMeterConsumption
in den Verbrauch (Zeitraum bis removedAt ist inklusive).

UI-Hinweise in beiden Folgezähler-Forms erklären den Effekt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 14:14:03 +02:00
parent 34e106f253
commit 61ce35821d
5 changed files with 151 additions and 1 deletions
@@ -4,6 +4,7 @@ import * as contractService from '../services/contract.service.js';
import * as contractCockpitService from '../services/contractCockpit.service.js';
import * as contractHistoryService from '../services/contractHistory.service.js';
import * as authorizationService from '../services/authorization.service.js';
import { recordPredecessorFinalReading } from '../services/customer.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import { sanitizeContract, sanitizeContractStrict, sanitizeContracts, sanitizeContractsStrict, stripHtml } from '../utils/sanitize.js';
@@ -554,6 +555,38 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
const existingMeters = [...contract.energyDetails.contractMeters];
const switchAt = installedAt ? new Date(installedAt) : new Date();
// Vorgänger ermitteln (letzter ContractMeter oder Single-Meter-Vertrag)
const predecessorMeterId = existingMeters.length > 0
? existingMeters[existingMeters.length - 1].meterId
: contract.energyDetails.meterId;
// Endstand bereits hier validieren (monoton-steigend gegen vorhandene
// Zählerstände des Vorgängers), damit wir nicht halb-geschriebene
// Zustände hinterlassen.
if (finalReadingPrevious !== undefined && finalReadingPrevious !== null && predecessorMeterId) {
const finalReadingValue = parseFloat(finalReadingPrevious);
// recordPredecessorFinalReading läuft erst NACH den Writes Pre-Check
// ohne Write hier separat über die Service-Validierung (idempotent, weil
// sie keinen Reading anlegt, wenn am Wechseltag schon einer existiert).
// Wir lassen den eigentlichen Write am Ende laufen, damit ein Fehler
// beim Reading die Kette nicht zerreißt.
const dayStart = new Date(switchAt); dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1);
const sameDay = await prisma.meterReading.findFirst({
where: { meterId: predecessorMeterId, readingDate: { gte: dayStart, lt: dayEnd } },
});
if (!sameDay) {
const lastBefore = await prisma.meterReading.findFirst({
where: { meterId: predecessorMeterId, readingDate: { lte: switchAt } },
orderBy: { readingDate: 'desc' },
});
if (lastBefore && finalReadingValue < lastBefore.value) {
const fmtDate = (d: Date) => d.toLocaleDateString('de-DE');
throw new Error(`Endstand (${finalReadingValue}) darf nicht kleiner sein als der Stand vom ${fmtDate(lastBefore.readingDate)} (${lastBefore.value})`);
}
}
}
// Backfill: Bei Single-Meter-Verträgen (kein ContractMeter-Eintrag) den
// bisherigen `energyDetails.meterId` als position 0 nachtragen, damit die
// Folgezähler-Kette lückenlos ist und der alte Zähler nicht aus dem
@@ -605,6 +638,16 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
data: { meterId: parseInt(meterId) },
});
// Endstand des Vorgängers als regulären Zählerstand erfassen, damit er in
// die Verbrauchsberechnung einfließt und in der Zählerstände-Liste auftaucht.
if (finalReadingPrevious !== undefined && finalReadingPrevious !== null && predecessorMeterId) {
await recordPredecessorFinalReading(
predecessorMeterId,
switchAt,
parseFloat(finalReadingPrevious),
);
}
await logChange({
req, action: 'CREATE', resourceType: 'ContractMeter',
resourceId: contractMeter.id.toString(),