1use billing::EuroAmount;
23use rust_decimal::Decimal;
24
25use crate::error::BillingError;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
30pub enum RedispatchVerguetungsart {
31 Eeg,
33 Kwkg,
36 Sonstige,
38}
39
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
42pub struct RedispatchVerguetungInput {
43 pub ausfallarbeit_kwh: Decimal,
46 pub verguetungsart: RedispatchVerguetungsart,
48 pub entgangene_einnahmen_eur: Decimal,
51 pub zusaetzliche_aufwendungen_eur: Decimal,
54 pub ersparte_aufwendungen_eur: Decimal,
57}
58
59#[derive(Debug, Clone, serde::Serialize)]
61pub struct RedispatchVerguetung {
62 pub ausfallarbeit_kwh: Decimal,
64 pub verguetungsart: RedispatchVerguetungsart,
66 pub entgangene_einnahmen_eur: Decimal,
68 pub zusaetzliche_aufwendungen_eur: Decimal,
70 pub ersparte_aufwendungen_eur: Decimal,
72 pub verguetung_eur: Decimal,
77 pub trace: Vec<String>,
79}
80
81#[must_use]
88pub fn eeg_entgangene_einnahmen(
89 ausfallarbeit_kwh: Decimal,
90 anzulegender_wert_ct: Decimal,
91) -> Decimal {
92 (ausfallarbeit_kwh * anzulegender_wert_ct / Decimal::ONE_HUNDRED)
93 .round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
94}
95
96pub fn redispatch_verguetung(
103 input: &RedispatchVerguetungInput,
104) -> Result<RedispatchVerguetung, BillingError> {
105 for (label, v) in [
106 ("ausfallarbeit_kwh", input.ausfallarbeit_kwh),
107 ("entgangene_einnahmen_eur", input.entgangene_einnahmen_eur),
108 (
109 "zusaetzliche_aufwendungen_eur",
110 input.zusaetzliche_aufwendungen_eur,
111 ),
112 ("ersparte_aufwendungen_eur", input.ersparte_aufwendungen_eur),
113 ] {
114 if v < Decimal::ZERO {
115 return Err(BillingError::InvalidInput {
116 reason: format!("§13a component {label} must be non-negative, got {v}"),
117 });
118 }
119 }
120
121 let round = |d: Decimal| {
122 d.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
123 };
124 let entgangene = round(input.entgangene_einnahmen_eur);
125 let zusaetzliche = round(input.zusaetzliche_aufwendungen_eur);
126 let ersparte = round(input.ersparte_aufwendungen_eur);
127 let total = entgangene + zusaetzliche - ersparte;
128
129 for v in [entgangene, zusaetzliche, ersparte, total] {
132 let _representable =
133 EuroAmount::checked_from_decimal(v).map_err(|_| BillingError::MonetaryOverflow {
134 input_value: Some(v),
135 })?;
136 }
137
138 let basis = match input.verguetungsart {
139 RedispatchVerguetungsart::Eeg => "entgangene EEG-Vergütung (§13a Abs. 2 S. 3 Nr. 5 EnWG)",
140 RedispatchVerguetungsart::Kwkg => "entgangene KWKG-Vergütung (§13a Abs. 2 S. 3 Nr. 5 EnWG)",
141 RedispatchVerguetungsart::Sonstige => {
142 "nachgewiesene entgangene Erlöse (§13a Abs. 2 S. 3 Nr. 3 EnWG)"
143 }
144 };
145
146 Ok(RedispatchVerguetung {
147 ausfallarbeit_kwh: input.ausfallarbeit_kwh,
148 verguetungsart: input.verguetungsart,
149 entgangene_einnahmen_eur: entgangene,
150 zusaetzliche_aufwendungen_eur: zusaetzliche,
151 ersparte_aufwendungen_eur: ersparte,
152 verguetung_eur: total,
153 trace: vec![
154 format!("Ausfallarbeit: {} kWh", input.ausfallarbeit_kwh),
155 format!("+ {entgangene} € {basis}"),
156 format!("+ {zusaetzliche} € zusätzliche Aufwendungen (Nr. 1/2/4)"),
157 format!("− {ersparte} € ersparte Aufwendungen (S. 4 — an den NB zu erstatten)"),
158 format!("= {total} € angemessene Vergütung (§13a Abs. 2 EnWG)"),
159 ],
160 })
161}
162
163pub fn bilarem_finanzielle_korrektur(
180 ausfallarbeit_kwh: Decimal,
181 ausgleich_kwh: Decimal,
182 id_aep_eur_per_mwh: Decimal,
183) -> Result<Decimal, BillingError> {
184 let korr = (ausfallarbeit_kwh - ausgleich_kwh) / Decimal::from(1000) * id_aep_eur_per_mwh;
185 let rounded =
186 korr.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero);
187 if rounded.abs() > Decimal::from(10_000_000) {
189 return Err(BillingError::InvalidInput {
190 reason: format!("BilAReM Korr_fin out of range: {rounded}"),
191 });
192 }
193 Ok(rounded)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use rust_decimal::dec;
200
201 #[test]
202 fn eeg_plant_compensation_from_the_anzulegender_wert() {
203 let entgangene = eeg_entgangene_einnahmen(dec!(12_500), dec!(7.30));
205 assert_eq!(entgangene, dec!(912.50));
206
207 let v = redispatch_verguetung(&RedispatchVerguetungInput {
208 ausfallarbeit_kwh: dec!(12_500),
209 verguetungsart: RedispatchVerguetungsart::Eeg,
210 entgangene_einnahmen_eur: entgangene,
211 zusaetzliche_aufwendungen_eur: dec!(40),
212 ersparte_aufwendungen_eur: dec!(12.50),
213 })
214 .unwrap();
215 assert_eq!(v.verguetung_eur, dec!(940.00));
216 assert!(v.trace.iter().any(|l| l.contains("Nr. 5")));
217 }
218
219 #[test]
220 fn bilarem_korrektur_settles_the_residual_financially() {
221 let k = bilarem_finanzielle_korrektur(dec!(1200), dec!(1000), dec!(80)).unwrap();
224 assert_eq!(k, dec!(16.00));
225 let k = bilarem_finanzielle_korrektur(dec!(800), dec!(1000), dec!(80)).unwrap();
227 assert_eq!(k, dec!(-16.00));
228 let k = bilarem_finanzielle_korrektur(dec!(1200), dec!(1000), dec!(-50)).unwrap();
230 assert_eq!(k, dec!(-10.00));
231 }
232
233 #[test]
234 fn saved_costs_can_exceed_the_claim() {
235 let v = redispatch_verguetung(&RedispatchVerguetungInput {
238 ausfallarbeit_kwh: dec!(50_000),
239 verguetungsart: RedispatchVerguetungsart::Sonstige,
240 entgangene_einnahmen_eur: dec!(2_000),
241 zusaetzliche_aufwendungen_eur: dec!(100),
242 ersparte_aufwendungen_eur: dec!(2_500),
243 })
244 .unwrap();
245 assert_eq!(v.verguetung_eur, dec!(-400.00));
246 }
247
248 #[test]
249 fn negative_components_are_rejected() {
250 let err = redispatch_verguetung(&RedispatchVerguetungInput {
251 ausfallarbeit_kwh: dec!(100),
252 verguetungsart: RedispatchVerguetungsart::Kwkg,
253 entgangene_einnahmen_eur: dec!(-1),
254 zusaetzliche_aufwendungen_eur: Decimal::ZERO,
255 ersparte_aufwendungen_eur: Decimal::ZERO,
256 });
257 assert!(err.is_err());
258 }
259}