use crate::EuroAmount;
use rust_decimal::Decimal;
use rust_decimal::dec;
use crate::model::{SettleInput, SettleOutput, SettlePosition, SettlementStatus};
use crate::scheme::SettlementScheme;
fn validated_eur(d: Decimal) -> Decimal {
EuroAmount::from_decimal_rounded(d, billing::RoundingStrategy::MidpointAwayFromZero)
.map(crate::EuroAmount::into_decimal)
.unwrap_or_else(|_| panic!("settlement amount {d} EUR exceeds the EuroAmount range"))
}
fn pos(
description: impl Into<String>,
legal_basis: impl Into<String>,
kwh: Decimal,
rate_ct_kwh: Decimal,
) -> SettlePosition {
let eur = validated_eur(kwh * rate_ct_kwh / Decimal::from(100));
SettlePosition {
description: description.into(),
legal_basis: legal_basis.into(),
kwh,
rate_ct_kwh,
eur,
}
}
fn total(positions: &[SettlePosition]) -> Option<Decimal> {
if positions.is_empty() {
None
} else {
Some(positions.iter().map(|p| p.eur).sum())
}
}
fn sect51_greift(
kwh_during_negative_epex: Option<Decimal>,
leistung_kwp: Option<Decimal>,
regime: crate::negativpreis::NegativpreisRegime,
erzeugungsart: Option<crate::technology::ErzeugungsArt>,
has_imesys: bool,
ist_pilotwindanlage: bool,
) -> bool {
if kwh_during_negative_epex.is_none_or(|k| k <= Decimal::ZERO) {
return false;
}
!regime.ist_befreit(leistung_kwp, erzeugungsart, has_imesys, ist_pilotwindanlage)
}
fn apply_negativpreis(kwh: Decimal, negative_kwh: Decimal) -> Decimal {
(kwh - negative_kwh).max(Decimal::ZERO)
}
fn aggregierte_leistung_kwp(input: &SettleInput) -> Option<Decimal> {
if input.capacity_blocks.is_empty() {
return input.leistung_kwp;
}
let blocks: Decimal = input.capacity_blocks.iter().map(|b| b.leistung_kwp).sum();
Some(input.leistung_kwp.unwrap_or(Decimal::ZERO) + blocks)
}
fn resolve_wind_korrekturfaktor(
wind_korrekturfaktor: Option<Decimal>,
wind_standort: Option<&crate::wind::WindStandort>,
) -> Option<Decimal> {
wind_korrekturfaktor.or_else(|| wind_standort.map(|ws| ws.korrekturfaktor))
}
fn calculate_with_capacity_blocks(input: &SettleInput, total_kwh: Decimal) -> SettleOutput {
let billing_date = input.billing_date;
let Some(primary_kwp) = input.leistung_kwp.filter(|kw| *kw > Decimal::ZERO) else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![crate::model::SettlePosition {
description: "§24 Konfigurationsfehler: leistung_kwp fehlt oder ist null"
.to_owned(),
legal_basis: "§24 EEG 2023".to_owned(),
kwh: Decimal::ZERO,
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::NoData,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let additional_total_kwp: Decimal = input.capacity_blocks.iter().map(|b| b.leistung_kwp).sum();
let total_kwp = primary_kwp + additional_total_kwp;
let Some(primary_rate) = input.scheme.verguetungssatz_ct() else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![crate::model::SettlePosition {
description: "§24 Anlagenerweiterung: das Abrechnungsmodell führt keinen \
Vergütungssatz je Block (Marktprämie/Post-EEG) — kein Preis ermittelbar"
.to_owned(),
legal_basis: "§24 EEG 2023".to_owned(),
kwh: Decimal::ZERO,
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let mut positions: Vec<SettlePosition> = Vec::new();
let mut total_eligible = Decimal::ZERO;
let primary_expired =
billing_date.is_some_and(|d| input.foerderendedatum.is_some_and(|fed| d > fed));
if !primary_expired {
let share = if total_kwp.is_zero() {
Decimal::ONE
} else {
(primary_kwp / total_kwp).round_dp(6)
};
let mut block_kwh = (total_kwh * share).round_dp(3);
if sect51_greift(
input.kwh_during_negative_epex,
Some(total_kwp),
input.negativpreis_regime(),
input.erzeugungsart,
input.has_imesys,
input.ist_pilotwindanlage,
) {
let neg_share = input
.kwh_during_negative_epex
.map(|n| (n * share).round_dp(3))
.unwrap_or(Decimal::ZERO);
block_kwh = apply_negativpreis(block_kwh, neg_share);
}
if block_kwh > Decimal::ZERO || primary_rate != Decimal::ZERO {
let ibn_label = input
.inbetriebnahme
.map(|d| format!(" (IBN {d})"))
.unwrap_or_default();
positions.push(pos(
format!("Einspeiseverg\u{00fc}tung {primary_kwp}\u{202f}kWp-Block{ibn_label}"),
"\u{00a7}21 EEG",
block_kwh,
primary_rate,
));
}
total_eligible += block_kwh;
}
for (idx, block) in input.capacity_blocks.iter().enumerate() {
let block_expired = billing_date.is_some_and(|d| d > block.foerderendedatum);
if block_expired {
continue;
}
let share = if total_kwp.is_zero() {
Decimal::ZERO
} else {
(block.leistung_kwp / total_kwp).round_dp(6)
};
let mut block_kwh = (total_kwh * share).round_dp(3);
if sect51_greift(
input.kwh_during_negative_epex,
Some(total_kwp),
crate::negativpreis::NegativpreisRegime::fuer_inbetriebnahme(block.inbetriebnahme),
input.erzeugungsart,
input.has_imesys,
input.ist_pilotwindanlage,
) {
let neg_share = input
.kwh_during_negative_epex
.map(|n| (n * share).round_dp(3))
.unwrap_or(Decimal::ZERO);
block_kwh = apply_negativpreis(block_kwh, neg_share);
}
let block_num = idx + 1;
if block_kwh > Decimal::ZERO || block.verguetungssatz_ct != Decimal::ZERO {
positions.push(pos(
format!(
"Einspeiseverg\u{00fc}tung {}\u{202f}kWp-Block\u{202f}{} (IBN {})",
block.leistung_kwp, block_num, block.inbetriebnahme
),
"\u{00a7}21 EEG",
block_kwh,
block.verguetungssatz_ct,
));
}
total_eligible += block_kwh;
}
let is_empty = positions.is_empty();
let settlement_eur = total(&positions);
SettleOutput {
settlement_eur,
eligible_kwh: Some(total_eligible),
positions,
status: if is_empty {
SettlementStatus::FoerderungBeendet
} else {
SettlementStatus::Calculated
},
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
pub fn calculate_settlement(input: &SettleInput) -> SettleOutput {
if input
.erzeugungsart
.is_some_and(|a| a == crate::technology::ErzeugungsArt::BiomassHolz)
&& input
.inbetriebnahme
.is_some_and(|d| d >= time::macros::date!(2026 - 01 - 01))
{
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: input.einspeisemenge_kwh,
positions: vec![crate::model::SettlePosition {
description: "§42a EEG 2023: Holzbiomasse-Anlage ab 2026-01-01 nicht förderfähig"
.to_owned(),
legal_basis: "§42a EEG 2023".to_owned(),
kwh: input.einspeisemenge_kwh.unwrap_or(Decimal::ZERO),
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::Sanctioned,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
if let Some(biomasse) = &input.biomasse
&& !biomasse.substrate_cap_ok
{
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: input.einspeisemenge_kwh,
positions: vec![crate::model::SettlePosition {
description: "§43 Abs. 1 Nr. 2 EEG 2023: Substratdeckel überschritten — \
Energiepflanzen-Anteil > 40 %"
.to_owned(),
legal_basis: "§43 Abs. 1 Nr. 2 EEG 2023".to_owned(),
kwh: input.einspeisemenge_kwh.unwrap_or(Decimal::ZERO),
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::Sanctioned,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
let deduplicated_pflichtverstoss: Vec<_> = {
use std::collections::HashMap;
let mut by_typ: HashMap<u8, &crate::model::Pflichtverstoss> = HashMap::new();
for v in &input.pflichtverstoss {
let key = v.typ as u8;
let entry = by_typ.entry(key).or_insert(v);
if v.monate_des_verstosses > entry.monate_des_verstosses {
*entry = v;
}
}
by_typ.into_values().collect()
};
let pflichtzahlung_eur = if deduplicated_pflichtverstoss.is_empty() {
None
} else {
let raw_sum: rust_decimal::Decimal = deduplicated_pflichtverstoss
.iter()
.map(|v| crate::foerderdauer::calculate_pflichtzahlung(v))
.sum();
let cap = deduplicated_pflichtverstoss
.iter()
.map(|v| {
use rust_decimal::dec;
v.leistung_kw * dec!(10) * rust_decimal::Decimal::from(v.monate_des_verstosses)
})
.fold(
rust_decimal::Decimal::ZERO,
|a, b| if b > a { b } else { a },
);
Some(raw_sum.min(cap))
};
let mut result = calculate_settlement_inner(input);
result.pflichtzahlung_eur = pflichtzahlung_eur;
if result
.pflichtzahlung_eur
.is_some_and(|p| p > rust_decimal::Decimal::ZERO)
{
result.pflichtzahlung_faelligkeitsdatum = result.faelligkeitsdatum; }
if !input.tariff_source.is_biogas_sect51b()
&& let Some(lost_qh) = input.negative_price_quarter_hours.filter(|&q| q > 0)
&& input.scheme.negativpreis_rule_applicable()
&& input
.negativpreis_regime()
.verlaengerungsanspruch(input.tariff_source.is_auction())
&& sect51_greift(
input.kwh_during_negative_epex,
aggregierte_leistung_kwp(input),
input.negativpreis_regime(),
input.erzeugungsart,
input.has_imesys,
input.ist_pilotwindanlage,
)
{
let is_solar = input.erzeugungsart.is_some_and(|a| a.is_solar());
result.verlaengerungsanspruch_qh =
crate::foerderdauer::verguetungszeitraum_verlaengerung_qh(lost_qh, is_solar);
}
if matches!(input.scheme, SettlementScheme::TemporaryFeedInTariff { .. })
&& input.sect51_abs3_unreported_days > 0
&& !matches!(
result.status,
SettlementStatus::NoData | SettlementStatus::PriceMissing
)
{
let tage = Decimal::from(input.sect51_abs3_unreported_days);
let faktor = (Decimal::ONE - dec!(0.05) * tage).max(Decimal::ZERO);
if let Some(eur) = result.settlement_eur {
let gekuerzt = validated_eur(eur * faktor);
result.positions.push(crate::model::SettlePosition {
description: format!(
"\u{00a7}51 Abs. 3 EEG: Meldung der Einspeisung w\u{00e4}hrend negativer Preise unterblieben \u{2014} {} % K\u{00fc}rzung ({tage} Kalendertage)",
(Decimal::ONE - faktor) * Decimal::from(100)
),
legal_basis: "\u{00a7}51 Abs. 3 EEG".to_owned(),
kwh: Decimal::ZERO,
rate_ct_kwh: Decimal::ZERO,
eur: gekuerzt - eur,
});
result.settlement_eur = Some(gekuerzt);
}
}
let billing_days_fraction = input.billing_days_fraction.or_else(|| {
crate::foerderdauer::compute_billing_days_fraction(
input.foerderendedatum,
input.billing_date,
)
});
if let Some(fraction) =
billing_days_fraction.filter(|&f| f > Decimal::ZERO && f < rust_decimal::Decimal::ONE)
{
if let Some(eur) = result.settlement_eur {
result.settlement_eur = Some(validated_eur(eur * fraction));
}
if let Some(kwh) = result.eligible_kwh {
result.eligible_kwh = Some((kwh * fraction).round_dp(3));
}
for p in &mut result.positions {
p.eur = validated_eur(p.eur * fraction);
p.kwh = (p.kwh * fraction).round_dp(3);
}
}
result.billing_days_fraction_applied =
billing_days_fraction.filter(|&f| f > Decimal::ZERO && f < rust_decimal::Decimal::ONE);
if let Some(einsman_kwh) = input.einspeisemanagement_kwh.filter(|k| *k > Decimal::ZERO)
&& !matches!(
result.status,
SettlementStatus::NoData | SettlementStatus::PriceMissing
)
&& !matches!(
input.scheme,
crate::scheme::SettlementScheme::Eigenverbrauch
)
{
let comp_rate_ct = match &input.scheme {
crate::scheme::SettlementScheme::MarketPremium {
direktverm_aw_ct,
wind_korrekturfaktor,
wind_standort,
..
} => {
let raw_aw = *direktverm_aw_ct;
if let Some(k) =
resolve_wind_korrekturfaktor(*wind_korrekturfaktor, wind_standort.as_ref())
{
(raw_aw * k).round_dp(5)
} else {
raw_aw
}
}
_ => input.scheme.verguetungssatz_ct().unwrap_or(Decimal::ZERO),
};
let comp_eur = validated_eur(einsman_kwh * comp_rate_ct / Decimal::from(100));
result.positions.push(crate::model::SettlePosition {
description: format!("Einspeisemanagement-Ausfall §13a EnWG ({einsman_kwh} kWh)"),
legal_basis: "§13a EnWG (Redispatch 2.0)".to_owned(),
kwh: einsman_kwh,
rate_ct_kwh: comp_rate_ct,
eur: comp_eur,
});
result.settlement_eur = Some(result.settlement_eur.unwrap_or(Decimal::ZERO) + comp_eur);
result.eligible_kwh = Some(result.eligible_kwh.unwrap_or(Decimal::ZERO) + einsman_kwh);
}
if let Some(bd) = input.billing_date {
let m = bd.month();
let y = bd.year();
let (next_year, next_month) = if m == time::Month::December {
(y + 1, time::Month::January)
} else {
(y, m.next())
};
result.faelligkeitsdatum = time::Date::from_calendar_date(next_year, next_month, 15).ok();
}
if result
.pflichtzahlung_eur
.is_some_and(|p| p > rust_decimal::Decimal::ZERO)
{
result.dezentrale_einspeisung_anspruch_verloren = true;
}
if let Some(sect44b_eligible) = input.biogas_sect44b_eligible_kwh {
let effective_kwh = result.eligible_kwh.unwrap_or(rust_decimal::Decimal::ZERO);
if effective_kwh > rust_decimal::Decimal::ZERO && sect44b_eligible < effective_kwh {
let excess_kwh = effective_kwh - sect44b_eligible;
let ratio = (sect44b_eligible / effective_kwh).min(rust_decimal::Decimal::ONE);
if let Some(eur) = result.settlement_eur {
result.settlement_eur = Some(validated_eur(eur * ratio));
}
for p in &mut result.positions {
p.eur = validated_eur(p.eur * ratio);
p.kwh = (p.kwh * ratio).round_dp(3);
}
match &input.scheme {
crate::scheme::SettlementScheme::FeedInTariff { .. }
| crate::scheme::SettlementScheme::TenantElectricity { .. }
| crate::scheme::SettlementScheme::TemporaryFeedInTariff { .. }
| crate::scheme::SettlementScheme::FlexibilityPremium { .. } => {
let Some(excess_rate) = input.marktwert_ct_kwh else {
result.status = SettlementStatus::PriceMissing;
return result;
};
let excess_eur =
validated_eur(excess_kwh * excess_rate / rust_decimal::Decimal::from(100));
result.positions.push(crate::model::SettlePosition {
description: format!("§44b Abs. 1 Überschuss Marktwert ({excess_kwh} kWh)"),
legal_basis: "§44b Abs. 1 EEG 2023".to_owned(),
kwh: excess_kwh,
rate_ct_kwh: excess_rate,
eur: excess_eur,
});
result.settlement_eur = Some(
result.settlement_eur.unwrap_or(rust_decimal::Decimal::ZERO) + excess_eur,
);
}
crate::scheme::SettlementScheme::MarketPremium { .. } => {
result.positions.push(crate::model::SettlePosition {
description: format!("§44b Abs. 1 Überschuss (AW = 0, {excess_kwh} kWh)"),
legal_basis: "§44b Abs. 1 EEG 2023".to_owned(),
kwh: excess_kwh,
rate_ct_kwh: rust_decimal::Decimal::ZERO,
eur: rust_decimal::Decimal::ZERO,
});
}
_ => {} }
}
}
result
}
fn calculate_settlement_inner(input: &SettleInput) -> SettleOutput {
use crate::model::SanktionAlt;
match input.sanktion {
Some(SanktionAlt::VerguetungAufNull) => {
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: input.einspeisemenge_kwh,
positions: vec![],
status: SettlementStatus::Sanctioned,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
Some(SanktionAlt::VerguetungAufMarktwert) => {
let Some(epex_ct) = input.marktwert_ct_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let Some(kwh) = input.einspeisemenge_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::NoData,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let positions = vec![pos(
"Einspeisevergütung §52 Abs. 2 EEG (auf Marktwert verringert)",
"§52 Abs. 2 EEG",
kwh,
epex_ct,
)];
return SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(kwh),
positions,
status: SettlementStatus::Sanctioned,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
Some(SanktionAlt::VerguetungReduziert20Prozent) => {
let base = settle_normal_body(input);
let reduced_eur = base.settlement_eur.map(|e| (e * dec!(0.80)).round_dp(2));
return SettleOutput {
settlement_eur: reduced_eur,
eligible_kwh: base.eligible_kwh,
positions: base.positions,
status: SettlementStatus::Sanctioned,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
None => {} }
settle_normal_body(input)
}
fn apply_aw_cuts(
aw_ct: Decimal,
input: &SettleInput,
) -> (Decimal, Vec<crate::aw_reductions::AwReductionApplied>) {
let aw_ct = if sect51_optin_active(input) {
aw_ct + crate::negativpreis::SECT51_OPTIN_ZUSCHLAG_CT_KWH
} else {
aw_ct
};
if input.aw_reductions.is_empty() {
return (aw_ct, Vec::new());
}
crate::aw_reductions::apply_aw_reductions(
aw_ct,
&input.aw_reductions,
&input.tariff_source,
input.erzeugungsart.unwrap_or_default(),
)
}
fn sect51_optin_active(input: &SettleInput) -> bool {
matches!(
(input.sect51_optin_wirksam_ab, input.billing_date),
(Some(ab), Some(bd)) if bd >= ab
)
}
fn aw_cut_positions(
cuts: &[crate::aw_reductions::AwReductionApplied],
kwh: Decimal,
) -> Vec<crate::model::SettlePosition> {
cuts.iter()
.map(|c| crate::model::SettlePosition {
description: c.description.clone(),
legal_basis: c.legal_basis.clone(),
kwh,
rate_ct_kwh: -c.deduction_ct_kwh,
eur: Decimal::ZERO,
})
.collect()
}
fn settle_normal_body(input: &SettleInput) -> SettleOutput {
if input.capacity_blocks.is_empty()
&& let (Some(billing), Some(fed)) = (input.billing_date, input.foerderendedatum)
&& billing > fed
{
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: input.einspeisemenge_kwh,
positions: vec![],
status: SettlementStatus::FoerderungBeendet,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
let Some(kwh) = input.einspeisemenge_kwh else {
if let SettlementScheme::FlexibilitySurcharge {
rate_eur_per_kw_year,
} = &input.scheme
{
let kwh_dummy = Decimal::ZERO;
let kw = input.leistung_kwp.unwrap_or(Decimal::ZERO);
let rate_eur_per_kw_year = *rate_eur_per_kw_year;
let monthly_eur = validated_eur(kw * rate_eur_per_kw_year / dec!(12));
let positions = vec![SettlePosition {
description: format!(
"Flexibilit\u{00e4}tszuschlag \u{00a7}50a EEG 2023 \
({kw}\u{202f}kW \u{00d7} {rate_eur_per_kw_year}\u{202f}EUR/kW/Jahr \u{00f7} 12)"
),
legal_basis: "\u{00a7}50a EEG 2023".to_owned(),
kwh: kw,
rate_ct_kwh: rate_eur_per_kw_year,
eur: monthly_eur,
}];
let _ = kwh_dummy; return SettleOutput {
settlement_eur: Some(monthly_eur),
eligible_kwh: Some(kw),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::NoData,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
if !input.capacity_blocks.is_empty() {
return calculate_with_capacity_blocks(input, kwh);
}
let apply_neg = input.scheme.negativpreis_rule_applicable()
&& sect51_greift(
input.kwh_during_negative_epex,
aggregierte_leistung_kwp(input),
input.negativpreis_regime(),
input.erzeugungsart,
input.has_imesys,
input.ist_pilotwindanlage,
);
let neg_kwh = if apply_neg {
input.kwh_during_negative_epex
} else {
None
};
match &input.scheme {
SettlementScheme::Eigenverbrauch => SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: Some(kwh),
positions: vec![],
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
},
SettlementScheme::SonstigeDirektvermarktung => SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: Some(kwh),
positions: vec![crate::model::SettlePosition {
description:
"Sonstige Direktvermarktung \u{00a7}21a EEG 2023 (kein EEG-Zahlungsanspruch)"
.to_owned(),
legal_basis: "\u{00a7}21a EEG 2023".to_owned(),
kwh,
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
},
SettlementScheme::TemporaryFeedInTariff { verguetungssatz_ct }
| SettlementScheme::FeedInTariff { verguetungssatz_ct } => {
let ist_ausfallverguetung =
matches!(input.scheme, SettlementScheme::TemporaryFeedInTariff { .. });
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let rate_ct = if input.aw_is_gross
&& !ist_ausfallverguetung
&& let Some(art) = input.erzeugungsart
{
(*verguetungssatz_ct - crate::rates::sect53_deduction(art)).max(Decimal::ZERO)
} else {
*verguetungssatz_ct
};
let rate_ct = if ist_ausfallverguetung {
(rate_ct * dec!(0.8)).round_dp(2)
} else {
rate_ct
};
let (rate_ct, aw_cuts) = apply_aw_cuts(rate_ct, input);
let (desc, basis) = match (ist_ausfallverguetung, neg_kwh.is_some()) {
(true, true) => (
"Ausfallverg\u{00fc}tung \u{00a7}21 Abs. 1 Satz 1 Nr. 3 EEG (\u{2212}20 % nach \u{00a7}53 Abs. 3; \u{00a7}51 Negativpreisregel angewendet)",
"\u{00a7}21 Abs. 1 Satz 1 Nr. 3 EEG 2023",
),
(true, false) => (
"Ausfallverg\u{00fc}tung \u{00a7}21 Abs. 1 Satz 1 Nr. 3 EEG (\u{2212}20 % nach \u{00a7}53 Abs. 3)",
"\u{00a7}21 Abs. 1 Satz 1 Nr. 3 EEG 2023",
),
(false, true) => (
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG (\u{00a7}51 Negativpreisregel angewendet)",
"\u{00a7}21 EEG 2023",
),
(false, false) => (
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG",
"\u{00a7}21 EEG 2023",
),
};
let mut positions = vec![pos(desc, basis, effective, rate_ct)];
positions.extend(aw_cut_positions(&aw_cuts, effective));
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::TenantElectricity {
verguetungssatz_ct,
mieter_zuschlag_ct,
} => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let zuschlag = mieter_zuschlag_ct.unwrap_or(Decimal::ZERO);
let base_desc = if neg_kwh.is_some() {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG (\u{00a7}51 Negativpreisregel angewendet)"
} else {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG"
};
let mut positions = vec![pos(
base_desc,
"\u{00a7}21 EEG 2023",
effective,
*verguetungssatz_ct,
)];
if zuschlag != Decimal::ZERO {
positions.push(pos(
"Mieterstrom-Zuschlag \u{00a7}21 Abs. 3 EEG 2023",
"\u{00a7}21 Abs. 3 EEG 2023",
effective,
zuschlag,
));
}
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::MarketPremium {
direktverm_aw_ct,
wind_korrekturfaktor,
wind_standort,
} => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let epex_source = input.marktwert_ct_kwh;
let Some(epex_ct) = epex_source else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let raw_aw_ct = *direktverm_aw_ct;
if raw_aw_ct <= Decimal::ZERO {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![crate::model::SettlePosition {
description: "Marktpr\u{00e4}mie ohne anzulegenden Wert: direktverm_aw_ct fehlt oder ist null"
.to_owned(),
legal_basis: "\u{00a7}20 EEG 2023 i.V.m. Anlage 1".to_owned(),
kwh: Decimal::ZERO,
rate_ct_kwh: Decimal::ZERO,
eur: Decimal::ZERO,
}],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
if input.tariff_source.is_biogas_sect51b()
&& crate::version::sect51b_anwendbar(input.billing_date)
&& epex_ct <= dec!(2)
{
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: input.einspeisemenge_kwh,
positions: vec![pos(
"\u{00a7}51b EEG 2023 Biogasanlage Ausschreibung \
(Spotmarktpreis \u{2264} 2\u{202f}ct/kWh \u{2192} AW = 0)",
"\u{00a7}51b EEG 2023",
kwh,
Decimal::ZERO,
)],
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
let aw_ct = if let Some(k) =
resolve_wind_korrekturfaktor(*wind_korrekturfaktor, wind_standort.as_ref())
{
(raw_aw_ct * k).round_dp(5)
} else {
raw_aw_ct
};
let (aw_ct, aw_cuts) = apply_aw_cuts(aw_ct, input);
if input.tariff_source.is_innovation_auction() {
let feste_praemie_eur = validated_eur(effective * aw_ct / Decimal::from(100));
let mut positions = vec![pos(
"Feste Marktpr\u{00e4}mie \u{00a7}39n EEG 2023 (Innovationsausschreibung, \u{00a7}3 InnAusV)",
"\u{00a7}39n EEG 2023 i.V.m. \u{00a7}3 InnAusV",
effective,
aw_ct,
)];
positions.extend(aw_cut_positions(&aw_cuts, effective));
return SettleOutput {
settlement_eur: Some(feste_praemie_eur),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
let praemie_ct = (aw_ct - epex_ct).max(Decimal::ZERO);
let (praemie_desc, praemie_basis) = if input.tariff_source.is_auction() {
(
"Gleitende Marktpr\u{00e4}mie \u{00a7}\u{00a7}22a,28 EEG 2023 (Ausschreibung)",
"\u{00a7}\u{00a7}22a,28 EEG 2023",
)
} else if neg_kwh.is_some() {
(
"Gleitende Marktpr\u{00e4}mie \u{00a7}23a EEG 2023 (\u{00a7}51 Negativpreisregel angewendet)",
"\u{00a7}23a EEG 2023 i.V.m. Anlage 1",
)
} else {
(
"Gleitende Marktpr\u{00e4}mie \u{00a7}23a EEG 2023",
"\u{00a7}23a EEG 2023 i.V.m. Anlage 1",
)
};
let mut positions = vec![pos(praemie_desc, praemie_basis, effective, praemie_ct)];
positions.extend(aw_cut_positions(&aw_cuts, effective));
let total_eur = positions.iter().map(|p| p.eur).sum();
SettleOutput {
settlement_eur: Some(total_eur),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::PostEeg { price_floor } => {
let Some(epex_ct) = input.marktwert_ct_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
};
let epex_floored = if let Some(floor) = *price_floor {
epex_ct.max(floor)
} else {
epex_ct
};
let effective_ct = if epex_floored > dec!(10) {
dec!(10)
} else {
epex_floored
};
let was_capped = epex_floored > dec!(10);
let desc = if was_capped {
format!(
"Einspeiseverg\u{00fc}tung Post-EEG Spot \
(\u{00a7}23b Jahresmarktwert-Deckel: EPEX {:.2}\u{202f}ct \u{2192} 10\u{202f}ct)",
epex_ct
)
} else {
"Einspeiseverg\u{00fc}tung Post-EEG Spot (\u{00a7}21 EEG, nach F\u{00f6}rderungsende)".to_owned()
};
let positions = vec![pos(
desc,
"\u{00a7}21 EEG (post-F\u{00f6}rderung)",
kwh,
effective_ct,
)];
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(kwh),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::KwkSurcharge {
verguetungssatz_ct,
kwh_paid_gesamt,
max_kwh,
} => {
use crate::foerderdauer::kwk_eligible_kwh;
let (eligible, limit_reached) = match (*kwh_paid_gesamt, *max_kwh) {
(Some(paid), Some(max)) => kwk_eligible_kwh(kwh, paid, max),
_ => (kwh, false),
};
if eligible <= Decimal::ZERO {
return SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: Some(Decimal::ZERO),
positions: vec![],
status: SettlementStatus::FoerderungBeendet,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
};
}
let desc = if limit_reached {
format!(
"KWK-Zuschlag \u{00a7}7 KWKG 2023 (F\u{00f6}rderdauer-Endabrechnung: {eligible} von {kwh} kWh)"
)
} else {
"KWK-Zuschlag \u{00a7}7 KWKG 2023".to_owned()
};
let positions = vec![pos(
desc,
"\u{00a7}7 KWKG 2023",
eligible,
*verguetungssatz_ct,
)];
let status = if limit_reached {
SettlementStatus::FoerderungBeendet
} else {
SettlementStatus::Calculated
};
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(eligible),
positions,
status,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::FlexibilityPremium {
verguetungssatz_ct,
flex_praemie_ct_kwh,
} => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let flex_ct = flex_praemie_ct_kwh.unwrap_or(Decimal::ZERO);
let base_desc = if neg_kwh.is_some() {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG (\u{00a7}51 Negativpreisregel angewendet)"
} else {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG"
};
let mut positions = vec![pos(
base_desc,
"\u{00a7}21 EEG 2023",
effective,
*verguetungssatz_ct,
)];
if flex_ct != Decimal::ZERO {
positions.push(pos(
"Flexibilit\u{00e4}tspr\u{00e4}mie \u{00a7}50b EEG 2023 (bestehende Anlage)",
"\u{00a7}50b EEG 2023",
effective,
flex_ct,
));
}
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
SettlementScheme::FlexibilitySurcharge {
rate_eur_per_kw_year,
} => {
let kw = input.leistung_kwp.unwrap_or(Decimal::ZERO);
let rate_eur_per_kw_year = *rate_eur_per_kw_year;
let monthly_eur = validated_eur(kw * rate_eur_per_kw_year / dec!(12));
let positions = vec![SettlePosition {
description: format!(
"Flexibilit\u{00e4}tszuschlag \u{00a7}50a EEG 2023 \
({kw}\u{202f}kW \u{00d7} {rate_eur_per_kw_year}\u{202f}EUR/kW/Jahr \u{00f7} 12)"
),
legal_basis: "\u{00a7}50a EEG 2023".to_owned(),
kwh: kw, rate_ct_kwh: rate_eur_per_kw_year, eur: monthly_eur,
}];
SettleOutput {
settlement_eur: Some(monthly_eur),
eligible_kwh: Some(kw),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
pflichtzahlung_faelligkeitsdatum: None,
verlaengerungsanspruch_qh: 0,
dezentrale_einspeisung_anspruch_verloren: false,
billing_days_fraction_applied: None,
faelligkeitsdatum: None,
}
}
}
}