use crate::rounding::RoundMoney;
use rust_decimal::Decimal;
#[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 BiomassBrennstoff {
PflanzlicheBiomasse,
BiomethanAusBiomasse,
Guelle,
Festmist,
Holzbiomasse,
Klaergas,
Deponiegas,
Grubengas,
BiogenicWaste,
}
impl BiomassBrennstoff {
#[must_use]
pub fn guellebonusanlage_eligible(self) -> bool {
matches!(self, Self::Guelle | Self::Festmist)
}
#[must_use]
pub fn ist_biogas(self) -> bool {
matches!(
self,
Self::PflanzlicheBiomasse
| Self::BiomethanAusBiomasse
| Self::Guelle
| Self::Festmist
| Self::BiogenicWaste
)
}
}
pub const SECT44_ABS2_NR2_GRENZE_KW: Decimal = rust_decimal::dec!(150);
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BiomassSettlementData {
pub hauptbrennstoff: BiomassBrennstoff,
pub guelle_anteil: Decimal,
pub ist_guellebonusanlage: bool,
pub getreide_mais_anteil: Decimal,
pub zuschlag_gebotstermin: Option<time::Date>,
}
impl BiomassSettlementData {
#[must_use]
pub fn new(
hauptbrennstoff: BiomassBrennstoff,
guelle_anteil: Decimal,
getreide_mais_anteil: Decimal,
leistung_kw: Decimal,
zuschlag_gebotstermin: Option<time::Date>,
) -> Self {
use rust_decimal::dec;
let ist_guellebonusanlage = hauptbrennstoff.guellebonusanlage_eligible()
&& guelle_anteil >= dec!(0.80)
&& leistung_kw <= SECT44_ABS2_NR2_GRENZE_KW;
Self {
hauptbrennstoff,
guelle_anteil,
ist_guellebonusanlage,
getreide_mais_anteil,
zuschlag_gebotstermin,
}
}
#[must_use]
pub fn sect39i_hoechstanteil(&self) -> Option<Decimal> {
if !self.hauptbrennstoff.ist_biogas() {
return None;
}
self.zuschlag_gebotstermin.and_then(sect39i_hoechstanteil)
}
#[must_use]
pub fn sect39i_eingehalten(&self) -> bool {
self.sect39i_hoechstanteil()
.is_none_or(|grenze| self.getreide_mais_anteil <= grenze)
}
}
#[must_use]
pub fn sect39i_hoechstanteil(gebotstermin: time::Date) -> Option<Decimal> {
use rust_decimal::dec;
const NR3_STICHTAG: time::Date = time::macros::date!(2025 - 02 - 25);
match gebotstermin.year() {
2023 => Some(dec!(0.40)),
2024 => Some(dec!(0.35)),
2025 if gebotstermin < NR3_STICHTAG => Some(dec!(0.35)),
2025 if gebotstermin > NR3_STICHTAG => Some(dec!(0.30)),
2026..=2028 => Some(dec!(0.25)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
use time::macros::date;
#[test]
fn guellebonusanlage_qualifies_when_criteria_met() {
let data = BiomassSettlementData::new(
BiomassBrennstoff::Guelle,
dec!(0.85),
dec!(0.05),
dec!(50),
None,
);
assert!(data.ist_guellebonusanlage);
}
#[test]
fn guellebonusanlage_runs_to_the_sect44_abs2_nr2_ceiling() {
let at = |kw| {
BiomassSettlementData::new(BiomassBrennstoff::Guelle, dec!(0.90), dec!(0.05), kw, None)
};
assert!(
at(dec!(100)).ist_guellebonusanlage,
"100 kW is inside Nr. 2"
);
assert!(
at(SECT44_ABS2_NR2_GRENZE_KW).ist_guellebonusanlage,
"\u{201e}höchstens 150 Kilowatt\u{201c} includes 150"
);
assert!(
!at(dec!(151)).ist_guellebonusanlage,
"past 150 kW Nr. 2 is not met"
);
}
#[test]
fn sect39i_reaches_only_biogas() {
use BiomassBrennstoff as B;
let at = |brennstoff| {
BiomassSettlementData::new(
brennstoff,
dec!(0.0),
dec!(0.90),
dec!(500),
Some(date!(2026 - 04 - 01)),
)
};
for ausserhalb in [B::Holzbiomasse, B::Klaergas, B::Deponiegas, B::Grubengas] {
assert_eq!(
at(ausserhalb).sect39i_hoechstanteil(),
None,
"{ausserhalb:?}"
);
assert!(at(ausserhalb).sect39i_eingehalten(), "{ausserhalb:?}");
}
for biogas in [
B::PflanzlicheBiomasse,
B::BiomethanAusBiomasse,
B::Guelle,
B::Festmist,
B::BiogenicWaste,
] {
assert_eq!(
at(biogas).sect39i_hoechstanteil(),
Some(dec!(0.25)),
"{biogas:?}"
);
assert!(!at(biogas).sect39i_eingehalten(), "{biogas:?}");
}
}
#[test]
fn sect39i_does_not_reach_a_plant_without_a_zuschlag() {
let data = BiomassSettlementData::new(
BiomassBrennstoff::PflanzlicheBiomasse,
dec!(0.0),
dec!(0.45),
dec!(120),
None,
);
assert_eq!(data.sect39i_hoechstanteil(), None);
assert!(data.sect39i_eingehalten());
}
#[test]
fn sect39i_steps_down_by_gebotstermin() {
let at = |termin| {
BiomassSettlementData::new(
BiomassBrennstoff::PflanzlicheBiomasse,
dec!(0.0),
dec!(0.28),
dec!(500),
Some(termin),
)
};
assert!(at(date!(2023 - 04 - 01)).sect39i_eingehalten());
assert!(at(date!(2024 - 10 - 01)).sect39i_eingehalten());
assert!(at(date!(2025 - 10 - 01)).sect39i_eingehalten());
assert!(
!at(date!(2026 - 04 - 01)).sect39i_eingehalten(),
"Nr. 4 caps a 2026 award at 25 Masseprozent"
);
}
}
#[must_use]
pub fn bemessungsleistung_stunden(
kalenderjahr: i32,
erstmalige_erzeugung: Option<time::Date>,
) -> Option<rust_decimal::Decimal> {
use rust_decimal::Decimal;
let jahresbeginn =
time::Date::from_calendar_date(kalenderjahr, time::Month::January, 1).ok()?;
let naechstes_jahr =
time::Date::from_calendar_date(kalenderjahr + 1, time::Month::January, 1).ok()?;
let start = match erstmalige_erzeugung {
Some(d) if d >= naechstes_jahr => return Some(Decimal::ZERO),
Some(d) if d > jahresbeginn => d,
_ => jahresbeginn,
};
let tage = (naechstes_jahr - start).whole_days();
Some(Decimal::from(tage) * Decimal::from(24))
}
#[must_use]
pub fn sect44b_jahreskontingent_kwh(
leistung_kw: rust_decimal::Decimal,
kalenderjahr: i32,
erstmalige_erzeugung: Option<time::Date>,
) -> rust_decimal::Decimal {
use rust_decimal::dec;
let stunden =
bemessungsleistung_stunden(kalenderjahr, erstmalige_erzeugung).unwrap_or(dec!(8760));
(leistung_kw * dec!(0.45) * stunden).round_kfm(3)
}
#[cfg(test)]
mod bemessungsleistung_tests {
use super::*;
use rust_decimal::dec;
use time::macros::date;
#[test]
fn a_leap_year_has_more_hours() {
assert_eq!(bemessungsleistung_stunden(2027, None), Some(dec!(8760)));
assert_eq!(bemessungsleistung_stunden(2028, None), Some(dec!(8784)));
assert_eq!(
sect44b_jahreskontingent_kwh(dec!(500), 2028, None)
- sect44b_jahreskontingent_kwh(dec!(500), 2027, None),
dec!(5400)
);
}
#[test]
fn hours_before_the_first_generation_are_deducted() {
assert_eq!(
bemessungsleistung_stunden(2027, Some(date!(2027 - 07 - 01))),
Some(dec!(4416))
);
assert_eq!(
bemessungsleistung_stunden(2027, Some(date!(2020 - 03 - 01))),
Some(dec!(8760))
);
assert_eq!(
bemessungsleistung_stunden(2027, Some(date!(2028 - 01 - 01))),
Some(dec!(0))
);
}
}