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
+65
View File
@@ -421,6 +421,45 @@ export async function getCustomerMeters(
});
}
// Schreibt den Endstand des Vorgänger-Zählers beim Zählerwechsel als
// MeterReading. Wird beim Folgezähler-Anlegen aufgerufen (sowohl aus der
// Kundenakte als auch aus der Vertragsansicht). Idempotent: existiert am
// Wechseltag schon ein Reading, wird nichts angelegt. Validierung
// monoton-steigend wird durchgereicht wirft bei Konflikt.
export async function recordPredecessorFinalReading(
predecessorMeterId: number,
switchAt: Date,
value: number,
) {
const meter = await prisma.meter.findUnique({
where: { id: predecessorMeterId },
select: { type: true },
});
if (!meter) return;
const dayStart = new Date(switchAt);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
const existingSameDay = await prisma.meterReading.findFirst({
where: { meterId: predecessorMeterId, readingDate: { gte: dayStart, lt: dayEnd } },
});
if (existingSameDay) return;
await validateReadingValue(predecessorMeterId, switchAt, value, undefined, 'HT');
await prisma.meterReading.create({
data: {
meterId: predecessorMeterId,
readingDate: switchAt,
value,
unit: meter.type === 'GAS' ? 'm³' : 'kWh',
notes: 'Endstand bei Zählerwechsel (automatisch beim Folgezähler-Anlegen erfasst)',
},
});
}
// Lieferadresse muss zum Kunden gehören und vom Typ DELIVERY_RESIDENCE sein.
// Wirft eine sprechende Fehlermeldung, die der Controller dem User durchreicht.
async function assertDeliveryAddressBelongsToCustomer(addressId: number, customerId: number) {
@@ -471,6 +510,21 @@ export async function createMeter(
throw new Error('Vorgänger-Zähler muss denselben Typ haben (Strom/Gas)');
}
predecessor = pred;
// Endstand bereits hier validieren, damit kein verwaister Meter entsteht
// wenn der Wert mit bestehenden Zählerständen kollidiert.
if (data.successorOf.finalReadingPrevious != null) {
const switchAt = data.successorOf.installedAt
? new Date(data.successorOf.installedAt)
: new Date();
await validateReadingValue(
pred.id,
switchAt,
data.successorOf.finalReadingPrevious,
undefined,
'HT',
);
}
}
const created = await prisma.meter.create({
@@ -557,6 +611,17 @@ export async function createMeter(
data: { meterId: created.id },
});
}
// Endstand des Vorgängers als regulären Zählerstand erfassen, damit er
// in die Verbrauchsberechnung einfließt und in der Zählerstände-Liste
// sichtbar ist. Idempotent gegen Doppel-Submit.
if (data.successorOf.finalReadingPrevious != null) {
await recordPredecessorFinalReading(
predecessor.id,
installedAt,
data.successorOf.finalReadingPrevious,
);
}
}
return created;