Folgezähler-Deklaration in der Kundenakte (Auto-Propagation)
- Meter.predecessorMeterId (Self-Relation) + Migration
20260530140000_meter_predecessor mit IF NOT EXISTS
- createMeter akzeptiert optional successorOf:
{predecessorMeterId, installedAt?, finalReadingPrevious?}.
Vorgänger wird validiert (gleicher Kunde + Typ); alle Verträge
mit dem Vorgänger als aktuellen Zähler werden analog zu
addSuccessorMeter automatisch auf den neuen Zähler umgestellt
(ContractMeter-Eintrag mit removedAt/finalReading für den
Vorgänger, neuer ContractMeter mit installedAt + nächster
Position, energyDetails.meterId aktualisiert)
- MeterModal: Checkbox "Als Folgezähler deklarieren" + Dropdown
Vorgänger + Wechseldatum + Endstand. Typ/Tarifmodell/Adresse
werden vom Vorgänger übernommen und disabled. Info-Banner über
Vertragsauto-Update
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -603,10 +603,13 @@ export async function createMeter(req: AuthRequest, res: Response): Promise<void
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
if (!(await canAccessCustomer(req, res, customerId))) return;
|
||||
const meter = await customerService.createMeter(customerId, req.body);
|
||||
const successorLabel = meter.predecessor
|
||||
? ` als Folgezähler von ${meter.predecessor.meterNumber}`
|
||||
: '';
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'Meter',
|
||||
resourceId: meter.id.toString(),
|
||||
label: `Zähler angelegt für Kunde #${customerId}`,
|
||||
label: `Zähler angelegt${successorLabel} für Kunde #${customerId}`,
|
||||
customerId,
|
||||
});
|
||||
res.status(201).json({ success: true, data: meter } as ApiResponse);
|
||||
|
||||
@@ -441,13 +441,39 @@ export async function createMeter(
|
||||
tariffModel?: 'SINGLE' | 'DUAL';
|
||||
location?: string;
|
||||
addressId?: number | null;
|
||||
// Optional: dieser Zähler ersetzt einen bestehenden (Folgezähler).
|
||||
// Beim Create werden alle Verträge, die den Vorgänger als aktuellen
|
||||
// Zähler nutzen, automatisch auf den neuen Zähler umgestellt
|
||||
// (ContractMeter-Eintrag analog zu Vertragsansicht).
|
||||
successorOf?: {
|
||||
predecessorMeterId: number;
|
||||
installedAt?: string;
|
||||
finalReadingPrevious?: number;
|
||||
};
|
||||
}
|
||||
) {
|
||||
if (data.addressId == null) {
|
||||
throw new Error('Lieferadresse ist erforderlich');
|
||||
}
|
||||
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
|
||||
return prisma.meter.create({
|
||||
|
||||
// Vorgänger validieren (wenn Folgezähler)
|
||||
let predecessor: { id: number; customerId: number; type: 'ELECTRICITY' | 'GAS' } | null = null;
|
||||
if (data.successorOf) {
|
||||
const pred = await prisma.meter.findUnique({
|
||||
where: { id: data.successorOf.predecessorMeterId },
|
||||
select: { id: true, customerId: true, type: true },
|
||||
});
|
||||
if (!pred || pred.customerId !== customerId) {
|
||||
throw new Error('Vorgänger-Zähler nicht gefunden');
|
||||
}
|
||||
if (pred.type !== data.type) {
|
||||
throw new Error('Vorgänger-Zähler muss denselben Typ haben (Strom/Gas)');
|
||||
}
|
||||
predecessor = pred;
|
||||
}
|
||||
|
||||
const created = await prisma.meter.create({
|
||||
data: {
|
||||
customerId,
|
||||
meterNumber: data.meterNumber,
|
||||
@@ -456,9 +482,84 @@ export async function createMeter(
|
||||
location: data.location,
|
||||
addressId: data.addressId,
|
||||
isActive: true,
|
||||
predecessorMeterId: predecessor?.id,
|
||||
},
|
||||
include: { address: true },
|
||||
include: { address: true, predecessor: true },
|
||||
});
|
||||
|
||||
// Folgezähler-Propagation: alle Verträge, die den Vorgänger als aktuellen
|
||||
// Zähler nutzen, bekommen den neuen Zähler als Nachfolger angehängt
|
||||
// (analog zu addSuccessorMeter im contract.controller).
|
||||
if (predecessor && data.successorOf) {
|
||||
const installedAt = data.successorOf.installedAt
|
||||
? new Date(data.successorOf.installedAt)
|
||||
: new Date();
|
||||
const finalReading = data.successorOf.finalReadingPrevious;
|
||||
|
||||
const affectedContracts = await prisma.energyContractDetails.findMany({
|
||||
where: { meterId: predecessor.id },
|
||||
include: { contractMeters: { orderBy: { position: 'asc' } } },
|
||||
});
|
||||
|
||||
for (const ecd of affectedContracts) {
|
||||
// Vorhandenen ContractMeter für den Vorgänger als gewechselt markieren.
|
||||
// Falls noch kein ContractMeter für den Vorgänger existiert (Single-Meter-
|
||||
// Vertrag vor Multi-Meter-Refactor), legen wir ihn als position 0 an,
|
||||
// damit die Kette lückenlos ist.
|
||||
let predCM = ecd.contractMeters.find((cm) => cm.meterId === predecessor!.id);
|
||||
if (!predCM) {
|
||||
predCM = await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: ecd.id,
|
||||
meterId: predecessor.id,
|
||||
position: 0,
|
||||
installedAt: null,
|
||||
},
|
||||
});
|
||||
ecd.contractMeters.push(predCM);
|
||||
}
|
||||
await prisma.contractMeter.update({
|
||||
where: { id: predCM.id },
|
||||
data: {
|
||||
removedAt: installedAt,
|
||||
finalReading: finalReading != null ? finalReading : predCM.finalReading,
|
||||
},
|
||||
});
|
||||
|
||||
const nextPosition = ecd.contractMeters.length > 0
|
||||
? Math.max(...ecd.contractMeters.map((cm) => cm.position)) + 1
|
||||
: 0;
|
||||
|
||||
// Idempotenz: falls (durch Doppel-Klick o.ä.) schon ein ContractMeter
|
||||
// mit dem neuen Zähler existiert, nicht doppelt anlegen.
|
||||
const existsForNew = await prisma.contractMeter.findUnique({
|
||||
where: {
|
||||
energyContractDetailsId_meterId: {
|
||||
energyContractDetailsId: ecd.id,
|
||||
meterId: created.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existsForNew) {
|
||||
await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: ecd.id,
|
||||
meterId: created.id,
|
||||
position: nextPosition,
|
||||
installedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Aktuellen Zähler am Vertrag aktualisieren
|
||||
await prisma.energyContractDetails.update({
|
||||
where: { id: ecd.id },
|
||||
data: { meterId: created.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateMeter(
|
||||
|
||||
Reference in New Issue
Block a user