import type { MeterReading, ContractMeter } from '../types'; // Konstante für Gas → kWh Umrechnung (Standard Erdgas H) export const GAS_TO_KWH_FACTOR = 10.5; // Ergebnis der Verbrauchsberechnung export interface ConsumptionCalculation { type: 'exact' | 'projected' | 'insufficient' | 'none'; consumptionM3?: number; // Nur bei Gas consumptionKwh: number; consumptionHt?: number; // Nur bei HT/NT: HT-Verbrauch in kWh consumptionNt?: number; // Nur bei HT/NT: NT-Verbrauch in kWh startReading?: MeterReading; endReading?: MeterReading; projectedEndDate?: string; // Nur bei Hochrechnung message?: string; } // Ergebnis der Kostenberechnung export interface CostCalculation { annualBaseCost: number; // basePrice × 12 annualConsumptionCost: number; // verbrauch × unitPrice (HT bei Zweitarif) annualConsumptionCostNt?: number; // NT-Verbrauch × unitPriceNt annualTotalCost: number; // Summe monthlyPayment: number; // annualTotalCost / 12 instantBonus?: number; // Sofort-Bonus newCustomerBonus?: number; // Neukunden-Bonus totalBonus?: number; // Summe = instantBonus + newCustomerBonus effectiveAnnualCost: number; // annualTotalCost - totalBonus } /** * Berechnet die Differenz in Tagen zwischen zwei Daten */ function daysDiff(startDate: string, endDate: string): number { const start = new Date(startDate); const end = new Date(endDate); start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); const diffTime = end.getTime() - start.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Filtert Zählerstände nach Vertragszeitraum * Ein Zählerstand gilt als "im Zeitraum" wenn: * readingDate >= startDate UND readingDate <= endDate */ export function filterReadingsByContractPeriod( readings: MeterReading[], startDate: string, endDate: string ): MeterReading[] { const start = new Date(startDate); const end = new Date(endDate); start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); return readings.filter((reading) => { const readingDate = new Date(reading.readingDate); readingDate.setHours(0, 0, 0, 0); return readingDate >= start && readingDate <= end; }); } /** * Berechnet den Verbrauch basierend auf den Zählerständen * * Fälle: * A) Anfangs- UND Endzählerstand vorhanden -> exakter Verbrauch * B) >= 2 Stände, kein Endzählerstand -> Hochrechnung * C) Nur 1 Stand -> insufficient * D) Keine Stände -> none */ export function calculateConsumption( readings: MeterReading[], startDate: string, endDate: string, contractType: 'ELECTRICITY' | 'GAS' ): ConsumptionCalculation { const filteredReadings = filterReadingsByContractPeriod(readings, startDate, endDate); // Fall D: Keine Zählerstände if (filteredReadings.length === 0) { return { type: 'none', consumptionKwh: 0 }; } // Fall C: Nur 1 Zählerstand if (filteredReadings.length === 1) { return { type: 'insufficient', consumptionKwh: 0, message: 'Berechnung auf Grund fehlender Stände nicht möglich', }; } // Sortieren nach Datum (älteste zuerst) const sorted = [...filteredReadings].sort( (a, b) => new Date(a.readingDate).getTime() - new Date(b.readingDate).getTime() ); const lastReading = sorted[sorted.length - 1]; // Prüfen ob Endzählerstand am/nach Vertragsende liegt const lastReadingDate = new Date(lastReading.readingDate); const contractEndDate = new Date(endDate); lastReadingDate.setHours(0, 0, 0, 0); contractEndDate.setHours(0, 0, 0, 0); // Verbrauch zwischen aufeinanderfolgenden Ständen berechnen // (Erkennt Zählerwechsel: wenn ein Wert sinkt, wird ab dem neuen Stand weitergerechnet) let totalConsumption = 0; let totalConsumptionNt = 0; const hasNt = sorted.some(r => r.valueNt !== undefined && r.valueNt !== null); let effectiveFirstReading = sorted[0]; for (let i = 1; i < sorted.length; i++) { const diff = sorted[i].value - sorted[i - 1].value; if (diff < 0) { totalConsumption = 0; totalConsumptionNt = 0; effectiveFirstReading = sorted[i]; } else { totalConsumption += diff; if (hasNt && sorted[i].valueNt != null && sorted[i - 1].valueNt != null) { const diffNt = sorted[i].valueNt! - sorted[i - 1].valueNt!; if (diffNt >= 0) totalConsumptionNt += diffNt; } } } const effectiveLastReading = sorted[sorted.length - 1]; // Fall A: Exakter Verbrauch if (lastReadingDate >= contractEndDate) { const result = formatConsumptionResult('exact', totalConsumption, contractType, effectiveFirstReading, effectiveLastReading); if (hasNt) { result.consumptionHt = totalConsumption; result.consumptionNt = totalConsumptionNt; } return result; } // Fall B: Hochrechnung erforderlich const daysBetweenReadings = daysDiff(effectiveFirstReading.readingDate, effectiveLastReading.readingDate); if (daysBetweenReadings < 1) { return { type: 'insufficient', consumptionKwh: 0, message: 'Zeitraum zwischen Zählerständen zu kurz für Berechnung', }; } const totalContractDays = daysDiff(startDate, endDate); const projectedConsumption = (totalConsumption / daysBetweenReadings) * totalContractDays; const result = formatConsumptionResult( 'projected', projectedConsumption, contractType, effectiveFirstReading, effectiveLastReading, endDate ); if (hasNt) { const projectedNt = (totalConsumptionNt / daysBetweenReadings) * totalContractDays; result.consumptionHt = projectedConsumption; result.consumptionNt = projectedNt; // Gesamt-kWh = HT + NT result.consumptionKwh = projectedConsumption + projectedNt; } return result; } /** * Formatiert das Ergebnis der Verbrauchsberechnung */ function formatConsumptionResult( type: 'exact' | 'projected', consumption: number, contractType: 'ELECTRICITY' | 'GAS', startReading: MeterReading, endReading: MeterReading, projectedEndDate?: string ): ConsumptionCalculation { if (contractType === 'GAS') { return { type, consumptionM3: consumption, consumptionKwh: consumption * GAS_TO_KWH_FACTOR, startReading, endReading, projectedEndDate, }; } return { type, consumptionKwh: consumption, startReading, endReading, projectedEndDate, }; } /** * Berechnet die Kosten basierend auf Verbrauch und Preisen * Gibt null zurück wenn keine Preise vorhanden sind */ export function calculateCosts( consumptionKwh: number, basePrice?: number, unitPrice?: number, instantBonus?: number, consumptionNtKwh?: number, unitPriceNt?: number, newCustomerBonus?: number, ): CostCalculation | null { // Mindestens ein Preis muss vorhanden sein if (basePrice == null && unitPrice == null) { return null; } const annualBaseCost = (basePrice ?? 0) * 12; // Bei HT/NT: consumptionKwh ist nur HT, NT wird separat berechnet const annualConsumptionCost = consumptionKwh * (unitPrice ?? 0); const annualConsumptionCostNt = (consumptionNtKwh ?? 0) * (unitPriceNt ?? 0); const annualTotalCost = annualBaseCost + annualConsumptionCost + annualConsumptionCostNt; const totalBonus = (instantBonus ?? 0) + (newCustomerBonus ?? 0); const effectiveAnnualCost = annualTotalCost - totalBonus; const monthlyPayment = effectiveAnnualCost / 12; return { annualBaseCost, annualConsumptionCost, annualConsumptionCostNt: annualConsumptionCostNt > 0 ? annualConsumptionCostNt : undefined, annualTotalCost, monthlyPayment, instantBonus: instantBonus && instantBonus > 0 ? instantBonus : undefined, newCustomerBonus: newCustomerBonus && newCustomerBonus > 0 ? newCustomerBonus : undefined, totalBonus: totalBonus > 0 ? totalBonus : undefined, effectiveAnnualCost, }; } /** * Berechnet den Verbrauch über mehrere Zähler (Folgezähler). * Pro Zähler wird der Verbrauch einzeln berechnet und dann summiert. */ export function calculateMultiMeterConsumption( contractMeters: ContractMeter[], startDate: string, endDate: string, contractType: 'ELECTRICITY' | 'GAS' ): ConsumptionCalculation { if (contractMeters.length === 0) { return { type: 'none', consumptionKwh: 0 }; } let totalConsumption = 0; let totalConsumptionM3 = 0; let hasExact = true; let hasAny = false; let firstStart: MeterReading | undefined; let lastEnd: MeterReading | undefined; const contractStartMs = new Date(startDate).getTime(); const contractEndMs = new Date(endDate).getTime(); for (const cm of contractMeters) { const readings = cm.meter?.readings || []; if (readings.length === 0) continue; // Zeitraum für diesen Zähler bestimmen, GE-CLAMPED auf die Vertragslaufzeit. // Ohne Clamp würden Folgezähler, die nach Vertragsende installiert wurden // (typisch bei Vorgängerverträgen einer Folgevertrags-Kette), zukünftige // Zählerstände in den Verbrauch dieses Vertrags einrechnen. const installedMs = cm.installedAt ? new Date(cm.installedAt).getTime() : contractStartMs; const removedMs = cm.removedAt ? new Date(cm.removedAt).getTime() : contractEndMs; const meterStartMs = Math.max(installedMs, contractStartMs); const meterEndMs = Math.min(removedMs, contractEndMs); if (meterStartMs > meterEndMs) continue; // Zähler liegt komplett außerhalb der Laufzeit const meterStart = new Date(meterStartMs).toISOString(); const meterEnd = new Date(meterEndMs).toISOString(); const result = calculateConsumption(readings, meterStart, meterEnd, contractType); if (result.type === 'none' || result.type === 'insufficient') continue; hasAny = true; if (result.type === 'projected') hasExact = false; totalConsumption += result.consumptionKwh; if (result.consumptionM3) totalConsumptionM3 += result.consumptionM3; if (!firstStart && result.startReading) firstStart = result.startReading; if (result.endReading) lastEnd = result.endReading; } if (!hasAny) { // Fallback: Einzelzähler-Berechnung mit allen Readings const allReadings = contractMeters.flatMap(cm => cm.meter?.readings || []); return calculateConsumption(allReadings, startDate, endDate, contractType); } return { type: hasExact ? 'exact' : 'projected', consumptionKwh: totalConsumption, consumptionM3: contractType === 'GAS' ? totalConsumptionM3 : undefined, startReading: firstStart, endReading: lastEnd, }; }