use billing::EuroAmount;
use rust_decimal::Decimal;
use crate::error::BillingError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RedispatchVerguetungsart {
Eeg,
Kwkg,
Sonstige,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RedispatchVerguetungInput {
pub ausfallarbeit_kwh: Decimal,
pub verguetungsart: RedispatchVerguetungsart,
pub entgangene_einnahmen_eur: Decimal,
pub zusaetzliche_aufwendungen_eur: Decimal,
pub ersparte_aufwendungen_eur: Decimal,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct RedispatchVerguetung {
pub ausfallarbeit_kwh: Decimal,
pub verguetungsart: RedispatchVerguetungsart,
pub entgangene_einnahmen_eur: Decimal,
pub zusaetzliche_aufwendungen_eur: Decimal,
pub ersparte_aufwendungen_eur: Decimal,
pub verguetung_eur: Decimal,
pub trace: Vec<String>,
}
#[must_use]
pub fn eeg_entgangene_einnahmen(
ausfallarbeit_kwh: Decimal,
anzulegender_wert_ct: Decimal,
) -> Decimal {
(ausfallarbeit_kwh * anzulegender_wert_ct / Decimal::ONE_HUNDRED)
.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
}
pub fn redispatch_verguetung(
input: &RedispatchVerguetungInput,
) -> Result<RedispatchVerguetung, BillingError> {
for (label, v) in [
("ausfallarbeit_kwh", input.ausfallarbeit_kwh),
("entgangene_einnahmen_eur", input.entgangene_einnahmen_eur),
(
"zusaetzliche_aufwendungen_eur",
input.zusaetzliche_aufwendungen_eur,
),
("ersparte_aufwendungen_eur", input.ersparte_aufwendungen_eur),
] {
if v < Decimal::ZERO {
return Err(BillingError::InvalidInput {
reason: format!("§13a component {label} must be non-negative, got {v}"),
});
}
}
let round = |d: Decimal| {
d.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
};
let entgangene = round(input.entgangene_einnahmen_eur);
let zusaetzliche = round(input.zusaetzliche_aufwendungen_eur);
let ersparte = round(input.ersparte_aufwendungen_eur);
let total = entgangene + zusaetzliche - ersparte;
for v in [entgangene, zusaetzliche, ersparte, total] {
let _representable =
EuroAmount::checked_from_decimal(v).map_err(|_| BillingError::MonetaryOverflow {
input_value: Some(v),
})?;
}
let basis = match input.verguetungsart {
RedispatchVerguetungsart::Eeg => "entgangene EEG-Vergütung (§13a Abs. 2 S. 3 Nr. 5 EnWG)",
RedispatchVerguetungsart::Kwkg => "entgangene KWKG-Vergütung (§13a Abs. 2 S. 3 Nr. 5 EnWG)",
RedispatchVerguetungsart::Sonstige => {
"nachgewiesene entgangene Erlöse (§13a Abs. 2 S. 3 Nr. 3 EnWG)"
}
};
Ok(RedispatchVerguetung {
ausfallarbeit_kwh: input.ausfallarbeit_kwh,
verguetungsart: input.verguetungsart,
entgangene_einnahmen_eur: entgangene,
zusaetzliche_aufwendungen_eur: zusaetzliche,
ersparte_aufwendungen_eur: ersparte,
verguetung_eur: total,
trace: vec![
format!("Ausfallarbeit: {} kWh", input.ausfallarbeit_kwh),
format!("+ {entgangene} € {basis}"),
format!("+ {zusaetzliche} € zusätzliche Aufwendungen (Nr. 1/2/4)"),
format!("− {ersparte} € ersparte Aufwendungen (S. 4 — an den NB zu erstatten)"),
format!("= {total} € angemessene Vergütung (§13a Abs. 2 EnWG)"),
],
})
}
pub fn bilarem_finanzielle_korrektur(
ausfallarbeit_kwh: Decimal,
ausgleich_kwh: Decimal,
id_aep_eur_per_mwh: Decimal,
) -> Result<Decimal, BillingError> {
let korr = (ausfallarbeit_kwh - ausgleich_kwh) / Decimal::from(1000) * id_aep_eur_per_mwh;
let rounded =
korr.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero);
if rounded.abs() > Decimal::from(10_000_000) {
return Err(BillingError::InvalidInput {
reason: format!("BilAReM Korr_fin out of range: {rounded}"),
});
}
Ok(rounded)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn eeg_plant_compensation_from_the_anzulegender_wert() {
let entgangene = eeg_entgangene_einnahmen(dec!(12_500), dec!(7.30));
assert_eq!(entgangene, dec!(912.50));
let v = redispatch_verguetung(&RedispatchVerguetungInput {
ausfallarbeit_kwh: dec!(12_500),
verguetungsart: RedispatchVerguetungsart::Eeg,
entgangene_einnahmen_eur: entgangene,
zusaetzliche_aufwendungen_eur: dec!(40),
ersparte_aufwendungen_eur: dec!(12.50),
})
.unwrap();
assert_eq!(v.verguetung_eur, dec!(940.00));
assert!(v.trace.iter().any(|l| l.contains("Nr. 5")));
}
#[test]
fn bilarem_korrektur_settles_the_residual_financially() {
let k = bilarem_finanzielle_korrektur(dec!(1200), dec!(1000), dec!(80)).unwrap();
assert_eq!(k, dec!(16.00));
let k = bilarem_finanzielle_korrektur(dec!(800), dec!(1000), dec!(80)).unwrap();
assert_eq!(k, dec!(-16.00));
let k = bilarem_finanzielle_korrektur(dec!(1200), dec!(1000), dec!(-50)).unwrap();
assert_eq!(k, dec!(-10.00));
}
#[test]
fn saved_costs_can_exceed_the_claim() {
let v = redispatch_verguetung(&RedispatchVerguetungInput {
ausfallarbeit_kwh: dec!(50_000),
verguetungsart: RedispatchVerguetungsart::Sonstige,
entgangene_einnahmen_eur: dec!(2_000),
zusaetzliche_aufwendungen_eur: dec!(100),
ersparte_aufwendungen_eur: dec!(2_500),
})
.unwrap();
assert_eq!(v.verguetung_eur, dec!(-400.00));
}
#[test]
fn negative_components_are_rejected() {
let err = redispatch_verguetung(&RedispatchVerguetungInput {
ausfallarbeit_kwh: dec!(100),
verguetungsart: RedispatchVerguetungsart::Kwkg,
entgangene_einnahmen_eur: dec!(-1),
zusaetzliche_aufwendungen_eur: Decimal::ZERO,
ersparte_aufwendungen_eur: Decimal::ZERO,
});
assert!(err.is_err());
}
}