use rust_decimal::Decimal;
use rust_decimal::dec;
#[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 SolarBauform {
Gebaeude,
Laermschutzwand,
Freiflaeche,
AgriPv,
Floating,
Parkplatz,
SteckerPv,
}
impl SolarBauform {
#[must_use]
pub fn volleinspeisung_bonus_eligible(self) -> bool {
matches!(
self,
Self::Gebaeude
| Self::Laermschutzwand
| Self::AgriPv
| Self::Floating
| Self::Parkplatz
)
}
#[must_use]
pub fn has_freiflaechen_restriction(self) -> bool {
self == Self::Freiflaeche
}
#[must_use]
pub fn auction_threshold_kwp(self) -> Decimal {
match self {
Self::SteckerPv => dec!(2), Self::AgriPv | Self::Floating | Self::Parkplatz => dec!(6000), _ => dec!(1000), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum EinspeisungsModus {
Volleinspeisung,
#[default]
Ueberschusseinspeisung,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SolarAnlageData {
pub bauform: SolarBauform,
pub einspeisungs_modus: EinspeisungsModus,
pub mastr_registriert: bool,
pub agripv_zertifiziert: bool,
}
impl SolarAnlageData {
#[must_use]
pub fn volleinspeisung_sanktionspflichtig(&self) -> bool {
self.einspeisungs_modus == EinspeisungsModus::Volleinspeisung
}
}
#[must_use]
pub fn requires_ausschreibung(leistung_kwp: Decimal, bauform: SolarBauform) -> bool {
leistung_kwp > bauform.auction_threshold_kwp()
}
pub const SECT51A_SOLAR_FACTOR_DENOMINATOR: u64 = 2;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gebaeude_qualifies_for_volleinspeisung_bonus() {
assert!(SolarBauform::Gebaeude.volleinspeisung_bonus_eligible());
assert!(SolarBauform::AgriPv.volleinspeisung_bonus_eligible());
assert!(!SolarBauform::Freiflaeche.volleinspeisung_bonus_eligible());
}
#[test]
fn freiflaeche_has_location_restriction() {
assert!(SolarBauform::Freiflaeche.has_freiflaechen_restriction());
assert!(!SolarBauform::Gebaeude.has_freiflaechen_restriction());
}
#[test]
fn agripv_has_higher_auction_threshold() {
assert_eq!(SolarBauform::Gebaeude.auction_threshold_kwp(), dec!(1000));
assert_eq!(SolarBauform::AgriPv.auction_threshold_kwp(), dec!(6000));
}
#[test]
fn auction_required_above_1mwp_gebaeude() {
assert!(!requires_ausschreibung(dec!(1000), SolarBauform::Gebaeude)); assert!(requires_ausschreibung(dec!(1001), SolarBauform::Gebaeude)); }
#[test]
fn no_auction_for_agripv_below_6mwp() {
assert!(!requires_ausschreibung(dec!(5999), SolarBauform::AgriPv));
assert!(requires_ausschreibung(dec!(6001), SolarBauform::AgriPv));
}
#[test]
fn stecker_pv_never_requires_auction() {
assert!(!requires_ausschreibung(dec!(2), SolarBauform::SteckerPv));
}
#[test]
fn volleinspeisung_triggers_sanktionspflicht() {
let anlage = SolarAnlageData {
bauform: SolarBauform::Gebaeude,
einspeisungs_modus: EinspeisungsModus::Volleinspeisung,
mastr_registriert: true,
agripv_zertifiziert: false,
};
assert!(anlage.volleinspeisung_sanktionspflichtig());
}
#[test]
fn ueberschuss_no_sanktionspflicht() {
let anlage = SolarAnlageData {
bauform: SolarBauform::Gebaeude,
einspeisungs_modus: EinspeisungsModus::Ueberschusseinspeisung,
mastr_registriert: true,
agripv_zertifiziert: false,
};
assert!(!anlage.volleinspeisung_sanktionspflichtig());
}
#[test]
fn sect51a_solar_uses_half_factor() {
assert_eq!(
crate::foerderdauer::verguetungszeitraum_verlaengerung_qh(200, true),
100 );
assert_eq!(
crate::foerderdauer::verguetungszeitraum_verlaengerung_qh(201, true),
101 );
}
#[test]
fn sect51a_wind_rounds_up_to_full_calendar_day() {
assert_eq!(
crate::foerderdauer::verguetungszeitraum_verlaengerung_qh(200, false),
288
);
}
}