addes cost and usage calculation

This commit is contained in:
2026-02-06 00:14:38 +01:00
parent b281801cdb
commit 1ad4fe0819
5 changed files with 450 additions and 96 deletions
+197
View File
@@ -0,0 +1,197 @@
import type { MeterReading } 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;
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
annualTotalCost: number; // Summe
monthlyPayment: number; // annualTotalCost / 12
bonus?: number;
effectiveAnnualCost: number; // annualTotalCost - bonus
}
/**
* 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 firstReading = sorted[0];
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);
// Fall A: Exakter Verbrauch (Endzählerstand am oder nach Vertragsende)
if (lastReadingDate >= contractEndDate) {
const consumption = lastReading.value - firstReading.value;
return formatConsumptionResult('exact', consumption, contractType, firstReading, lastReading);
}
// Fall B: Hochrechnung erforderlich
const daysBetweenReadings = daysDiff(firstReading.readingDate, lastReading.readingDate);
// Mindestens 1 Tag zwischen den Messungen für sinnvolle Hochrechnung
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 measuredConsumption = lastReading.value - firstReading.value;
const projectedConsumption = (measuredConsumption / daysBetweenReadings) * totalContractDays;
return formatConsumptionResult(
'projected',
projectedConsumption,
contractType,
firstReading,
lastReading,
endDate
);
}
/**
* 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,
bonus?: number
): CostCalculation | null {
// Mindestens ein Preis muss vorhanden sein
if (basePrice == null && unitPrice == null) {
return null;
}
const annualBaseCost = (basePrice ?? 0) * 12;
const annualConsumptionCost = consumptionKwh * (unitPrice ?? 0);
const annualTotalCost = annualBaseCost + annualConsumptionCost;
const effectiveAnnualCost = annualTotalCost - (bonus ?? 0);
const monthlyPayment = effectiveAnnualCost / 12;
return {
annualBaseCost,
annualConsumptionCost,
annualTotalCost,
monthlyPayment,
bonus: bonus ?? undefined,
effectiveAnnualCost,
};
}