use rust_decimal::Decimal;
use rust_decimal::dec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MessstellenKategorie {
Pflichteinbau(PflichtEinstufung),
OptionalerEinbau,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct PflichtEinstufung {
#[serde(default)]
pub jahresverbrauch_kwh: Option<Decimal>,
#[serde(default)]
pub installierte_leistung_kw: Option<Decimal>,
#[serde(default)]
pub steuerbare_verbrauchseinrichtung: bool,
}
impl PflichtEinstufung {
#[must_use]
pub fn band(&self) -> PflichtBand {
let kwh = self.jahresverbrauch_kwh.unwrap_or(Decimal::ZERO);
let kw = self.installierte_leistung_kw.unwrap_or(Decimal::ZERO);
if kwh > dec!(100_000) || kw > dec!(100) {
PflichtBand::Ueber100000
} else if kwh > dec!(50_000) || kw > dec!(25) {
PflichtBand::Bis100000
} else if kwh > dec!(20_000) || kw > dec!(15) {
PflichtBand::Bis50000
} else if kwh > dec!(10_000) || kw > dec!(7) || self.steuerbare_verbrauchseinrichtung {
PflichtBand::Bis20000
} else {
PflichtBand::Bis10000
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum PflichtBand {
Bis10000,
Bis20000,
Bis50000,
Bis100000,
Ueber100000,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Entgeltschuldner {
Netzbetreiber,
Letztverbraucher,
}
pub const SCHEDULE_AB: time::Date = time::macros::date!(2025 - 01 - 01);
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Preisobergrenze {
Betrag(Decimal),
Angemessen,
VorSchedule,
}
impl Preisobergrenze {
#[must_use]
pub const fn betrag(self) -> Option<Decimal> {
match self {
Self::Betrag(eur) => Some(eur),
Self::Angemessen | Self::VorSchedule => None,
}
}
}
#[must_use]
pub fn preisobergrenze_eur_per_jahr(
kategorie: MessstellenKategorie,
schuldner: Entgeltschuldner,
period_to: time::Date,
) -> Preisobergrenze {
use Entgeltschuldner as E;
use MessstellenKategorie as K;
use PflichtBand as B;
use Preisobergrenze as P;
if period_to < SCHEDULE_AB {
return P::VorSchedule;
}
match (kategorie, schuldner) {
(K::Pflichteinbau(_), E::Netzbetreiber) => P::Betrag(dec!(80)),
(K::Pflichteinbau(einstufung), E::Letztverbraucher) => match einstufung.band() {
B::Bis10000 => P::Betrag(dec!(40)),
B::Bis20000 => P::Betrag(dec!(50)),
B::Bis50000 => P::Betrag(dec!(110)),
B::Bis100000 => P::Betrag(dec!(140)),
B::Ueber100000 => P::Angemessen,
},
(K::OptionalerEinbau, _) => P::Betrag(dec!(30)),
}
}
#[must_use]
pub fn gesamtobergrenze_eur_per_jahr(
kategorie: MessstellenKategorie,
period_to: time::Date,
) -> Option<Decimal> {
let nb = preisobergrenze_eur_per_jahr(kategorie, Entgeltschuldner::Netzbetreiber, period_to)
.betrag()?;
let lv = preisobergrenze_eur_per_jahr(kategorie, Entgeltschuldner::Letztverbraucher, period_to)
.betrag()?;
Some(nb + lv)
}
pub const STEUEREINRICHTUNG_OBERGRENZE_EUR_PER_JAHR: Decimal = dec!(50);
#[cfg(test)]
mod tests {
use super::*;
use Entgeltschuldner as E;
use MessstellenKategorie as K;
use PflichtBand as B;
const IM_SCHEDULE: time::Date = time::macros::date!(2026 - 12 - 31);
fn verbrauch(kwh: Decimal) -> MessstellenKategorie {
K::Pflichteinbau(PflichtEinstufung {
jahresverbrauch_kwh: Some(kwh),
..PflichtEinstufung::default()
})
}
fn leistung(kw: Decimal) -> PflichtEinstufung {
PflichtEinstufung {
installierte_leistung_kw: Some(kw),
..PflichtEinstufung::default()
}
}
#[test]
fn the_pflichteinbau_schedule() {
for (kwh, lv, total) in [
(dec!(9_000), dec!(40), dec!(120)),
(dec!(15_000), dec!(50), dec!(130)),
(dec!(30_000), dec!(110), dec!(190)),
(dec!(80_000), dec!(140), dec!(220)),
] {
let k = verbrauch(kwh);
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Netzbetreiber, IM_SCHEDULE),
Preisobergrenze::Betrag(dec!(80))
);
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
Preisobergrenze::Betrag(lv)
);
assert_eq!(gesamtobergrenze_eur_per_jahr(k, IM_SCHEDULE), Some(total));
}
}
#[test]
fn the_consumption_bounds_are_exclusive_below_and_inclusive_above() {
for (kwh, band) in [
(dec!(6_001), B::Bis10000),
(dec!(10_000), B::Bis10000),
(dec!(10_001), B::Bis20000),
(dec!(20_000), B::Bis20000),
(dec!(20_001), B::Bis50000),
(dec!(50_000), B::Bis50000),
(dec!(50_001), B::Bis100000),
(dec!(100_000), B::Bis100000),
(dec!(100_001), B::Ueber100000),
] {
let einstufung = PflichtEinstufung {
jahresverbrauch_kwh: Some(kwh),
..PflichtEinstufung::default()
};
assert_eq!(einstufung.band(), band, "{kwh} kWh");
}
}
#[test]
fn the_capacity_bounds_classify_a_generating_point() {
for (kw, band) in [
(dec!(7), B::Bis10000),
(dec!(7.5), B::Bis20000),
(dec!(15), B::Bis20000),
(dec!(15.1), B::Bis50000),
(dec!(25), B::Bis50000),
(dec!(25.1), B::Bis100000),
(dec!(100), B::Bis100000),
(dec!(100.1), B::Ueber100000),
] {
assert_eq!(leistung(kw).band(), band, "{kw} kW");
}
}
#[test]
fn a_sect14a_zaehlpunkt_reaches_the_nummer_four_band() {
let einstufung = PflichtEinstufung {
jahresverbrauch_kwh: Some(dec!(7_000)),
steuerbare_verbrauchseinrichtung: true,
..PflichtEinstufung::default()
};
assert_eq!(einstufung.band(), B::Bis20000);
assert_eq!(
preisobergrenze_eur_per_jahr(
K::Pflichteinbau(einstufung),
E::Letztverbraucher,
IM_SCHEDULE
),
Preisobergrenze::Betrag(dec!(50))
);
}
#[test]
fn the_highest_matching_nummer_wins() {
let einstufung = PflichtEinstufung {
jahresverbrauch_kwh: Some(dec!(7_000)),
installierte_leistung_kw: Some(dec!(60)),
steuerbare_verbrauchseinrichtung: true,
};
assert_eq!(
einstufung.band(),
B::Bis100000,
"60 kW is Nr. 2 — the consumption and the §14a fact classify lower"
);
}
#[test]
fn an_unclassifiable_point_takes_the_tightest_ceiling() {
let k = K::Pflichteinbau(PflichtEinstufung::default());
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
Preisobergrenze::Betrag(dec!(40))
);
}
#[test]
fn the_top_band_caps_only_the_grid_operators_share() {
let k = verbrauch(dec!(250_000));
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Netzbetreiber, IM_SCHEDULE),
Preisobergrenze::Betrag(dec!(80))
);
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
Preisobergrenze::Angemessen
);
assert_eq!(
gesamtobergrenze_eur_per_jahr(k, IM_SCHEDULE),
None,
"no total where one share is open"
);
}
#[test]
fn an_optional_installation_is_capped_at_thirty_each() {
for schuldner in [E::Netzbetreiber, E::Letztverbraucher] {
assert_eq!(
preisobergrenze_eur_per_jahr(K::OptionalerEinbau, schuldner, IM_SCHEDULE),
Preisobergrenze::Betrag(dec!(30))
);
}
assert_eq!(
gesamtobergrenze_eur_per_jahr(K::OptionalerEinbau, IM_SCHEDULE),
Some(dec!(60))
);
}
#[test]
fn a_period_before_the_schedule_is_not_measured_against_it() {
let vorher = time::macros::date!(2024 - 12 - 31);
let k = verbrauch(dec!(9_000));
for schuldner in [E::Netzbetreiber, E::Letztverbraucher] {
assert_eq!(
preisobergrenze_eur_per_jahr(k, schuldner, vorher),
Preisobergrenze::VorSchedule
);
}
assert_eq!(gesamtobergrenze_eur_per_jahr(k, vorher), None);
assert_eq!(
preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, SCHEDULE_AB),
Preisobergrenze::Betrag(dec!(40))
);
}
#[test]
fn only_a_fixed_ceiling_yields_a_number() {
assert_eq!(Preisobergrenze::Betrag(dec!(80)).betrag(), Some(dec!(80)));
assert_eq!(Preisobergrenze::Angemessen.betrag(), None);
assert_eq!(Preisobergrenze::VorSchedule.betrag(), None);
}
#[test]
fn the_bands_rise_monotonically() {
let mut previous = Decimal::ZERO;
for kwh in [dec!(9_000), dec!(15_000), dec!(30_000), dec!(80_000)] {
let ceiling =
preisobergrenze_eur_per_jahr(verbrauch(kwh), E::Letztverbraucher, IM_SCHEDULE)
.betrag()
.expect("a fixed ceiling");
assert!(
ceiling > previous,
"{kwh} kWh must exceed the band below it"
);
previous = ceiling;
}
}
}