use crate::technology::ErzeugungsArt;
use time::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SolarMontage {
AnGebaeudeOderLaermschutzwand,
Freiflaeche,
#[default]
Sonstige,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Steckersolar {
pub installierte_leistung_kw: rust_decimal::Decimal,
pub wechselrichter_va: rust_decimal::Decimal,
pub hinter_entnahmestelle: bool,
}
impl Steckersolar {
#[must_use]
pub fn wird_nicht_beruecksichtigt(&self) -> bool {
self.hinter_entnahmestelle
&& self.installierte_leistung_kw <= rust_decimal::dec!(2)
&& self.wechselrichter_va <= rust_decimal::dec!(800)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnlageFuerZusammenfassung {
pub inbetriebnahme: Date,
pub art: ErzeugungsArt,
pub standort_id: String,
pub anspruch_leistungsabhaengig: bool,
pub montage: SolarMontage,
pub netzverknuepfungspunkt: Option<String>,
pub biogaserzeugungsanlage_id: Option<String>,
pub steckersolar: Option<Steckersolar>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZusammenfassungGrund {
Satz5SteckersolarUnberuecksichtigt,
Satz2GleicheBiogaserzeugungsanlage,
Satz3FreiflaecheNichtMitGebaeude,
Satz4VerschiedeneNetzverknuepfungspunkte,
Satz1AlleVoraussetzungen,
Satz1Nr1StandortVerschieden,
Satz1Nr2ArtVerschieden,
Satz1Nr3AnspruchNichtLeistungsabhaengig,
Satz1Nr4AusserhalbZwoelfMonatsfenster,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ZusammenfassungErgebnis {
pub gelten_als_eine_anlage: bool,
pub grund: ZusammenfassungGrund,
}
#[must_use]
pub fn zusammenlegung_within_12_months(ibn_a: Date, ibn_b: Date) -> bool {
let months = |d: Date| i32::from(d.year() as i16) * 12 + (u8::from(d.month()) as i32 - 1);
(months(ibn_a) - months(ibn_b)).abs() < 12
}
#[must_use]
pub fn sind_eine_anlage(
a: &AnlageFuerZusammenfassung,
b: &AnlageFuerZusammenfassung,
unmittelbare_raeumliche_naehe: bool,
) -> ZusammenfassungErgebnis {
let nein = |grund| ZusammenfassungErgebnis {
gelten_als_eine_anlage: false,
grund,
};
let ja = |grund| ZusammenfassungErgebnis {
gelten_als_eine_anlage: true,
grund,
};
if a.steckersolar
.is_some_and(|s| s.wird_nicht_beruecksichtigt())
|| b.steckersolar
.is_some_and(|s| s.wird_nicht_beruecksichtigt())
{
return nein(ZusammenfassungGrund::Satz5SteckersolarUnberuecksichtigt);
}
let biogas_ohne_biomethan = |x: &AnlageFuerZusammenfassung| x.art == ErzeugungsArt::Biogas;
if biogas_ohne_biomethan(a)
&& biogas_ohne_biomethan(b)
&& let (Some(ba), Some(bb)) = (&a.biogaserzeugungsanlage_id, &b.biogaserzeugungsanlage_id)
&& ba == bb
{
return ja(ZusammenfassungGrund::Satz2GleicheBiogaserzeugungsanlage);
}
let ist_freiflaeche = |x: &AnlageFuerZusammenfassung| x.montage == SolarMontage::Freiflaeche;
let ist_gebaeude =
|x: &AnlageFuerZusammenfassung| x.montage == SolarMontage::AnGebaeudeOderLaermschutzwand;
if (ist_freiflaeche(a) && ist_gebaeude(b)) || (ist_gebaeude(a) && ist_freiflaeche(b)) {
return nein(ZusammenfassungGrund::Satz3FreiflaecheNichtMitGebaeude);
}
if ist_gebaeude(a)
&& ist_gebaeude(b)
&& let (Some(na), Some(nb)) = (&a.netzverknuepfungspunkt, &b.netzverknuepfungspunkt)
&& na != nb
{
return nein(ZusammenfassungGrund::Satz4VerschiedeneNetzverknuepfungspunkte);
}
if a.standort_id != b.standort_id && !unmittelbare_raeumliche_naehe {
return nein(ZusammenfassungGrund::Satz1Nr1StandortVerschieden);
}
if !gleichartige_energien(a.art, b.art) {
return nein(ZusammenfassungGrund::Satz1Nr2ArtVerschieden);
}
if !a.anspruch_leistungsabhaengig || !b.anspruch_leistungsabhaengig {
return nein(ZusammenfassungGrund::Satz1Nr3AnspruchNichtLeistungsabhaengig);
}
if !zusammenlegung_within_12_months(a.inbetriebnahme, b.inbetriebnahme) {
return nein(ZusammenfassungGrund::Satz1Nr4AusserhalbZwoelfMonatsfenster);
}
ja(ZusammenfassungGrund::Satz1AlleVoraussetzungen)
}
#[must_use]
pub fn gleichartige_energien(a: ErzeugungsArt, b: ErzeugungsArt) -> bool {
if a == b {
return true;
}
if a.is_solar() && b.is_solar() {
return true;
}
if a.is_wind() && b.is_wind() {
return true;
}
if a.is_biomasse_or_gas() && b.is_biomasse_or_gas() {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
use time::macros::date;
fn anlage(ibn: Date, art: ErzeugungsArt, standort: &str) -> AnlageFuerZusammenfassung {
AnlageFuerZusammenfassung {
inbetriebnahme: ibn,
art,
standort_id: standort.to_owned(),
anspruch_leistungsabhaengig: true,
montage: SolarMontage::Sonstige,
netzverknuepfungspunkt: None,
biogaserzeugungsanlage_id: None,
steckersolar: None,
}
}
#[test]
fn all_four_conditions_fuse_the_plants() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let b = anlage(date!(2024 - 12 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let r = sind_eine_anlage(&a, &b, false);
assert!(r.gelten_als_eine_anlage);
assert_eq!(r.grund, ZusammenfassungGrund::Satz1AlleVoraussetzungen);
}
#[test]
fn twelve_month_window_is_exclusive_at_twelve() {
assert!(zusammenlegung_within_12_months(
date!(2024 - 01 - 15),
date!(2024 - 12 - 15)
));
assert!(!zusammenlegung_within_12_months(
date!(2024 - 01 - 01),
date!(2025 - 01 - 01)
));
assert!(!zusammenlegung_within_12_months(
date!(2024 - 01 - 01),
date!(2025 - 02 - 01)
));
assert!(zusammenlegung_within_12_months(
date!(2024 - 12 - 01),
date!(2025 - 11 - 01)
));
}
#[test]
fn a_different_site_without_proximity_does_not_fuse() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarAufdach, "FLST-2");
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz1Nr1StandortVerschieden
);
assert!(sind_eine_anlage(&a, &b, true).gelten_als_eine_anlage);
}
#[test]
fn different_energy_sources_do_not_fuse() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::WindOnshore, "FLST-1");
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz1Nr2ArtVerschieden
);
}
#[test]
fn a_size_independent_claim_is_outside_the_fiction() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
b.anspruch_leistungsabhaengig = false;
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz1Nr3AnspruchNichtLeistungsabhaengig
);
}
#[test]
fn same_biogas_plant_fuses_regardless_of_satz_1() {
let mut a = anlage(date!(2018 - 01 - 15), ErzeugungsArt::Biogas, "FLST-1");
let mut b = anlage(date!(2024 - 09 - 15), ErzeugungsArt::Biogas, "FLST-9");
a.biogaserzeugungsanlage_id = Some("BGA-7".to_owned());
b.biogaserzeugungsanlage_id = Some("BGA-7".to_owned());
let r = sind_eine_anlage(&a, &b, false);
assert!(r.gelten_als_eine_anlage);
assert_eq!(
r.grund,
ZusammenfassungGrund::Satz2GleicheBiogaserzeugungsanlage
);
}
#[test]
fn different_biogas_plants_fall_back_to_satz_1() {
let mut a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::Biogas, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::Biogas, "FLST-9");
a.biogaserzeugungsanlage_id = Some("BGA-7".to_owned());
b.biogaserzeugungsanlage_id = Some("BGA-8".to_owned());
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz1Nr1StandortVerschieden
);
}
#[test]
fn freiflaeche_is_never_fused_with_building_solar() {
let mut a = anlage(
date!(2024 - 01 - 15),
ErzeugungsArt::SolarFreiflaeche,
"FLST-1",
);
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
a.montage = SolarMontage::Freiflaeche;
b.montage = SolarMontage::AnGebaeudeOderLaermschutzwand;
let r = sind_eine_anlage(&a, &b, false);
assert!(!r.gelten_als_eine_anlage);
assert_eq!(
r.grund,
ZusammenfassungGrund::Satz3FreiflaecheNichtMitGebaeude
);
}
#[test]
fn building_solar_behind_different_grid_points_does_not_fuse() {
let mut a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
a.montage = SolarMontage::AnGebaeudeOderLaermschutzwand;
b.montage = SolarMontage::AnGebaeudeOderLaermschutzwand;
a.netzverknuepfungspunkt = Some("NVP-A".to_owned());
b.netzverknuepfungspunkt = Some("NVP-B".to_owned());
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz4VerschiedeneNetzverknuepfungspunkte
);
b.netzverknuepfungspunkt = Some("NVP-A".to_owned());
assert!(sind_eine_anlage(&a, &b, false).gelten_als_eine_anlage);
}
#[test]
fn unknown_grid_points_do_not_separate_building_solar() {
let mut a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
a.montage = SolarMontage::AnGebaeudeOderLaermschutzwand;
b.montage = SolarMontage::AnGebaeudeOderLaermschutzwand;
assert!(sind_eine_anlage(&a, &b, false).gelten_als_eine_anlage);
}
#[test]
fn a_qualifying_steckersolargeraet_is_disregarded() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarStecker, "FLST-1");
b.steckersolar = Some(Steckersolar {
installierte_leistung_kw: dec!(2),
wechselrichter_va: dec!(800),
hinter_entnahmestelle: true,
});
assert_eq!(
sind_eine_anlage(&a, &b, false).grund,
ZusammenfassungGrund::Satz5SteckersolarUnberuecksichtigt
);
}
#[test]
fn an_oversized_steckersolargeraet_counts_normally() {
let a = anlage(date!(2024 - 01 - 15), ErzeugungsArt::SolarAufdach, "FLST-1");
let mut b = anlage(date!(2024 - 03 - 15), ErzeugungsArt::SolarStecker, "FLST-1");
for over in [
Steckersolar {
installierte_leistung_kw: dec!(2.1),
wechselrichter_va: dec!(800),
hinter_entnahmestelle: true,
},
Steckersolar {
installierte_leistung_kw: dec!(2),
wechselrichter_va: dec!(801),
hinter_entnahmestelle: true,
},
Steckersolar {
installierte_leistung_kw: dec!(2),
wechselrichter_va: dec!(800),
hinter_entnahmestelle: false,
},
] {
b.steckersolar = Some(over);
assert!(
sind_eine_anlage(&a, &b, false).gelten_als_eine_anlage,
"device outside Satz 5 must be fused normally: {over:?}"
);
}
}
}