use crate::rounding::RoundMoney;
use rust_decimal::Decimal;
use rust_decimal::dec;
use time::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum KwkAnlagenart {
Neu,
Modernisiert,
Nachgeruestet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum KwkVerwendung {
NetzDerAllgemeinenVersorgung,
NichtEingespeistBis100Kw,
NichtEingespeistKundenanlage,
NichtEingespeistStromkostenintensiv,
NichtEingespeistBrancheAnlage2,
}
impl KwkVerwendung {
#[must_use]
pub fn ist_eingespeist(self) -> bool {
self == Self::NetzDerAllgemeinenVersorgung
}
}
type Staffel = &'static [(Option<Decimal>, Decimal)];
static ABS1_UNTER_2MW: [(Option<Decimal>, Decimal); 4] = [
(Some(dec!(50)), dec!(8)),
(Some(dec!(100)), dec!(6)),
(Some(dec!(250)), dec!(5)),
(Some(dec!(2_000)), dec!(4.4)),
];
const ABS1_NR5_NEU_MODERNISIERT: Decimal = dec!(3.4);
const ABS1_NR5_NACHGERUESTET: Decimal = dec!(3.1);
const ABS1_SATZ2_ERHOEHUNG_CT: Decimal = dec!(0.5);
const SATZ2_FRUEHESTE_INBETRIEBNAHME: Date = time::macros::date!(2023 - 01 - 01);
const ABS1_NR5_FRUEHESTER_DAUERBETRIEB: Date = time::macros::date!(2020 - 12 - 31);
static ABS2_NR1: [(Option<Decimal>, Decimal); 2] =
[(Some(dec!(50)), dec!(4)), (Some(dec!(100)), dec!(3))];
static ABS2_NR2: [(Option<Decimal>, Decimal); 5] = [
(Some(dec!(50)), dec!(4)),
(Some(dec!(100)), dec!(3)),
(Some(dec!(250)), dec!(2)),
(Some(dec!(2_000)), dec!(1.5)),
(None, dec!(1)),
];
static ABS2_NR3: [(Option<Decimal>, Decimal); 4] = [
(Some(dec!(50)), dec!(5.41)),
(Some(dec!(250)), dec!(4)),
(Some(dec!(2_000)), dec!(2.4)),
(None, dec!(1.8)),
];
const ABS3A_EINGESPEIST_CT: Decimal = dec!(16);
const ABS3A_NICHT_EINGESPEIST_CT: Decimal = dec!(8);
const ABS3A_GRENZE_KW: Decimal = dec!(50);
const ABS3A_FRUEHESTER_DAUERBETRIEB: Date = time::macros::date!(2019 - 12 - 31);
#[derive(Debug, Clone, PartialEq)]
pub struct KwkZuschlagInput {
pub kwk_leistung_kw: Decimal,
pub anlagenart: KwkAnlagenart,
pub verwendung: KwkVerwendung,
pub dauerbetrieb: Date,
pub bmwk_feststellung_veroeffentlicht: bool,
}
impl KwkZuschlagInput {
#[must_use]
pub fn neu_eingespeist(kwk_leistung_kw: Decimal, dauerbetrieb: Date) -> Self {
Self {
kwk_leistung_kw,
anlagenart: KwkAnlagenart::Neu,
verwendung: KwkVerwendung::NetzDerAllgemeinenVersorgung,
dauerbetrieb,
bmwk_feststellung_veroeffentlicht: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct KwkLeistungsanteil {
pub von_kw: Decimal,
pub bis_kw: Option<Decimal>,
pub anteil_kw: Decimal,
pub satz_ct: Decimal,
pub rechtsgrundlage: &'static str,
}
fn abs1_staffel(anlagenart: KwkAnlagenart) -> (Staffel, Decimal, &'static str) {
let (top_ct, top_basis) = match anlagenart {
KwkAnlagenart::Neu => (
ABS1_NR5_NEU_MODERNISIERT,
"§ 7 Abs. 1 Satz 1 Nr. 5 lit. a KWKG",
),
KwkAnlagenart::Modernisiert => (
ABS1_NR5_NEU_MODERNISIERT,
"§ 7 Abs. 1 Satz 1 Nr. 5 lit. b KWKG",
),
KwkAnlagenart::Nachgeruestet => (
ABS1_NR5_NACHGERUESTET,
"§ 7 Abs. 1 Satz 1 Nr. 5 lit. c KWKG",
),
};
(&ABS1_UNTER_2MW, top_ct, top_basis)
}
fn staffel(input: &KwkZuschlagInput) -> Option<Vec<(Option<Decimal>, Decimal, &'static str)>> {
let mut bands: Vec<(Option<Decimal>, Decimal, &'static str)> = Vec::new();
match input.verwendung {
KwkVerwendung::NetzDerAllgemeinenVersorgung => {
let (unter_2mw, top_ct, top_basis) = abs1_staffel(input.anlagenart);
const NUMMERN: [&str; 4] = [
"§ 7 Abs. 1 Satz 1 Nr. 1 KWKG",
"§ 7 Abs. 1 Satz 1 Nr. 2 KWKG",
"§ 7 Abs. 1 Satz 1 Nr. 3 KWKG",
"§ 7 Abs. 1 Satz 1 Nr. 4 KWKG",
];
for (i, (bis, ct)) in unter_2mw.iter().enumerate() {
bands.push((*bis, *ct, NUMMERN[i]));
}
let satz2 = input.anlagenart == KwkAnlagenart::Neu
&& input.bmwk_feststellung_veroeffentlicht
&& input.dauerbetrieb >= SATZ2_FRUEHESTE_INBETRIEBNAHME;
if input.dauerbetrieb > ABS1_NR5_FRUEHESTER_DAUERBETRIEB {
let top = if satz2 {
top_ct + ABS1_SATZ2_ERHOEHUNG_CT
} else {
top_ct
};
bands.push((None, top, top_basis));
}
}
KwkVerwendung::NichtEingespeistBis100Kw => {
for (i, (bis, ct)) in ABS2_NR1.iter().enumerate() {
bands.push((
*bis,
*ct,
[
"§ 7 Abs. 2 Nr. 1 lit. a KWKG",
"§ 7 Abs. 2 Nr. 1 lit. b KWKG",
][i],
));
}
}
KwkVerwendung::NichtEingespeistKundenanlage => {
const LIT: [&str; 5] = [
"§ 7 Abs. 2 Nr. 2 lit. a KWKG",
"§ 7 Abs. 2 Nr. 2 lit. b KWKG",
"§ 7 Abs. 2 Nr. 2 lit. c KWKG",
"§ 7 Abs. 2 Nr. 2 lit. d KWKG",
"§ 7 Abs. 2 Nr. 2 lit. e KWKG",
];
for (i, (bis, ct)) in ABS2_NR2.iter().enumerate() {
bands.push((*bis, *ct, LIT[i]));
}
}
KwkVerwendung::NichtEingespeistStromkostenintensiv => {
const LIT: [&str; 4] = [
"§ 7 Abs. 2 Nr. 3 lit. a KWKG",
"§ 7 Abs. 2 Nr. 3 lit. b KWKG",
"§ 7 Abs. 2 Nr. 3 lit. c KWKG",
"§ 7 Abs. 2 Nr. 3 lit. d KWKG",
];
for (i, (bis, ct)) in ABS2_NR3.iter().enumerate() {
bands.push((*bis, *ct, LIT[i]));
}
}
KwkVerwendung::NichtEingespeistBrancheAnlage2 => return None,
}
Some(bands)
}
fn abs3a_satz_ct(input: &KwkZuschlagInput) -> Option<(Decimal, &'static str)> {
if input.anlagenart != KwkAnlagenart::Neu
|| input.kwk_leistung_kw > ABS3A_GRENZE_KW
|| input.dauerbetrieb <= ABS3A_FRUEHESTER_DAUERBETRIEB
{
return None;
}
match input.verwendung {
KwkVerwendung::NetzDerAllgemeinenVersorgung => {
Some((ABS3A_EINGESPEIST_CT, "§ 7 Abs. 3a Nr. 1 KWKG"))
}
KwkVerwendung::NichtEingespeistBrancheAnlage2 => None,
_ => Some((ABS3A_NICHT_EINGESPEIST_CT, "§ 7 Abs. 3a Nr. 2 KWKG")),
}
}
#[must_use]
pub fn zuschlag_leistungsanteile(input: &KwkZuschlagInput) -> Option<Vec<KwkLeistungsanteil>> {
if input.kwk_leistung_kw <= Decimal::ZERO {
return None;
}
if let Some((ct, basis)) = abs3a_satz_ct(input) {
return Some(vec![KwkLeistungsanteil {
von_kw: Decimal::ZERO,
bis_kw: Some(ABS3A_GRENZE_KW),
anteil_kw: input.kwk_leistung_kw,
satz_ct: ct,
rechtsgrundlage: basis,
}]);
}
let bands = staffel(input)?;
let mut anteile = Vec::with_capacity(bands.len());
let mut untergrenze = Decimal::ZERO;
for (bis, ct, basis) in bands {
let obergrenze = bis
.unwrap_or(input.kwk_leistung_kw)
.min(input.kwk_leistung_kw);
let anteil = obergrenze - untergrenze;
if anteil > Decimal::ZERO {
anteile.push(KwkLeistungsanteil {
von_kw: untergrenze,
bis_kw: bis,
anteil_kw: anteil,
satz_ct: ct,
rechtsgrundlage: basis,
});
}
untergrenze = obergrenze;
if untergrenze >= input.kwk_leistung_kw {
break;
}
}
(untergrenze >= input.kwk_leistung_kw).then_some(anteile)
}
#[must_use]
pub fn zuschlag_ct_kwh(input: &KwkZuschlagInput) -> Option<Decimal> {
let anteile = zuschlag_leistungsanteile(input)?;
let summe: Decimal = anteile.iter().map(|a| a.anteil_kw * a.satz_ct).sum();
Some(summe / input.kwk_leistung_kw)
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct KwkFoerderdauerInput {
pub anlagenart: KwkAnlagenart,
pub kostenanteil: Option<Decimal>,
pub jahre_seit_dauerbetrieb: Option<u32>,
pub ist_dampfsammelschiene_ueber_50_mw: bool,
}
#[must_use]
pub fn foerderdauer_vollbenutzungsstunden(input: &KwkFoerderdauerInput) -> Option<u32> {
match input.anlagenart {
KwkAnlagenart::Neu => Some(30_000),
KwkAnlagenart::Modernisiert => {
let anteil = input.kostenanteil?;
let jahre = input.jahre_seit_dauerbetrieb.unwrap_or(0);
if anteil >= dec!(0.50) && jahre >= 10 {
Some(30_000)
} else if anteil >= dec!(0.25) && jahre >= 5 {
Some(15_000)
} else if anteil >= dec!(0.10) && jahre >= 2 && input.ist_dampfsammelschiene_ueber_50_mw
{
Some(6_000)
} else {
None
}
}
KwkAnlagenart::Nachgeruestet => {
let anteil = input.kostenanteil?;
if anteil >= dec!(0.50) {
Some(30_000)
} else if anteil >= dec!(0.25) {
Some(15_000)
} else if anteil >= dec!(0.10) {
Some(10_000)
} else {
None
}
}
}
}
#[must_use]
pub fn jahreshoechstbetrag_vollbenutzungsstunden(kalenderjahr: i32) -> Option<u32> {
Some(match kalenderjahr {
..=2020 => return None,
2021 | 2022 => 5_000,
2023 | 2024 => 4_000,
2025 => 3_500,
2026 => 3_300,
2027 => 3_100,
2028 => 2_900,
2029 => 2_700,
_ => 2_500,
})
}
#[must_use]
pub fn jahreskontingent_kwh(kwk_leistung_kw: Decimal, kalenderjahr: i32) -> Option<Decimal> {
let stunden = jahreshoechstbetrag_vollbenutzungsstunden(kalenderjahr)?;
Some((kwk_leistung_kw * Decimal::from(stunden)).round_kfm(3))
}
#[cfg(test)]
mod statutory_kwkg_tests {
use super::*;
use time::macros::date;
const IBN: Date = date!(2024 - 03 - 01);
fn neu(kw: Decimal) -> KwkZuschlagInput {
KwkZuschlagInput::neu_eingespeist(kw, IBN)
}
#[test]
fn abs1_prices_each_leistungsanteil_at_its_own_rate() {
assert_eq!(zuschlag_ct_kwh(&neu(dec!(100))), Some(dec!(7)));
assert_eq!(zuschlag_ct_kwh(&neu(dec!(250))), Some(dec!(5.8)));
assert_eq!(zuschlag_ct_kwh(&neu(dec!(2_000))), Some(dec!(4.575)));
}
#[test]
fn abs1_nr5_splits_by_anlagenart() {
let at = |art| {
zuschlag_leistungsanteile(&KwkZuschlagInput {
anlagenart: art,
..neu(dec!(5_000))
})
.expect("§ 7 Abs. 1 has an open top band")
.last()
.expect("five bands")
.satz_ct
};
assert_eq!(at(KwkAnlagenart::Neu), dec!(3.4));
assert_eq!(at(KwkAnlagenart::Modernisiert), dec!(3.4));
assert_eq!(at(KwkAnlagenart::Nachgeruestet), dec!(3.1));
}
#[test]
fn abs1_satz2_uplift_needs_the_bundesanzeiger_publication() {
let top = |input: &KwkZuschlagInput| {
zuschlag_leistungsanteile(input)
.expect("open top band")
.last()
.expect("bands")
.satz_ct
};
let ohne = neu(dec!(5_000));
assert_eq!(top(&ohne), dec!(3.4));
let mit = KwkZuschlagInput {
bmwk_feststellung_veroeffentlicht: true,
..ohne.clone()
};
assert_eq!(top(&mit), dec!(3.9));
let alt = KwkZuschlagInput {
dauerbetrieb: date!(2022 - 12 - 31),
..mit.clone()
};
assert_eq!(top(&alt), dec!(3.4));
let modernisiert = KwkZuschlagInput {
anlagenart: KwkAnlagenart::Modernisiert,
..mit
};
assert_eq!(top(&modernisiert), dec!(3.4));
}
#[test]
fn abs2_is_its_own_ladder_per_sect6_abs3_nummer() {
let with = |v| KwkZuschlagInput {
verwendung: v,
anlagenart: KwkAnlagenart::Modernisiert,
..neu(dec!(100))
};
assert_eq!(
zuschlag_ct_kwh(&with(KwkVerwendung::NichtEingespeistBis100Kw)),
Some(dec!(3.5))
);
assert_eq!(
zuschlag_ct_kwh(&with(KwkVerwendung::NichtEingespeistKundenanlage)),
Some(dec!(3.5))
);
assert_eq!(
zuschlag_ct_kwh(&with(KwkVerwendung::NichtEingespeistStromkostenintensiv)),
Some(dec!(4.705))
);
}
#[test]
fn abs2_nr1_has_no_rate_above_100_kw() {
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput {
verwendung: KwkVerwendung::NichtEingespeistBis100Kw,
anlagenart: KwkAnlagenart::Modernisiert,
..neu(dec!(150))
}),
None
);
}
#[test]
fn abs3_has_no_statutory_rate() {
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput {
verwendung: KwkVerwendung::NichtEingespeistBrancheAnlage2,
..neu(dec!(80))
}),
None
);
}
#[test]
fn abs3a_displaces_the_ladders_for_new_plants_up_to_50_kw() {
assert_eq!(zuschlag_ct_kwh(&neu(dec!(30))), Some(dec!(16)));
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput {
verwendung: KwkVerwendung::NichtEingespeistKundenanlage,
..neu(dec!(30))
}),
Some(dec!(8))
);
assert_eq!(
zuschlag_ct_kwh(&neu(dec!(100))),
Some(dec!(7)),
"above 50 kW the Abs. 1 Staffel applies"
);
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput {
dauerbetrieb: date!(2019 - 06 - 01),
..neu(dec!(30))
}),
Some(dec!(8))
);
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput {
anlagenart: KwkAnlagenart::Modernisiert,
..neu(dec!(30))
}),
Some(dec!(8))
);
}
#[test]
fn sect8_keys_on_anlagenart_and_kostenanteil() {
let base = KwkFoerderdauerInput {
anlagenart: KwkAnlagenart::Neu,
kostenanteil: None,
jahre_seit_dauerbetrieb: None,
ist_dampfsammelschiene_ueber_50_mw: false,
};
assert_eq!(foerderdauer_vollbenutzungsstunden(&base), Some(30_000));
let modern = |anteil, jahre| KwkFoerderdauerInput {
anlagenart: KwkAnlagenart::Modernisiert,
kostenanteil: Some(anteil),
jahre_seit_dauerbetrieb: Some(jahre),
ist_dampfsammelschiene_ueber_50_mw: false,
};
assert_eq!(
foerderdauer_vollbenutzungsstunden(&modern(dec!(0.50), 10)),
Some(30_000)
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&modern(dec!(0.50), 6)),
Some(15_000),
"the 30 000 h tier needs ten years, the 15 000 h tier five"
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&modern(dec!(0.30), 4)),
None,
"inside the Karenzzeit no Nummer is met"
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&modern(dec!(0.10), 3)),
None
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&KwkFoerderdauerInput {
ist_dampfsammelschiene_ueber_50_mw: true,
..modern(dec!(0.10), 3)
}),
Some(6_000)
);
let nach = |anteil| KwkFoerderdauerInput {
anlagenart: KwkAnlagenart::Nachgeruestet,
kostenanteil: Some(anteil),
..base.clone()
};
assert_eq!(
foerderdauer_vollbenutzungsstunden(&nach(dec!(0.10))),
Some(10_000)
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&nach(dec!(0.25))),
Some(15_000)
);
assert_eq!(
foerderdauer_vollbenutzungsstunden(&nach(dec!(0.50))),
Some(30_000)
);
assert_eq!(foerderdauer_vollbenutzungsstunden(&nach(dec!(0.09))), None);
}
#[test]
fn sect8_abs4_caps_each_calendar_year() {
for (jahr, stunden) in [
(2021, 5_000),
(2022, 5_000),
(2023, 4_000),
(2024, 4_000),
(2025, 3_500),
(2026, 3_300),
(2027, 3_100),
(2028, 2_900),
(2029, 2_700),
(2030, 2_500),
(2040, 2_500),
] {
assert_eq!(
jahreshoechstbetrag_vollbenutzungsstunden(jahr),
Some(stunden),
"{jahr}"
);
}
assert_eq!(jahreshoechstbetrag_vollbenutzungsstunden(2020), None);
assert_eq!(
jahreskontingent_kwh(dec!(500), 2026),
Some(dec!(1650000.000))
);
}
#[test]
fn abs1_nr5_reaches_only_dauerbetrieb_after_2020() {
let am = |tag| KwkZuschlagInput::neu_eingespeist(dec!(5_000), tag);
assert_eq!(zuschlag_ct_kwh(&am(date!(2020 - 12 - 31))), None);
assert_eq!(zuschlag_ct_kwh(&am(date!(2019 - 07 - 01))), None);
assert_eq!(
zuschlag_ct_kwh(&am(date!(2021 - 01 - 01))),
Some(dec!(3.87))
);
assert_eq!(
zuschlag_ct_kwh(&KwkZuschlagInput::neu_eingespeist(
dec!(2_000),
date!(2019 - 07 - 01)
)),
Some(dec!(4.575))
);
}
}