use rust_decimal::Decimal;
use rust_decimal::dec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum Letztverbrauchergruppe {
#[default]
A,
B,
C,
Befreit,
}
pub const ERSTES_ERFASSTES_JAHR: i32 = 2026;
pub const ENFG_SCHWELLE_KWH: Decimal = dec!(1_000_000);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Umlagenaufteilung {
pub voller_satz_kwh: Decimal,
pub privilegiert_kwh: Decimal,
}
#[must_use]
pub fn aufteilung(
gruppe: Letztverbrauchergruppe,
menge_kwh: Decimal,
jahresvorverbrauch_kwh: Decimal,
) -> Umlagenaufteilung {
use Letztverbrauchergruppe as G;
if matches!(gruppe, G::A | G::Befreit) {
return Umlagenaufteilung {
voller_satz_kwh: menge_kwh,
privilegiert_kwh: Decimal::ZERO,
};
}
let rest = (ENFG_SCHWELLE_KWH - jahresvorverbrauch_kwh).max(Decimal::ZERO);
let voller_satz_kwh = menge_kwh.min(rest);
Umlagenaufteilung {
voller_satz_kwh,
privilegiert_kwh: menge_kwh - voller_satz_kwh,
}
}
const SECT19_STROMNEV: &[(i32, Decimal, Decimal, Decimal)] = &[
(2026, dec!(1.559), dec!(0.050), dec!(0.025)),
];
const OFFSHORE_NETZUMLAGE: &[(i32, Decimal)] = &[(2026, dec!(0.941))];
const KWKG_UMLAGE: &[(i32, Decimal)] = &[(2026, dec!(0.446))];
#[must_use]
pub fn sect19_stromnev_ct_per_kwh(year: i32, gruppe: Letztverbrauchergruppe) -> Option<Decimal> {
if gruppe == Letztverbrauchergruppe::Befreit {
return Some(Decimal::ZERO);
}
SECT19_STROMNEV
.iter()
.find(|(y, ..)| *y == year)
.map(|(_, a, b, c)| match gruppe {
Letztverbrauchergruppe::A => *a,
Letztverbrauchergruppe::B => *b,
Letztverbrauchergruppe::C => *c,
Letztverbrauchergruppe::Befreit => Decimal::ZERO,
})
}
#[must_use]
pub fn offshore_netzumlage_ct_per_kwh(
year: i32,
gruppe: Letztverbrauchergruppe,
) -> Option<Decimal> {
if gruppe == Letztverbrauchergruppe::Befreit {
return Some(Decimal::ZERO);
}
OFFSHORE_NETZUMLAGE
.iter()
.find(|(y, _)| *y == year)
.map(|(_, r)| *r)
}
#[must_use]
pub fn kwkg_umlage_ct_per_kwh(year: i32, gruppe: Letztverbrauchergruppe) -> Option<Decimal> {
if gruppe == Letztverbrauchergruppe::Befreit {
return Some(Decimal::ZERO);
}
KWKG_UMLAGE
.iter()
.find(|(y, _)| *y == year)
.map(|(_, r)| *r)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn published_2026_rates() {
use Letztverbrauchergruppe as G;
assert_eq!(sect19_stromnev_ct_per_kwh(2026, G::A), Some(dec!(1.559)));
assert_eq!(sect19_stromnev_ct_per_kwh(2026, G::B), Some(dec!(0.050)));
assert_eq!(sect19_stromnev_ct_per_kwh(2026, G::C), Some(dec!(0.025)));
assert_eq!(
offshore_netzumlage_ct_per_kwh(2026, G::A),
Some(dec!(0.941))
);
assert_eq!(kwkg_umlage_ct_per_kwh(2026, G::A), Some(dec!(0.446)));
}
#[test]
fn exemption_is_zero_not_missing() {
use Letztverbrauchergruppe::Befreit;
for year in [2020, 2026, 2099] {
assert_eq!(
sect19_stromnev_ct_per_kwh(year, Befreit),
Some(Decimal::ZERO)
);
assert_eq!(
offshore_netzumlage_ct_per_kwh(year, Befreit),
Some(Decimal::ZERO)
);
assert_eq!(kwkg_umlage_ct_per_kwh(year, Befreit), Some(Decimal::ZERO));
}
}
#[test]
fn an_uncovered_year_has_no_rate() {
use Letztverbrauchergruppe as G;
assert_eq!(sect19_stromnev_ct_per_kwh(2025, G::A), None);
assert_eq!(offshore_netzumlage_ct_per_kwh(2027, G::A), None);
assert_eq!(kwkg_umlage_ct_per_kwh(1999, G::A), None);
}
#[test]
fn privileged_bands_are_lower_than_the_full_rate() {
use Letztverbrauchergruppe as G;
let a = sect19_stromnev_ct_per_kwh(2026, G::A).unwrap();
let b = sect19_stromnev_ct_per_kwh(2026, G::B).unwrap();
let c = sect19_stromnev_ct_per_kwh(2026, G::C).unwrap();
assert!(b < a, "B′ must be below A′");
assert!(c < b, "C′ must be below B′");
}
#[test]
fn an_unprivileged_group_is_never_split() {
use Letztverbrauchergruppe as G;
for g in [G::A, G::Befreit] {
let a = aufteilung(g, dec!(5_000_000), dec!(3_000_000));
assert_eq!(a.voller_satz_kwh, dec!(5_000_000), "{g:?}");
assert_eq!(a.privilegiert_kwh, Decimal::ZERO, "{g:?}");
}
}
#[test]
fn the_first_gigawatt_hour_of_the_year_is_never_privileged() {
let a = aufteilung(Letztverbrauchergruppe::B, dec!(2_500_000), Decimal::ZERO);
assert_eq!(a.voller_satz_kwh, ENFG_SCHWELLE_KWH);
assert_eq!(a.privilegiert_kwh, dec!(1_500_000));
}
#[test]
fn a_period_past_the_threshold_is_wholly_privileged() {
let a = aufteilung(Letztverbrauchergruppe::C, dec!(400_000), dec!(1_000_000));
assert_eq!(a.voller_satz_kwh, Decimal::ZERO);
assert_eq!(a.privilegiert_kwh, dec!(400_000));
}
#[test]
fn a_straddling_period_splits_at_the_boundary() {
let a = aufteilung(Letztverbrauchergruppe::B, dec!(300_000), dec!(900_000));
assert_eq!(a.voller_satz_kwh, dec!(100_000));
assert_eq!(a.privilegiert_kwh, dec!(200_000));
}
#[test]
fn the_split_conserves_the_quantity() {
use Letztverbrauchergruppe as G;
for g in [G::A, G::B, G::C, G::Befreit] {
for vor in [dec!(0), dec!(999_999), dec!(1_000_000), dec!(9_000_000)] {
let menge = dec!(750_000);
let a = aufteilung(g, menge, vor);
assert_eq!(
a.voller_satz_kwh + a.privilegiert_kwh,
menge,
"{g:?} after {vor}"
);
assert!(a.voller_satz_kwh >= Decimal::ZERO);
assert!(a.privilegiert_kwh >= Decimal::ZERO);
}
}
}
}