use billing::EuroAmount;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use crate::model::{SettleInput, SettleOutput, SettlePosition, SettlementModel, SettlementStatus};
fn validated_eur(d: Decimal) -> Decimal {
EuroAmount::checked_from_decimal(d)
.map(|a| a.into_decimal())
.unwrap_or(Decimal::ZERO)
}
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 should_apply_negativpreis_versioned(
kwh_during_negative_epex: Option<Decimal>,
leistung_kwp: Option<Decimal>,
eeg_gesetz: crate::version::EegGesetz,
erzeugungsart: Option<crate::technology::ErzeugungsArt>,
) -> bool {
if kwh_during_negative_epex.is_none_or(|k| k <= Decimal::ZERO) {
return false;
}
let art = erzeugungsart.unwrap_or(crate::technology::ErzeugungsArt::Solar);
let Some(threshold_kw) = eeg_gesetz.negativpreis_kw_grenze(&art) else {
return false; };
leistung_kwp.is_none_or(|kw| kw >= Decimal::from(threshold_kw))
}
fn apply_negativpreis(kwh: Decimal, negative_kwh: Decimal) -> Decimal {
(kwh - negative_kwh).max(Decimal::ZERO)
}
fn resolve_managementpraemie(
managementpraemie_ct: Option<Decimal>,
leistung_kwp: Option<Decimal>,
) -> Decimal {
managementpraemie_ct.unwrap_or_else(|| {
leistung_kwp
.map(crate::foerderdauer::managementpraemie_ct)
.unwrap_or(Decimal::ZERO)
})
}
fn calculate_with_capacity_blocks(input: &SettleInput, total_kwh: Decimal) -> SettleOutput {
let billing_date = input.billing_date;
let primary_kwp = input.leistung_kwp.unwrap_or(dec!(1)); let additional_total_kwp: Decimal = input.capacity_blocks.iter().map(|b| b.leistung_kwp).sum();
let total_kwp = primary_kwp + additional_total_kwp;
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 should_apply_negativpreis_versioned(
input.kwh_during_negative_epex,
Some(primary_kwp),
input.eeg_gesetz,
input.erzeugungsart,
) {
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 || input.verguetungssatz_ct != 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,
input.verguetungssatz_ct,
));
}
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);
let block_gesetz =
crate::version::EegGesetz::from_inbetriebnahme_year(block.inbetriebnahme.year());
if should_apply_negativpreis_versioned(
input.kwh_during_negative_epex,
Some(block.leistung_kwp),
block_gesetz,
input.erzeugungsart,
) {
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,
}
}
pub fn calculate_settlement(input: &SettleInput) -> SettleOutput {
let pflichtzahlung_eur = input
.pflichtverstoss
.as_ref()
.map(crate::foerderdauer::calculate_pflichtzahlung);
let mut result = calculate_settlement_inner(input);
result.pflichtzahlung_eur = pflichtzahlung_eur;
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,
};
}
Some(SanktionAlt::VerguetungAufMarktwert) => {
let Some(epex_ct) = input.epex_avg_ct_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
};
};
let Some(kwh) = input.einspeisemenge_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::NoData,
pflichtzahlung_eur: 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,
};
}
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,
};
}
None => {} }
settle_normal_body(input)
}
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,
};
}
let Some(kwh) = input.einspeisemenge_kwh else {
if input.model == SettlementModel::FlexibilitaetZuschlag {
let kwh_dummy = Decimal::ZERO;
let kw = input.leistung_kwp.unwrap_or(Decimal::ZERO);
let rate_eur_per_kw_year = input.verguetungssatz_ct;
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,
};
}
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::NoData,
pflichtzahlung_eur: None,
};
};
if !input.capacity_blocks.is_empty() {
return calculate_with_capacity_blocks(input, kwh);
}
let apply_neg = should_apply_negativpreis_versioned(
input.kwh_during_negative_epex,
input.leistung_kwp,
input.eeg_gesetz,
input.erzeugungsart,
);
let neg_kwh = if apply_neg {
input.kwh_during_negative_epex
} else {
None
};
match input.model {
SettlementModel::Eigenverbrauch => SettleOutput {
settlement_eur: Some(Decimal::ZERO),
eligible_kwh: Some(kwh),
positions: vec![],
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
},
SettlementModel::Verguetung => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let desc = if neg_kwh.is_some() {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG (\u{00a7}27 Negativpreisregel angewendet)"
} else {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG"
};
let positions = vec![pos(
desc,
"\u{00a7}21 EEG 2023",
effective,
input.verguetungssatz_ct,
)];
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
}
}
SettlementModel::Mieterstrom => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let zuschlag = input.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}27 Negativpreisregel angewendet)"
} else {
"Einspeiseverg\u{00fc}tung \u{00a7}21 EEG"
};
let mut positions = vec![pos(
base_desc,
"\u{00a7}21 EEG 2023",
effective,
input.verguetungssatz_ct,
)];
if zuschlag != Decimal::ZERO {
positions.push(pos(
"Mieterstrom-Zuschlag \u{00a7}38a EEG 2023",
"\u{00a7}38a EEG 2023",
effective,
zuschlag,
));
}
SettleOutput {
settlement_eur: total(&positions),
eligible_kwh: Some(effective),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
}
}
SettlementModel::Direktvermarktung | SettlementModel::Ausschreibung => {
let (Some(aw_ct), Some(epex_ct)) = (input.direktverm_aw_ct, input.epex_avg_ct_kwh)
else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
};
};
let praemie_ct = (aw_ct - epex_ct).max(Decimal::ZERO);
let mgmt_ct = resolve_managementpraemie(input.managementpraemie_ct, input.leistung_kwp);
let (praemie_desc, praemie_basis) = if input.model == SettlementModel::Ausschreibung {
(
"Gleitende Marktpr\u{00e4}mie \u{00a7}\u{00a7}22a,28 EEG 2023 (Ausschreibung)",
"\u{00a7}\u{00a7}22a,28 EEG 2023",
)
} else {
(
"Gleitende Marktpr\u{00e4}mie \u{00a7}20 EEG 2023",
"\u{00a7}20 EEG 2023",
)
};
let mut positions = vec![];
if praemie_ct > Decimal::ZERO || mgmt_ct == Decimal::ZERO {
positions.push(pos(praemie_desc, praemie_basis, kwh, praemie_ct));
}
if mgmt_ct > Decimal::ZERO {
positions.push(pos(
"Managementpr\u{00e4}mie \u{00a7}20 Abs.\u{202f}3 EEG 2023",
"\u{00a7}20 Abs. 3 EEG 2023",
kwh,
mgmt_ct,
));
}
if positions.is_empty() {
positions.push(pos(praemie_desc, praemie_basis, kwh, Decimal::ZERO));
}
let total_eur = positions.iter().map(|p| p.eur).sum();
SettleOutput {
settlement_eur: Some(total_eur),
eligible_kwh: Some(kwh),
positions,
status: SettlementStatus::Calculated,
pflichtzahlung_eur: None,
}
}
SettlementModel::PostEegSpot => {
let Some(epex_ct) = input.epex_avg_ct_kwh else {
return SettleOutput {
settlement_eur: None,
eligible_kwh: None,
positions: vec![],
status: SettlementStatus::PriceMissing,
pflichtzahlung_eur: None,
};
};
let effective_ct = if epex_ct > dec!(10) {
dec!(10)
} else {
epex_ct
};
let was_capped = epex_ct > 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,
}
}
SettlementModel::KwkgZuschlag => {
use crate::foerderdauer::kwk_eligible_kwh;
let (eligible, limit_reached) = match (input.kwk_strom_kwh_gesamt, input.kwk_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,
};
}
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,
input.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,
}
}
SettlementModel::Flexibilitaet => {
let effective = match neg_kwh {
Some(n) => apply_negativpreis(kwh, n),
None => kwh,
};
let flex_ct = input.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,
input.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,
}
}
SettlementModel::FlexibilitaetZuschlag => {
let kw = input.leistung_kwp.unwrap_or(Decimal::ZERO);
let rate_eur_per_kw_year = input.verguetungssatz_ct; 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,
}
}
}
}