use crate::rounding::RoundMoney;
use rust_decimal::Decimal;
use rust_decimal::dec;
use time::{Date, Month};
pub const HORIZONT: Date = time::macros::date!(2030 - 12 - 31);
const EEG2023_START: Date = time::macros::date!(2023 - 01 - 01);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerguetungssatzRow {
pub erzeugungsart: &'static str,
pub leistung_min_kwp: Decimal,
pub leistung_max_kwp: Option<Decimal>,
pub verguetungsform: &'static str,
pub verguetungssatz_ct: Decimal,
pub billing_start: Date,
pub billing_end: Date,
pub eeg_gesetz: i16,
pub notes: String,
}
struct Band {
max: Option<Decimal>,
startwert_ct: Decimal,
nummer: &'static str,
}
const fn band(max: Option<Decimal>, startwert_ct: Decimal, nummer: &'static str) -> Band {
Band {
max,
startwert_ct,
nummer,
}
}
#[derive(Clone, Copy)]
enum Absenkung {
Halbjaehrlich,
Jaehrlich(crate::degression::JaehrlicheAbsenkung),
}
impl Absenkung {
fn fensterstarts(self) -> Vec<Date> {
let mut starts = vec![EEG2023_START];
let mut next = match self {
Self::Halbjaehrlich => crate::degression::DEGRESSIONSBEGINN,
Self::Jaehrlich(a) => a.beginn,
};
while next <= HORIZONT {
starts.push(next);
next = match self {
Self::Halbjaehrlich => plus_monate(next, 6),
Self::Jaehrlich(_) => plus_monate(next, 12),
};
}
starts
}
fn wert(self, startwert_ct: Decimal, ibn: Date) -> Decimal {
match self {
Self::Halbjaehrlich => {
crate::degression::anzulegender_wert_bei_inbetriebnahme(startwert_ct, ibn)
}
Self::Jaehrlich(a) => a.anzulegender_wert(startwert_ct, ibn),
}
}
}
fn plus_monate(von: Date, monate: u32) -> Date {
let total = i32::from(von.month() as u8 - 1) + i32::try_from(monate).unwrap_or(0);
let jahr = von.year() + total / 12;
let monat =
Month::try_from(u8::try_from(total % 12 + 1).unwrap_or(1)).unwrap_or(Month::January);
Date::from_calendar_date(jahr, monat, 1).unwrap_or(von)
}
fn tag_davor(d: Date) -> Date {
d.previous_day().unwrap_or(d)
}
fn sect53_ct(erzeugungsart: &str) -> Decimal {
if erzeugungsart.starts_with("SOLAR") || erzeugungsart.starts_with("WIND") {
dec!(0.4)
} else {
dec!(0.2)
}
}
fn staffel(
erzeugungsart: &'static str,
verguetungsform: &'static str,
bands: &[Band],
absenkung: Absenkung,
out: &mut Vec<VerguetungssatzRow>,
) {
let abzug = sect53_ct(erzeugungsart);
let starts = absenkung.fensterstarts();
for (i, start) in starts.iter().copied().enumerate() {
let ende = starts.get(i + 1).map_or(HORIZONT, |n| tag_davor(*n));
if ende < start {
continue;
}
let mut min = Decimal::ZERO;
for b in bands {
out.push(VerguetungssatzRow {
erzeugungsart,
leistung_min_kwp: min,
leistung_max_kwp: b.max,
verguetungsform,
verguetungssatz_ct: absenkung.wert(b.startwert_ct, start) - abzug,
billing_start: start,
billing_end: ende,
eeg_gesetz: 2023,
notes: b.nummer.to_owned(),
});
min = b.max.unwrap_or(min);
}
}
}
#[must_use]
pub fn verguetungssatz_rows() -> Vec<VerguetungssatzRow> {
use crate::degression::JaehrlicheAbsenkung as A;
let mut out = Vec::new();
staffel(
"SOLAR_AUFDACH",
"UEBERSCHUSS",
&[
band(Some(dec!(10)), dec!(8.60), "§48 Abs. 2 Nr. 1 EEG 2023"),
band(Some(dec!(40)), dec!(7.50), "§48 Abs. 2 Nr. 2 EEG 2023"),
band(Some(dec!(1000)), dec!(6.20), "§48 Abs. 2 Nr. 3 EEG 2023"),
],
Absenkung::Halbjaehrlich,
&mut out,
);
staffel(
"SOLAR_AUFDACH",
"VOLLEINSPEISUNG",
&[
band(
Some(dec!(10)),
dec!(13.40),
"§48 Abs. 2 Nr. 1 + Abs. 2a Nr. 1 EEG 2023",
),
band(
Some(dec!(40)),
dec!(11.30),
"§48 Abs. 2 Nr. 2 + Abs. 2a Nr. 2 EEG 2023",
),
band(
Some(dec!(100)),
dec!(11.30),
"§48 Abs. 2 Nr. 3 + Abs. 2a Nr. 3 EEG 2023",
),
band(
Some(dec!(400)),
dec!(9.40),
"§48 Abs. 2 Nr. 3 + Abs. 2a Nr. 4 EEG 2023",
),
band(
Some(dec!(1000)),
dec!(8.10),
"§48 Abs. 2 Nr. 3 + Abs. 2a Nr. 5 EEG 2023",
),
],
Absenkung::Halbjaehrlich,
&mut out,
);
staffel(
"WASSERKRAFT",
"UEBERSCHUSS",
&[
band(Some(dec!(500)), dec!(12.03), "§40 Abs. 1 Nr. 1 EEG 2023"),
band(Some(dec!(2000)), dec!(7.93), "§40 Abs. 1 Nr. 2 EEG 2023"),
band(Some(dec!(5000)), dec!(6.07), "§40 Abs. 1 Nr. 3 EEG 2023"),
band(Some(dec!(10000)), dec!(5.32), "§40 Abs. 1 Nr. 4 EEG 2023"),
band(Some(dec!(20000)), dec!(5.13), "§40 Abs. 1 Nr. 5 EEG 2023"),
band(Some(dec!(50000)), dec!(4.12), "§40 Abs. 1 Nr. 6 EEG 2023"),
band(None, dec!(3.37), "§40 Abs. 1 Nr. 7 EEG 2023"),
],
Absenkung::Jaehrlich(A::WASSERKRAFT),
&mut out,
);
staffel(
"DEPONIEGAS",
"UEBERSCHUSS",
&[
band(Some(dec!(500)), dec!(7.46), "§41 Abs. 1 Nr. 1 EEG 2023"),
band(Some(dec!(5000)), dec!(5.17), "§41 Abs. 1 Nr. 2 EEG 2023"),
],
Absenkung::Jaehrlich(A::GASE),
&mut out,
);
staffel(
"KLAERGAS",
"UEBERSCHUSS",
&[
band(Some(dec!(500)), dec!(5.93), "§41 Abs. 2 Nr. 1 EEG 2023"),
band(Some(dec!(5000)), dec!(5.17), "§41 Abs. 2 Nr. 2 EEG 2023"),
],
Absenkung::Jaehrlich(A::GASE),
&mut out,
);
staffel(
"GRUBENGAS",
"UEBERSCHUSS",
&[
band(Some(dec!(1000)), dec!(5.98), "§41 Abs. 3 Nr. 1 EEG 2023"),
band(Some(dec!(5000)), dec!(3.81), "§41 Abs. 3 Nr. 2 EEG 2023"),
band(None, dec!(3.37), "§41 Abs. 3 Nr. 3 EEG 2023"),
],
Absenkung::Jaehrlich(A::GASE),
&mut out,
);
staffel(
"BIOMASSE",
"UEBERSCHUSS",
&[band(Some(dec!(150)), dec!(12.67), "§42 Satz 1 EEG 2023")],
Absenkung::Jaehrlich(A::BIOMASSE),
&mut out,
);
staffel(
"BIOGAS",
"UEBERSCHUSS",
&[
band(Some(dec!(500)), dec!(14.16), "§43 Abs. 1 Nr. 1 EEG 2023"),
band(Some(dec!(20000)), dec!(12.41), "§43 Abs. 1 Nr. 2 EEG 2023"),
],
Absenkung::Jaehrlich(A::BIOMASSE),
&mut out,
);
staffel(
"GEOTHERMIE",
"UEBERSCHUSS",
&[band(None, dec!(25.20), "§45 Abs. 1 EEG 2023")],
Absenkung::Jaehrlich(A::GEOTHERMIE),
&mut out,
);
for row in &mut out {
row.verguetungssatz_ct = row.verguetungssatz_ct.round_kfm(4);
}
out
}
#[cfg(test)]
mod statutory_seed_tests {
use super::*;
fn finde(art: &str, form: &str, min: Decimal, start: Date) -> VerguetungssatzRow {
verguetungssatz_rows()
.into_iter()
.find(|r| {
r.erzeugungsart == art
&& r.verguetungsform == form
&& r.leistung_min_kwp == min
&& r.billing_start == start
})
.unwrap_or_else(|| panic!("{art}/{form} band from {min} kW, window of {start}"))
}
#[test]
fn solar_ueberschuss_has_exactly_the_three_sect48_abs2_bands() {
let start = time::macros::date!(2024 - 02 - 01);
let bands: Vec<_> = verguetungssatz_rows()
.into_iter()
.filter(|r| {
r.erzeugungsart == "SOLAR_AUFDACH"
&& r.verguetungsform == "UEBERSCHUSS"
&& r.billing_start == start
})
.map(|r| (r.leistung_min_kwp, r.leistung_max_kwp, r.verguetungssatz_ct))
.collect();
assert_eq!(
bands,
vec![
(dec!(0), Some(dec!(10)), dec!(8.11)),
(dec!(10), Some(dec!(40)), dec!(7.03)),
(dec!(40), Some(dec!(1000)), dec!(5.74)),
]
);
}
#[test]
fn sect49_windows_run_on_and_close() {
for (start, ende, ct) in [
(
time::macros::date!(2023 - 01 - 01),
time::macros::date!(2024 - 01 - 31),
dec!(8.20),
),
(
time::macros::date!(2024 - 02 - 01),
time::macros::date!(2024 - 07 - 31),
dec!(8.11),
),
(
time::macros::date!(2026 - 02 - 01),
time::macros::date!(2026 - 07 - 31),
dec!(7.78),
),
] {
let r = finde("SOLAR_AUFDACH", "UEBERSCHUSS", dec!(0), start);
assert_eq!(r.billing_end, ende, "window of {start}");
assert_eq!(r.verguetungssatz_ct, ct, "window of {start}");
}
}
#[test]
fn volleinspeisung_takes_the_uplift_of_its_own_band() {
let start = time::macros::date!(2024 - 02 - 01);
assert_eq!(
finde("SOLAR_AUFDACH", "VOLLEINSPEISUNG", dec!(40), start).verguetungssatz_ct,
dec!(10.79)
);
assert_eq!(
finde("SOLAR_AUFDACH", "VOLLEINSPEISUNG", dec!(400), start).verguetungssatz_ct,
dec!(7.62)
);
}
#[test]
fn non_solar_rows_are_net_and_degressed() {
assert_eq!(
finde(
"WASSERKRAFT",
"UEBERSCHUSS",
dec!(0),
time::macros::date!(2026 - 01 - 01)
)
.verguetungssatz_ct,
dec!(11.65)
);
let bio = finde(
"BIOMASSE",
"UEBERSCHUSS",
dec!(0),
time::macros::date!(2024 - 07 - 01),
);
assert_eq!(bio.billing_end, time::macros::date!(2025 - 06 - 30));
assert_eq!(bio.verguetungssatz_ct, dec!(12.41));
}
#[test]
fn wind_and_kwk_are_not_tabulated() {
for row in verguetungssatz_rows() {
assert!(
!row.erzeugungsart.starts_with("WIND") && row.erzeugungsart != "KWKG",
"{} must not be seeded",
row.erzeugungsart
);
}
}
#[test]
fn windows_tile_without_gaps() {
let mut rows = verguetungssatz_rows();
rows.sort_by_key(|r| {
(
r.erzeugungsart,
r.verguetungsform,
r.leistung_min_kwp,
r.billing_start,
)
});
for pair in rows.windows(2) {
let (a, b) = (&pair[0], &pair[1]);
if a.erzeugungsart == b.erzeugungsart
&& a.verguetungsform == b.verguetungsform
&& a.leistung_min_kwp == b.leistung_min_kwp
{
assert_eq!(
a.billing_end.next_day(),
Some(b.billing_start),
"gap between {} and {}",
a.billing_end,
b.billing_start
);
}
assert!(a.billing_end >= a.billing_start);
}
}
}