use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use time::Date;
use crate::technology::ErzeugungsArt;
use crate::version::EegGesetz;
pub const DIREKTVERMARKTUNG_PFLICHT_KW: Decimal = dec!(100);
#[must_use]
pub fn is_direktvermarktung_mandatory(leistung_kw: Decimal, gesetz: EegGesetz) -> bool {
match gesetz {
EegGesetz::Eeg2012 | EegGesetz::Eeg2017 | EegGesetz::Eeg2021 | EegGesetz::Eeg2023 => {
leistung_kw > DIREKTVERMARKTUNG_PFLICHT_KW
}
_ => false,
}
}
#[must_use]
pub fn requires_ausschreibung(leistung_kw: Decimal, art: ErzeugungsArt) -> bool {
match art {
ErzeugungsArt::Solar
| ErzeugungsArt::SolarAufdach
| ErzeugungsArt::SolarFreiflaeche
| ErzeugungsArt::SolarAgriPv
| ErzeugungsArt::SolarMieterstrom
| ErzeugungsArt::SolarStecker => leistung_kw > dec!(1000),
ErzeugungsArt::WindOnshore => leistung_kw > dec!(750),
ErzeugungsArt::WindOffshore => true,
ErzeugungsArt::Biomasse
| ErzeugungsArt::BiomassHolz
| ErzeugungsArt::Biogas
| ErzeugungsArt::Biomethan => leistung_kw > dec!(150),
ErzeugungsArt::Wasserkraft => leistung_kw > dec!(500),
ErzeugungsArt::Geothermie => leistung_kw > dec!(150),
ErzeugungsArt::Klaegas
| ErzeugungsArt::Grubengas
| ErzeugungsArt::Deponiegas
| ErzeugungsArt::Gezeiten
| ErzeugungsArt::Kwk => false,
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DirektvermarktungsPeriode {
pub beginn_datum: Date,
pub ende_datum: Option<Date>,
pub direktvermarkter_mp_id: Option<String>,
pub ist_freiwillig: bool,
pub anzulegender_wert_ct: Decimal,
}
impl DirektvermarktungsPeriode {
#[must_use]
pub fn is_active_on(&self, billing_date: Date) -> bool {
if billing_date < self.beginn_datum {
return false;
}
match self.ende_datum {
Some(end) => billing_date <= end,
None => true,
}
}
}
#[must_use]
pub fn current_period(
periods: &[DirektvermarktungsPeriode],
billing_date: Date,
) -> Option<&DirektvermarktungsPeriode> {
periods.iter().find(|p| p.is_active_on(billing_date))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SwitchBlockedReason {
PflichtgemasseDirektvermarktung,
AlreadySwitchedThisMonth {
last_switch: Date,
},
}
pub fn validate_switch_to_vergütung(
leistung_kw: Decimal,
gesetz: EegGesetz,
effective_date: Date,
last_switch_date: Option<Date>,
) -> Result<(), SwitchBlockedReason> {
if is_direktvermarktung_mandatory(leistung_kw, gesetz) {
return Err(SwitchBlockedReason::PflichtgemasseDirektvermarktung);
}
if let Some(last) = last_switch_date
&& last.year() == effective_date.year()
&& last.month() == effective_date.month()
{
return Err(SwitchBlockedReason::AlreadySwitchedThisMonth { last_switch: last });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::date;
#[test]
fn mandatory_above_100kw() {
assert!(is_direktvermarktung_mandatory(
dec!(100.1),
EegGesetz::Eeg2023
));
assert!(!is_direktvermarktung_mandatory(
dec!(100),
EegGesetz::Eeg2023
));
assert!(!is_direktvermarktung_mandatory(
dec!(50),
EegGesetz::Eeg2023
));
}
#[test]
fn no_mandatory_for_old_eeg() {
assert!(!is_direktvermarktung_mandatory(
dec!(500),
EegGesetz::Eeg2009
));
assert!(!is_direktvermarktung_mandatory(
dec!(500),
EegGesetz::Eeg2000
));
}
#[test]
fn ausschreibung_solar_above_1mwp() {
assert!(requires_ausschreibung(
dec!(1001),
ErzeugungsArt::SolarAufdach
));
assert!(!requires_ausschreibung(
dec!(999),
ErzeugungsArt::SolarAufdach
));
}
#[test]
fn ausschreibung_wind_onshore_above_750kw() {
assert!(requires_ausschreibung(
dec!(751),
ErzeugungsArt::WindOnshore
));
assert!(!requires_ausschreibung(
dec!(750),
ErzeugungsArt::WindOnshore
));
}
#[test]
fn ausschreibung_wind_offshore_always() {
assert!(requires_ausschreibung(dec!(1), ErzeugungsArt::WindOffshore));
}
#[test]
fn ausschreibung_biomasse_above_150kw() {
assert!(requires_ausschreibung(dec!(151), ErzeugungsArt::Biomasse));
assert!(!requires_ausschreibung(dec!(150), ErzeugungsArt::Biomasse));
}
#[test]
fn period_active_on() {
let p = DirektvermarktungsPeriode {
beginn_datum: date!(2024 - 01 - 01),
ende_datum: Some(date!(2024 - 06 - 30)),
direktvermarkter_mp_id: None,
ist_freiwillig: true,
anzulegender_wert_ct: dec!(6.28),
};
assert!(p.is_active_on(date!(2024 - 03 - 15)));
assert!(p.is_active_on(date!(2024 - 01 - 01)));
assert!(p.is_active_on(date!(2024 - 06 - 30)));
assert!(!p.is_active_on(date!(2024 - 07 - 01)));
assert!(!p.is_active_on(date!(2023 - 12 - 31)));
}
#[test]
fn open_period_always_active() {
let p = DirektvermarktungsPeriode {
beginn_datum: date!(2024 - 01 - 01),
ende_datum: None,
direktvermarkter_mp_id: None,
ist_freiwillig: false,
anzulegender_wert_ct: dec!(6.28),
};
assert!(p.is_active_on(date!(2030 - 12 - 31)));
}
#[test]
fn current_period_finds_active() {
let periods = vec![DirektvermarktungsPeriode {
beginn_datum: date!(2024 - 01 - 01),
ende_datum: Some(date!(2024 - 06 - 30)),
direktvermarkter_mp_id: Some("9904234560001".into()),
ist_freiwillig: false,
anzulegender_wert_ct: dec!(6.28),
}];
assert!(current_period(&periods, date!(2024 - 03 - 15)).is_some());
assert!(current_period(&periods, date!(2024 - 07 - 01)).is_none());
}
#[test]
fn switch_blocked_mandatory_plant() {
let result = validate_switch_to_vergütung(
dec!(150),
EegGesetz::Eeg2023,
date!(2025 - 07 - 01),
None,
);
assert_eq!(
result,
Err(SwitchBlockedReason::PflichtgemasseDirektvermarktung)
);
}
#[test]
fn switch_blocked_same_month() {
let result = validate_switch_to_vergütung(
dec!(80),
EegGesetz::Eeg2023,
date!(2025 - 07 - 15),
Some(date!(2025 - 07 - 01)),
);
assert!(matches!(
result,
Err(SwitchBlockedReason::AlreadySwitchedThisMonth { .. })
));
}
#[test]
fn switch_allowed_different_month() {
let result = validate_switch_to_vergütung(
dec!(80),
EegGesetz::Eeg2023,
date!(2025 - 08 - 01),
Some(date!(2025 - 07 - 01)),
);
assert!(result.is_ok());
}
#[test]
fn switch_allowed_voluntary_no_history() {
let result = validate_switch_to_vergütung(
dec!(50),
EegGesetz::Eeg2023,
date!(2025 - 07 - 01),
None,
);
assert!(result.is_ok());
}
}