use crate::scheme::TariffSource;
use crate::technology::ErzeugungsArt;
use rust_decimal::Decimal;
use rust_decimal::dec;
pub const SECT53B_REGIONALNACHWEIS_CT_KWH: Decimal = dec!(0.1);
pub const SECT54_ABS1_ABS2_CT_KWH: Decimal = dec!(0.3);
pub const SECT54_ABS3_CT_KWH: Decimal = dec!(2.5);
pub const STROMSTEUER_VOLLSATZ_CT_KWH: Decimal = dec!(2.05);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Sect54SolarReduction {
pub zahlungsberechtigung_nach_18_monaten: bool,
pub flurstueck_abweichung: bool,
pub agri_nutzungsnachweis_fehlt: bool,
pub landesverordnung_nicht_erfuellt: bool,
}
impl Sect54SolarReduction {
#[must_use]
pub fn is_clean(&self) -> bool {
*self == Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AwReductionApplied {
pub description: String,
pub legal_basis: String,
pub deduction_ct_kwh: Decimal,
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct AwReductionContext {
pub regionalnachweis_ausgestellt: bool,
pub stromsteuerbefreiung_ct_kwh: Option<Decimal>,
pub sect54_solar: Option<Sect54SolarReduction>,
}
impl AwReductionContext {
#[must_use]
pub fn is_empty(&self) -> bool {
!self.regionalnachweis_ausgestellt
&& self.stromsteuerbefreiung_ct_kwh.is_none()
&& self.sect54_solar.is_none_or(|s| s.is_clean())
}
}
#[must_use]
pub fn apply_aw_reductions(
aw_ct: Decimal,
ctx: &AwReductionContext,
tariff_source: &TariffSource,
art: ErzeugungsArt,
) -> (Decimal, Vec<AwReductionApplied>) {
let mut aw = aw_ct;
let mut applied = Vec::new();
if ctx.regionalnachweis_ausgestellt && !tariff_source.is_auction() {
aw -= SECT53B_REGIONALNACHWEIS_CT_KWH;
applied.push(AwReductionApplied {
description: "\u{00a7}53b EEG 2023 Regionalnachweis (\u{00a7}79a)".to_owned(),
legal_basis: "\u{00a7}53b EEG 2023".to_owned(),
deduction_ct_kwh: SECT53B_REGIONALNACHWEIS_CT_KWH,
});
}
if let Some(befreiung) = ctx
.stromsteuerbefreiung_ct_kwh
.filter(|c| *c > Decimal::ZERO)
{
let deduction = befreiung.min(STROMSTEUER_VOLLSATZ_CT_KWH);
aw -= deduction;
applied.push(AwReductionApplied {
description: format!(
"\u{00a7}53c EEG 2023 Stromsteuerbefreiung ({deduction}\u{202f}ct/kWh)"
),
legal_basis: "\u{00a7}53c EEG 2023".to_owned(),
deduction_ct_kwh: deduction,
});
}
if let Some(s54) = ctx.sect54_solar.filter(|s| !s.is_clean() && art.is_solar()) {
if s54.landesverordnung_nicht_erfuellt {
let deduction = aw.max(Decimal::ZERO);
applied.push(AwReductionApplied {
description: "\u{00a7}54 Abs.\u{202f}4 EEG 2023 Landesverordnung \
nicht erf\u{00fc}llt (AW \u{2192} 0)"
.to_owned(),
legal_basis: "\u{00a7}54 Abs. 4 EEG 2023".to_owned(),
deduction_ct_kwh: deduction,
});
return (Decimal::ZERO, applied);
}
if s54.zahlungsberechtigung_nach_18_monaten {
aw -= SECT54_ABS1_ABS2_CT_KWH;
applied.push(AwReductionApplied {
description: "\u{00a7}54 Abs.\u{202f}1 EEG 2023 Zahlungsberechtigung erst \
nach dem 18.\u{202f}Kalendermonat beantragt"
.to_owned(),
legal_basis: "\u{00a7}54 Abs. 1 EEG 2023".to_owned(),
deduction_ct_kwh: SECT54_ABS1_ABS2_CT_KWH,
});
}
if s54.flurstueck_abweichung {
aw -= SECT54_ABS1_ABS2_CT_KWH;
applied.push(AwReductionApplied {
description: "\u{00a7}54 Abs.\u{202f}2 EEG 2023 Standort weicht von den \
Gebots-Flurst\u{00fc}cken ab"
.to_owned(),
legal_basis: "\u{00a7}54 Abs. 2 EEG 2023".to_owned(),
deduction_ct_kwh: SECT54_ABS1_ABS2_CT_KWH,
});
}
if s54.agri_nutzungsnachweis_fehlt {
aw -= SECT54_ABS3_CT_KWH;
applied.push(AwReductionApplied {
description: "\u{00a7}54 Abs.\u{202f}3 EEG 2023 Nachweis der gleichzeitigen \
landwirtschaftlichen Nutzung fehlt"
.to_owned(),
legal_basis: "\u{00a7}54 Abs. 3 EEG 2023".to_owned(),
deduction_ct_kwh: SECT54_ABS3_CT_KWH,
});
}
}
(aw.max(Decimal::ZERO), applied)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::{AusschreibungMetadata, TariffSource};
fn auction() -> TariffSource {
TariffSource::Auction(AusschreibungMetadata::default())
}
#[test]
fn sect53b_deducts_the_statutory_tenth_of_a_cent() {
let ctx = AwReductionContext {
regionalnachweis_ausgestellt: true,
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(8.11),
&ctx,
&TariffSource::Statutory,
ErzeugungsArt::SolarAufdach,
);
assert_eq!(
aw,
dec!(8.01),
"0,1 ct/kWh is fixed by §53b, not a parameter"
);
assert_eq!(applied.len(), 1);
assert_eq!(applied[0].legal_basis, "§53b EEG 2023");
}
#[test]
fn sect53b_does_not_touch_a_tender_determined_aw() {
let ctx = AwReductionContext {
regionalnachweis_ausgestellt: true,
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(5.80),
&ctx,
&auction(),
ErzeugungsArt::SolarFreiflaeche,
);
assert_eq!(aw, dec!(5.80));
assert!(applied.is_empty());
}
#[test]
fn sect53b_reaches_transitional_plants() {
let ctx = AwReductionContext {
regionalnachweis_ausgestellt: true,
..AwReductionContext::default()
};
let ts = TariffSource::Transitional(crate::scheme::Paragraph100Rule::OldPlantBeforeEeg2023);
let (aw, _) = apply_aw_reductions(dec!(9.00), &ctx, &ts, ErzeugungsArt::SolarAufdach);
assert_eq!(aw, dec!(8.90));
}
#[test]
fn sect53c_deducts_the_granted_exemption() {
let ctx = AwReductionContext {
stromsteuerbefreiung_ct_kwh: Some(dec!(2.05)),
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(8.11),
&ctx,
&TariffSource::Statutory,
ErzeugungsArt::SolarAufdach,
);
assert_eq!(aw, dec!(6.06));
assert_eq!(applied[0].legal_basis, "§53c EEG 2023");
}
#[test]
fn sect53c_is_capped_at_the_full_stromsteuer_rate() {
let ctx = AwReductionContext {
stromsteuerbefreiung_ct_kwh: Some(dec!(9.99)),
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(8.11),
&ctx,
&TariffSource::Statutory,
ErzeugungsArt::SolarAufdach,
);
assert_eq!(aw, dec!(6.06));
assert_eq!(applied[0].deduction_ct_kwh, dec!(2.05));
}
#[test]
fn sect54_does_not_reach_wind() {
let ctx = AwReductionContext {
sect54_solar: Some(Sect54SolarReduction {
zahlungsberechtigung_nach_18_monaten: true,
..Sect54SolarReduction::default()
}),
..AwReductionContext::default()
};
let (aw, applied) =
apply_aw_reductions(dec!(7.35), &ctx, &auction(), ErzeugungsArt::WindOnshore);
assert_eq!(aw, dec!(7.35));
assert!(applied.is_empty());
}
#[test]
fn sect54_abs1_and_abs2_stack() {
let ctx = AwReductionContext {
sect54_solar: Some(Sect54SolarReduction {
zahlungsberechtigung_nach_18_monaten: true,
flurstueck_abweichung: true,
..Sect54SolarReduction::default()
}),
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(5.80),
&ctx,
&auction(),
ErzeugungsArt::SolarFreiflaeche,
);
assert_eq!(aw, dec!(5.20));
assert_eq!(applied.len(), 2);
}
#[test]
fn sect54_abs3_deducts_two_and_a_half_cents() {
let ctx = AwReductionContext {
sect54_solar: Some(Sect54SolarReduction {
agri_nutzungsnachweis_fehlt: true,
..Sect54SolarReduction::default()
}),
..AwReductionContext::default()
};
let (aw, _) = apply_aw_reductions(dec!(8.00), &ctx, &auction(), ErzeugungsArt::SolarAgriPv);
assert_eq!(aw, dec!(5.50));
}
#[test]
fn sect54_abs4_zeroes_the_aw_and_subsumes_the_rest() {
let ctx = AwReductionContext {
sect54_solar: Some(Sect54SolarReduction {
zahlungsberechtigung_nach_18_monaten: true,
flurstueck_abweichung: true,
agri_nutzungsnachweis_fehlt: true,
landesverordnung_nicht_erfuellt: true,
}),
..AwReductionContext::default()
};
let (aw, applied) = apply_aw_reductions(
dec!(5.80),
&ctx,
&auction(),
ErzeugungsArt::SolarFreiflaeche,
);
assert_eq!(aw, Decimal::ZERO);
assert_eq!(
applied.len(),
1,
"Abs. 4 replaces the others, not adds to them"
);
assert_eq!(applied[0].legal_basis, "§54 Abs. 4 EEG 2023");
}
#[test]
fn the_reduced_aw_never_goes_negative() {
let ctx = AwReductionContext {
regionalnachweis_ausgestellt: true,
stromsteuerbefreiung_ct_kwh: Some(dec!(2.05)),
sect54_solar: Some(Sect54SolarReduction {
agri_nutzungsnachweis_fehlt: true,
..Sect54SolarReduction::default()
}),
};
let (aw, _) = apply_aw_reductions(
dec!(1.00),
&ctx,
&TariffSource::Statutory,
ErzeugungsArt::SolarAufdach,
);
assert_eq!(aw, Decimal::ZERO);
}
#[test]
fn an_empty_context_is_detected_and_changes_nothing() {
let ctx = AwReductionContext::default();
assert!(ctx.is_empty());
let (aw, applied) = apply_aw_reductions(
dec!(8.11),
&ctx,
&TariffSource::Statutory,
ErzeugungsArt::SolarAufdach,
);
assert_eq!(aw, dec!(8.11));
assert!(applied.is_empty());
}
}