eeg_billing/model.rs
1//! Settlement model types — the input/output contract for [`calculate_settlement`].
2//!
3//! [`calculate_settlement`]: crate::calculate_settlement
4
5use crate::scheme::{SettlementScheme, SettlementType, TariffSource};
6use crate::technology::ErzeugungsArt;
7use crate::version::EegGesetz;
8use rust_decimal::Decimal;
9use time::Date;
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14// ── Enums ─────────────────────────────────────────────────────────────────────
15
16// ── SanktionsTyp / Pflichtverstoss ────────────────────────────────────────────
17
18/// §52 EEG ≤2021 (old regime) — sanction tier reducing the Vergütung.
19///
20/// Three distinct tiers based on §52 EEG 2021/2017 (via §100 Übergangsregelung).
21/// For EEG 2023 plants, use [`Pflichtverstoss`] instead (separate €10/kW/month penalty).
22///
23/// ## Legal basis: §52 EEG 2021
24///
25/// ```text
26/// Abs. 1: verringert sich auf null → VerguetungAufNull
27/// Abs. 2: verringert sich auf den Marktwert → VerguetungAufMarktwert
28/// Abs. 3: verringert sich um 20 Prozent → VerguetungReduziert20Prozent
29/// ```
30///
31/// ## §52 Abs. 3 rounding (EEG 2021)
32/// "wobei das Ergebnis auf zwei Stellen nach dem Komma gerundet wird"
33/// The 20% reduction result is rounded to 2 decimal places.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
38pub enum SanktionAlt {
39 /// §52 Abs. 1 EEG ≤2021: Vergütung verringert sich auf **null**.
40 ///
41 /// Applies to:
42 /// - Nr. 1: MaStR not registered AND §71 Nr. 1 not done
43 /// - Nr. 2: Capacity increase not reported AND §71 Nr. 1 not done
44 /// - Nr. 2a: §10b Direktvermarktungspflicht violation
45 /// - Nr. 3: §21b Abs. 2/3 violation (wrongful form change, 3 months)
46 /// - Nr. 4: §27a violation for Ausschreibungsanlagen (full calendar year)
47 VerguetungAufNull,
48 /// §52 Abs. 2 EEG ≤2021: Vergütung verringert sich auf den **Monatsmarktwert**
49 /// (= EPEX monthly average ct/kWh, same price as `PostEegSpot`).
50 ///
51 /// Applies to:
52 /// - Nr. 1: §9 Abs. 1/1a/2/5 violation (Fernsteuerbarkeit not installed)
53 /// - Nr. 1a: §9 Abs. 8 violation (Messeinrichtung not installed)
54 /// - Nr. 2: §21b/§21c notification not sent
55 /// - Nr. 3: Ausfallvergütung Höchstdauer exceeded
56 /// - Nr. 4: §21 Abs. 2 Einspeisevergütung violation
57 /// - Nr. 5: §80 Doppelvermarktungsverbot violation
58 ///
59 /// Requires `epex_avg_ct_kwh` in `SettleInput`. Returns `PriceMissing` if absent.
60 VerguetungAufMarktwert,
61 /// §52 Abs. 3 EEG ≤2021: Vergütung verringert sich um **20 Prozent**
62 /// (result rounded to 2 decimal places per §52 Abs. 3).
63 ///
64 /// Applies to:
65 /// - Nr. 1: §71 Nr. 1 was done but MaStR registration data is incomplete
66 /// - Nr. 2: Capacity increase not reported, but §71 Nr. 1 was done
67 VerguetungReduziert20Prozent,
68}
69
70/// §52 EEG 2023 compliance violation type.
71///
72/// Each type triggers a payment obligation to the NB of €10/kW/month (§52 Abs. 2).
73/// The obligation can be retroactively reduced to €2/kW/month once fulfilled (§52 Abs. 3).
74///
75/// Use [`crate::foerderdauer::calculate_pflichtzahlung`] to compute the penalty.
76///
77/// ## EEG version note
78///
79/// §52 EEG 2023 applies to plants under current EEG 2023 rules.
80/// For old plants (commissioned before 01.01.2023) under §100 Übergangsregelung,
81/// the old §47 EEG 2021 "Vergütung = 0" rule applies instead — use `sanktion: Some(SanktionAlt::VerguetungAufNull)`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[non_exhaustive]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
86pub enum SanktionsTyp {
87 /// §52 Abs. 1 Nr. 1 — Missing Fernsteuerbarkeit (§9 Abs. 1/2).
88 ///
89 /// Plant ≥25 kW does not have remote control capability installed.
90 /// Obligation fulfilled → reduced to €2/kW/month retroactively.
91 /// Technical defect grace: 2 months waived.
92 FernsteuerbarkeitFehlend,
93
94 /// §52 Abs. 1 Nr. 2 — Missing Speicher / §9 Abs. 5 violation.
95 ///
96 /// Plant does not meet the storage requirement for certain EE/KWK plants.
97 ///
98 /// **Rate: €10/kW/month** (§52 Abs. 2). Not in the §52 Abs. 3 Nr. 1 reduction list
99 /// — this violation is NOT retroactively reducible to €2/kW.
100 SpeicherAnforderungNichtErfuellt,
101
102 /// §52 Abs. 1 Nr. 3 — Missing iMSys Messeinrichtung (§9 Abs. 8).
103 ///
104 /// Plant not equipped with the required intelligent measurement system infrastructure.
105 IMssAnforderungNichtErfuellt,
106
107 /// §52 Abs. 1 Nr. 4 — Verstoß gegen §10b (Vorgaben zur Direktvermarktung).
108 ///
109 /// §10b Abs. 1 obliges the operator of a plant **over 25 kW that direct-markets**
110 /// to fit the technical equipment through which the Direktvermarktungsunternehmen
111 /// can call up the Ist-Einspeisung and remotely curtail it, and to grant it that
112 /// authority. It is **not** the duty to direct-market: a plant over 100 kW that
113 /// stays on the Einspeisevergütung breaches nothing here — it simply has no
114 /// §21 Abs. 1 Satz 1 Nr. 1 claim, which is
115 /// [`SettlementStatus::KeinAnspruch`], not a Zahlung.
116 ///
117 /// **Rate: €10/kW/month**, reduced to €2 retroactively on cure (§52 Abs. 3 Satz 1
118 /// Nr. 1) and waived for two months on a technical defect (Satz 2).
119 Sect10bVorgabenVerletzt,
120
121 /// §52 Abs. 1 Nr. 11 — Plant not registered in MaStR.
122 ///
123 /// Required registration data not submitted per Marktstammdatenregisterverordnung.
124 /// Obligation fulfilled → reduced to €2/kW/month retroactively.
125 ///
126 /// **EEG 2023 change**: Old §47 EEG 2021 reduced Vergütung to EUR 0.
127 /// §52 EEG 2023 instead charges €10/kW/month; Vergütung remains payable.
128 /// Use `sanktion: Some(SanktionAlt::VerguetungAufNull)` for old plants (EEG ≤2021, §100 Übergangsregelung).
129 MastrNichtRegistriert,
130
131 /// §52 Abs. 1 Nr. 9a — Post-commissioning violation of §37 Abs. 1a or §48 Abs. 6.
132 ///
133 /// Plant violates the obligations that arise after commissioning under those paragraphs
134 /// (§37 Abs. 1a: iMSys Nachrüstung after commissioning; §48 Abs. 6: solar Segment obligations).
135 ///
136 /// **Rate: always €2/kW/month** (§52 Abs. 3 Nr. 2 EEG 2023).
137 /// This is a permanently lower rate — NOT reduced from €10; starts at €2 for this type.
138 /// `nachtraeglich_erfuellt` has NO effect on this type.
139 InbetriebnahmeVorgabeVerletzt,
140
141 /// §52 Abs. 1 Nr. 10 — Volleinspeisung obligation violated (§48 Abs. 2a).
142 ///
143 /// Plant registered for Volleinspeisung (100% grid feed-in bonus, §48 Abs. 2a EEG 2023)
144 /// but does not feed all generated electricity into the grid in a calendar year.
145 ///
146 /// **Rate: always €2/kW/month** (§52 Abs. 3 Nr. 2 EEG 2023).
147 /// `nachtraeglich_erfuellt` has NO effect on this type.
148 ///
149 /// ## §52 Abs. 4 Nr. 3: calendar-year scope
150 ///
151 /// This violation is assessed for **all calendar months of the year** in which
152 /// the under-delivery occurs (not just the months of non-delivery).
153 /// Include all 12 months in `monate_des_verstosses`.
154 VolleinspeisungspflichtVerletzt,
155
156 // ── §52 Abs. 1 Nr. 5–12 — additional violations ──────────────────────────────
157 /// §52 Abs. 1 Nr. 5 — Ausfallvergütung Höchstdauer exceeded
158 /// (§21 Abs. 1 Satz 1 Nr. 3).
159 ///
160 /// Plant in Ausfallvergütung exceeds the statutory 3-month maximum.
161 ///
162 /// The §10/kW Pflichtzahlung is owed for the months of the violation only —
163 /// §52 Abs. 4 grants *additional* months solely to Nr. 7 (+3), Nr. 9 (+1),
164 /// Nr. 10 (full calendar year) and Nr. 12 (+6). Nr. 5 is **not** listed there,
165 /// so no extra months are added (adding +3 here over-charged the operator).
166 AusfallverguetungHoechstdauerUeberschritten,
167
168 /// §52 Abs. 1 Nr. 6 — Unzulässige Inanspruchnahme von Einspeisevergütung (§21 Abs. 2).
169 ///
170 /// Plant claims Einspeisevergütung while violating the conditions of §21 Abs. 2
171 /// (e.g., plant participates in Regelenergiemarkt while on Einspeisevergütung).
172 EinspeiseverguetungUnzulaessigeNutzung,
173
174 /// §52 Abs. 1 Nr. 7 — Unzulässiger Veräußerungsform-Wechsel (§21b Abs. 2 Satz 1 zweiter Halbsatz).
175 ///
176 /// Operator performs an impermissible switch of Veräußerungsform (e.g., switching
177 /// when mandatory Direktvermarktung applies and return to Einspeisevergütung is blocked).
178 ///
179 /// ## §52 Abs. 4 Nr. 1: +3 extra months
180 ///
181 /// Payment is also owed for the **3 calendar months following** the violation period.
182 /// Callers should add these 3 months to `monate_des_verstosses`.
183 VeraeusserungsformWechselUngueltig,
184
185 /// §52 Abs. 1 Nr. 8 — Pflichtnachweis-Verletzung (§21b Abs. 3).
186 ///
187 /// Operator fails to provide required evidence/documentation after a Veräußerungsform
188 /// switch (§21b Abs. 3 documentation obligations).
189 ///
190 /// ## §52 Abs. 3 Satz 2: Technical defect grace
191 ///
192 /// When `technischer_defekt = true` and violation occurred after 31 Dec 2023:
193 /// payment waived for the violation month and the following month.
194 VeraeusserungsformNachweispflichtVerletzt,
195
196 /// §52 Abs. 1 Nr. 9 — Zuordnungs-/Wechselmeldung nicht übermittelt (§21c).
197 ///
198 /// Operator did not notify the NB of a Veräußerungsform assignment or switch
199 /// within the deadline per §21c EEG 2023.
200 ///
201 /// ## §52 Abs. 4 Nr. 2: +1 extra month
202 ///
203 /// Payment is also owed for the **1 calendar month following** the violation period.
204 /// Callers should add this 1 month to `monate_des_verstosses`.
205 ZuordnungsWechselNichtGemeldet,
206
207 /// §52 Abs. 1 Nr. 12 — Doppelvermarktungsverbot verletzt (§80 EEG 2023).
208 ///
209 /// Strom was claimed for EEG payment AND simultaneously used in another subsidised
210 /// scheme (e.g., EEG + EEG, or EEG + KWKG, or EEG + HKN). §80 prohibits double-counting.
211 ///
212 /// ## §52 Abs. 4 Nr. 4: +6 extra months
213 ///
214 /// Payment is also owed for the **6 calendar months following** the violation period.
215 /// Callers should add these 6 months to `monate_des_verstosses`.
216 DoppelvermarktungsverbotVerletzt,
217}
218
219impl SanktionsTyp {
220 /// Every variant, in the order §52 Abs. 1 lists them.
221 ///
222 /// Thirteen, not twelve: Abs. 1 counts to 12 but inserts **Nr. 9a** between
223 /// 9 and 10. The list is what a persistence layer's `CHECK` constraint and a
224 /// REST surface's vocabulary are held against, so a new Pflichtverstoß
225 /// cannot be added in one place and forgotten in the other.
226 pub const ALL: [Self; 13] = [
227 Self::FernsteuerbarkeitFehlend,
228 Self::SpeicherAnforderungNichtErfuellt,
229 Self::IMssAnforderungNichtErfuellt,
230 Self::Sect10bVorgabenVerletzt,
231 Self::AusfallverguetungHoechstdauerUeberschritten,
232 Self::EinspeiseverguetungUnzulaessigeNutzung,
233 Self::VeraeusserungsformWechselUngueltig,
234 Self::VeraeusserungsformNachweispflichtVerletzt,
235 Self::ZuordnungsWechselNichtGemeldet,
236 Self::InbetriebnahmeVorgabeVerletzt,
237 Self::VolleinspeisungspflichtVerletzt,
238 Self::MastrNichtRegistriert,
239 Self::DoppelvermarktungsverbotVerletzt,
240 ];
241
242 /// Which Nummer of §52 Abs. 1 this is, as the statute writes it — `"9a"`
243 /// is a Nummer of its own, which is why this is a string and not a number.
244 #[must_use]
245 pub fn nummer(self) -> &'static str {
246 match self {
247 Self::FernsteuerbarkeitFehlend => "1",
248 Self::SpeicherAnforderungNichtErfuellt => "2",
249 Self::IMssAnforderungNichtErfuellt => "3",
250 Self::Sect10bVorgabenVerletzt => "4",
251 Self::AusfallverguetungHoechstdauerUeberschritten => "5",
252 Self::EinspeiseverguetungUnzulaessigeNutzung => "6",
253 Self::VeraeusserungsformWechselUngueltig => "7",
254 Self::VeraeusserungsformNachweispflichtVerletzt => "8",
255 Self::ZuordnungsWechselNichtGemeldet => "9",
256 Self::InbetriebnahmeVorgabeVerletzt => "9a",
257 Self::VolleinspeisungspflichtVerletzt => "10",
258 Self::MastrNichtRegistriert => "11",
259 Self::DoppelvermarktungsverbotVerletzt => "12",
260 }
261 }
262
263 /// The stored/wire token — the `SCREAMING_SNAKE_CASE` serde name.
264 #[must_use]
265 pub fn as_db_str(self) -> &'static str {
266 match self {
267 Self::FernsteuerbarkeitFehlend => "FERNSTEUERBARKEIT_FEHLEND",
268 Self::SpeicherAnforderungNichtErfuellt => "SPEICHER_ANFORDERUNG_NICHT_ERFUELLT",
269 Self::IMssAnforderungNichtErfuellt => "I_MSS_ANFORDERUNG_NICHT_ERFUELLT",
270 Self::Sect10bVorgabenVerletzt => "SECT10B_VORGABEN_VERLETZT",
271 Self::AusfallverguetungHoechstdauerUeberschritten => {
272 "AUSFALLVERGUETUNG_HOECHSTDAUER_UEBERSCHRITTEN"
273 }
274 Self::EinspeiseverguetungUnzulaessigeNutzung => {
275 "EINSPEISEVERGUETUNG_UNZULAESSIGE_NUTZUNG"
276 }
277 Self::VeraeusserungsformWechselUngueltig => "VERAEUSSERUNGSFORM_WECHSEL_UNGUELTIG",
278 Self::VeraeusserungsformNachweispflichtVerletzt => {
279 "VERAEUSSERUNGSFORM_NACHWEISPFLICHT_VERLETZT"
280 }
281 Self::ZuordnungsWechselNichtGemeldet => "ZUORDNUNGS_WECHSEL_NICHT_GEMELDET",
282 Self::InbetriebnahmeVorgabeVerletzt => "INBETRIEBNAHME_VORGABE_VERLETZT",
283 Self::VolleinspeisungspflichtVerletzt => "VOLLEINSPEISUNGSPFLICHT_VERLETZT",
284 Self::MastrNichtRegistriert => "MASTR_NICHT_REGISTRIERT",
285 Self::DoppelvermarktungsverbotVerletzt => "DOPPELVERMARKTUNGSVERBOT_VERLETZT",
286 }
287 }
288
289 /// Parse a stored/wire token back. `None` for anything not in [`Self::ALL`].
290 #[must_use]
291 pub fn from_db_str(s: &str) -> Option<Self> {
292 Self::ALL.into_iter().find(|t| t.as_db_str() == s)
293 }
294
295 /// §52 **Abs. 4** — how many calendar months the Zahlung is owed for, given
296 /// the months the breach itself lasted.
297 ///
298 /// Four Nummern run past the breach: Nr. 7 „zusätzlich für die folgenden
299 /// drei Kalendermonate", Nr. 9 „zusätzlich für den folgenden
300 /// Kalendermonat", Nr. 12 „zusätzlich für die folgenden sechs
301 /// Kalendermonate" — and Nr. 10 is not an extension at all but a
302 /// replacement: „für **alle** Kalendermonate des Kalenderjahres".
303 ///
304 /// Nr. 5 is deliberately absent. Abs. 4 does not name it, and adding three
305 /// months there would charge a plant on the Ausfallvergütung a quarter it
306 /// does not owe.
307 ///
308 /// # Example
309 ///
310 /// ```rust
311 /// use eeg_billing::SanktionsTyp;
312 ///
313 /// assert_eq!(SanktionsTyp::ZuordnungsWechselNichtGemeldet.abs4_monate(2), 3);
314 /// assert_eq!(SanktionsTyp::VolleinspeisungspflichtVerletzt.abs4_monate(2), 12);
315 /// assert_eq!(SanktionsTyp::AusfallverguetungHoechstdauerUeberschritten.abs4_monate(2), 2);
316 /// ```
317 #[must_use]
318 pub fn abs4_monate(self, monate_des_verstosses: u32) -> u32 {
319 match self {
320 Self::VeraeusserungsformWechselUngueltig => monate_des_verstosses.saturating_add(3),
321 Self::ZuordnungsWechselNichtGemeldet => monate_des_verstosses.saturating_add(1),
322 Self::DoppelvermarktungsverbotVerletzt => monate_des_verstosses.saturating_add(6),
323 // Abs. 4 Nr. 3 — the whole calendar year, not the breach plus a tail.
324 Self::VolleinspeisungspflichtVerletzt => 12,
325 _ => monate_des_verstosses,
326 }
327 }
328}
329
330#[cfg(test)]
331mod sanktionstyp_tests {
332 use super::SanktionsTyp;
333
334 /// §52 Abs. 1 counts to twelve but inserts Nr. 9a, so there are thirteen —
335 /// and `ALL` has to hold every one of them, because a persistence `CHECK`
336 /// and a REST vocabulary are generated from it.
337 #[test]
338 fn all_holds_every_variant_exactly_once() {
339 let mut nummern: Vec<&str> = SanktionsTyp::ALL.iter().map(|t| t.nummer()).collect();
340 nummern.sort_unstable();
341 nummern.dedup();
342 assert_eq!(nummern.len(), SanktionsTyp::ALL.len());
343 let mut tokens: Vec<&str> = SanktionsTyp::ALL.iter().map(|t| t.as_db_str()).collect();
344 tokens.sort_unstable();
345 tokens.dedup();
346 assert_eq!(tokens.len(), SanktionsTyp::ALL.len());
347 for t in SanktionsTyp::ALL {
348 assert_eq!(SanktionsTyp::from_db_str(t.as_db_str()), Some(t));
349 }
350 assert_eq!(SanktionsTyp::from_db_str("NOT_A_VIOLATION"), None);
351 }
352
353 /// §52 Abs. 4 names exactly four Nummern, and Nr. 5 is not one of them.
354 #[test]
355 fn abs4_extends_only_the_four_nummern_it_names() {
356 let extended: Vec<&str> = SanktionsTyp::ALL
357 .iter()
358 .filter(|t| t.abs4_monate(2) != 2)
359 .map(|t| t.nummer())
360 .collect();
361 assert_eq!(extended, ["7", "9", "10", "12"]);
362 assert_eq!(
363 SanktionsTyp::VeraeusserungsformWechselUngueltig.abs4_monate(2),
364 5
365 );
366 assert_eq!(
367 SanktionsTyp::ZuordnungsWechselNichtGemeldet.abs4_monate(2),
368 3
369 );
370 assert_eq!(
371 SanktionsTyp::DoppelvermarktungsverbotVerletzt.abs4_monate(2),
372 8
373 );
374 // Nr. 10 is a replacement, not an addition: „für alle Kalendermonate
375 // des Kalenderjahres" — twelve however long the under-delivery ran.
376 assert_eq!(
377 SanktionsTyp::VolleinspeisungspflichtVerletzt.abs4_monate(2),
378 12
379 );
380 assert_eq!(
381 SanktionsTyp::VolleinspeisungspflichtVerletzt.abs4_monate(11),
382 12
383 );
384 }
385}
386
387/// §52 EEG 2023 — Pflichtverstoss input for penalty calculation.
388///
389/// A compliance violation that triggers a payment obligation of €10/kW/month
390/// from the plant operator to the NB (§52 Abs. 2 EEG 2023).
391///
392/// ## Penalty calculation
393///
394/// ```rust
395/// use eeg_billing::Pflichtverstoss;
396/// use eeg_billing::SanktionsTyp;
397/// use eeg_billing::foerderdauer::calculate_pflichtzahlung;
398/// use rust_decimal::dec;
399///
400/// // Missing Fernsteuerbarkeit for 3 months, 500 kW plant, obligation not yet fulfilled
401/// let violation = Pflichtverstoss {
402/// typ: SanktionsTyp::FernsteuerbarkeitFehlend,
403/// leistung_kw: dec!(500),
404/// monate_des_verstosses: 3,
405/// beginn: None,
406/// nachtraeglich_erfuellt: false,
407/// technischer_defekt: false,
408/// };
409/// let penalty = calculate_pflichtzahlung(&violation);
410/// assert_eq!(penalty, dec!(15000)); // 500 kW × 10 EUR × 3 months
411/// ```
412#[derive(Debug, Clone)]
413#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
414pub struct Pflichtverstoss {
415 /// Type of compliance violation.
416 pub typ: SanktionsTyp,
417 /// Installed capacity of the plant in kW (basis for €10/kW/month).
418 pub leistung_kw: Decimal,
419 /// Number of calendar months during which the violation is/was in effect.
420 ///
421 /// **Already including the §52 Abs. 4 months** — apply
422 /// [`SanktionsTyp::abs4_monate`] to the raw duration rather than doing it by
423 /// hand: Nr. 7 +3, Nr. 9 +1, Nr. 12 +6, and Nr. 10 the full calendar year
424 /// (12), which is a replacement rather than an addition. Abs. 4 does **not**
425 /// name Nr. 5.
426 pub monate_des_verstosses: u32,
427 /// The first calendar month the violation runs in.
428 ///
429 /// §52 Abs. 5 caps concurrent violations „pro Kilowatt installierter
430 /// Leistung der Anlage **und Kalendermonat**", so the cap needs to know
431 /// which months each violation occupies, not only how many. Any day in the
432 /// month will do — only the year and month are read.
433 ///
434 /// `None` leaves a violation unplaced, and the Abs. 5 cap then treats it as
435 /// running concurrently with everything else, which is the reading that
436 /// cannot overcharge the operator.
437 pub beginn: Option<time::Date>,
438 /// Whether the obligation has since been fulfilled.
439 ///
440 /// When `true`, §52 Abs. 3 reduces the penalty retroactively to €2/kW/month
441 /// for violation types Nr. 1, 3, 4, 11. Has no effect for Nr. 2, 9a, 10.
442 pub nachtraeglich_erfuellt: bool,
443 /// Whether the violation was caused by a **technical defect** of plant equipment.
444 ///
445 /// Per §52 Abs. 3 Satz 2 EEG 2023 (in force from 01.01.2024):
446 /// For violations of Nr. 1 (Fernsteuerbarkeit), Nr. 3 (iMSys), Nr. 4 (§10b), Nr. 8 (§21b Abs. 3)
447 /// caused by a technical defect, the penalty is **waived for the defect month and
448 /// the following calendar month**.
449 ///
450 /// - Only applies to violations occurring **after 31 December 2023**.
451 /// - The operator bears the burden of proof for the defect (Darlegungs- und Beweislast).
452 /// - Does **not** apply to Nr. 2, 5, 6, 7, 9, 10, 11, 12.
453 ///
454 /// When `true`: effective months = `max(0, monate_des_verstosses - 2)` for eligible types.
455 pub technischer_defekt: bool,
456}
457
458// ── CapacityBlock ─────────────────────────────────────────────────────────────
459
460/// A single capacity block for §24 EEG Anlagenerweiterung (plant extension).
461///
462/// When an existing EEG plant is extended with additional capacity
463/// (e.g. adding 5 kWp to an existing 10 kWp installation), the extension
464/// receives its own:
465/// - Feed-in tariff rate (the statutory rate at the **extension** date, which
466/// is typically lower due to annual degression)
467/// - 20-year Förderdauer starting from the extension commissioning date
468///
469/// The settlement engine allocates the measured Einspeisemenge proportionally
470/// across all blocks by installed capacity (§24 Abs. 1 EEG 2023).
471///
472/// ## Zusammenlegung vs. Erweiterung
473///
474/// - **Zusammenlegung** (§24 EEG): two legally separate plants merged into one
475/// entity. Both plants contribute their original rates and end dates.
476/// Model via two `CapacityBlock`s.
477///
478/// - **Erweiterung**: capacity added to an existing plant at a later date.
479/// New capacity block gets current statutory rate from extension date.
480/// Model via one primary block (in `SettleInput`) + one `CapacityBlock`.
481///
482/// ## Example
483///
484/// ```rust
485/// use eeg_billing::CapacityBlock;
486/// use rust_decimal::dec;
487/// use time::macros::date;
488///
489/// // Original 10 kWp at 9.25 ct/kWh (EEG 2020)
490/// let original = CapacityBlock {
491/// leistung_kwp: dec!(10),
492/// verguetungssatz_ct: dec!(9.25),
493/// inbetriebnahme: date!(2020-03-15),
494/// foerderendedatum: date!(2040-03-15),
495/// };
496///
497/// // Extension: +5 kWp at 8.11 ct/kWh (EEG 2023)
498/// let extension = CapacityBlock {
499/// leistung_kwp: dec!(5),
500/// verguetungssatz_ct: dec!(8.11),
501/// inbetriebnahme: date!(2024-06-01),
502/// foerderendedatum: date!(2044-06-01),
503/// };
504/// ```
505#[derive(Debug, Clone)]
506#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
507pub struct CapacityBlock {
508 /// Installed capacity for this block in kWp (or kW_el for KWKG).
509 pub leistung_kwp: Decimal,
510 /// EEG feed-in tariff rate for this block in ct/kWh.
511 ///
512 /// Fixed at the commissioning date of **this block** for its full Förderdauer.
513 pub verguetungssatz_ct: Decimal,
514 /// Commissioning date for this block (Inbetriebnahmedatum).
515 pub inbetriebnahme: Date,
516 /// Subsidy end date for this block (`inbetriebnahme + 20 years`).
517 ///
518 /// When the billing period start date exceeds this, the block is expired
519 /// and contributes EUR 0 (or EPEX spot price for `PostEegSpot` transition).
520 pub foerderendedatum: Date,
521}
522
523// ── SettleInput ───────────────────────────────────────────────────────────────
524
525/// Input for a single settlement period calculation.
526///
527/// All monetary rates are in **ct/kWh** (Cent per kWh), not EUR/kWh.
528/// Supply `Default::default()` for fields not applicable to the model.
529///
530/// ## Multi-EEG-version support
531///
532/// EEG has been revised many times (2000, 2004, 2009, 2012, 2014, 2017, 2021, 2023).
533/// The correct `verguetungssatz_ct` is fixed at the plant's commissioning date and
534/// does not change over the 20-year Förderdauer. Supply the rate that was valid
535/// when the plant was commissioned — use `eeg_billing::rates` or `einsd`'s
536/// rate lookup table for historical rates.
537///
538/// The formula logic (which model is applicable, whether §27 applies, etc.)
539/// differs by EEG version and commissioning date. Supply `inbetriebnahme` so
540/// the engine can apply the correct version-specific guards automatically.
541#[derive(Debug, Clone, Default)]
542#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
543pub struct SettleInput {
544 // ── Settlement scheme — HOW remuneration is determined ────────────────
545 /// Which regulatory formula to apply.
546 ///
547 /// - `FeedInTariff` → §21 EEG Einspeisevergütung
548 /// - `MarketPremium` → §20 EEG Gleitende Marktprämie (incl. Ausschreibung via `tariff_source`)
549 /// - `TenantElectricity` → §21 Abs. 3 Mieterstrom
550 /// - `PostEeg` → post-Förderung spot (configurable `post_eeg_price_floor`)
551 /// - `KwkSurcharge` → §7 KWKG
552 /// - `TemporaryFeedInTariff` → §21 Abs. 1 Satz 1 Nr. 3 Ausfallvergütung
553 /// - `Eigenverbrauch` → no payment
554 /// - `FlexibilityPremium` → §50b bestehende Biomasseanlagen
555 /// - `FlexibilitySurcharge` → §50a neue Biomasseanlagen (capacity payment)
556 pub scheme: SettlementScheme,
557
558 /// Where the Anzulegender Wert (AW) comes from.
559 ///
560 /// - `Statutory` → §48 EEG statutory tables (default)
561 /// - `Auction(meta)` → BNetzA tender award (Ausschreibung: same formula as MarketPremium)
562 /// - `Transitional(rule)` → §100 EEG Übergangsregelung
563 pub tariff_source: TariffSource,
564
565 /// Settlement type: initial, correction, or reversal.
566 pub settlement_type: SettlementType,
567
568 /// Einspeisemenge kWh for the billing period.
569 /// `None` → output status = [`SettlementStatus::NoData`].
570 pub einspeisemenge_kwh: Option<Decimal>,
571
572 /// Monthly EPEX Spot Day-Ahead average **or** technology-specific Jahresmarktwert
573 /// (§20 Abs. 2 + Anlage 1 EEG 2023) in ct/kWh.
574 ///
575 /// This is the **market reference price** used for all calculations that compare
576 /// EEG payment against the market:
577 /// - `MarketPremium`: spread = `max(0, eff_AW - marktwert_ct_kwh)`
578 /// - `PostEeg`: plant paid at `marktwert_ct_kwh` per kWh
579 /// - `SanktionAlt::VerguetungAufMarktwert`: old-regime sanction uses this as rate
580 /// - `§44b` biogas excess: excess kWh paid at `marktwert_ct_kwh`
581 ///
582 /// The library does not distinguish between EPEX monthly average and Jahresmarktwert —
583 /// the caller resolves which value applies and passes it here.
584 pub marktwert_ct_kwh: Option<Decimal>,
585
586 /// **§47 EEG (weggefallen/deleted in EEG 2023) / old EEG rules via §100 Übergangsregelung.**
587 ///
588 /// §52 EEG ≤2021 sanction tier (old regime via §100 Übergangsregelung).
589 ///
590 /// §52 EEG 2021/2017 has **three tiers** that each reduce the Vergütung differently.
591 /// These are for plants governed by EEG ≤2021 rules (commissioned before 01.01.2023).
592 ///
593 /// | `SanktionAlt` | §52 EEG ≤2021 | Vergütung effect |
594 /// |---|---|---|
595 /// | `VerguetungAufNull` | Abs. 1 Nr. 1: MaStR not registered | **EUR 0** |
596 /// | `VerguetungAufNull` | Abs. 1 Nr. 2a: §10b Direktvermarktungspflicht | **EUR 0** |
597 /// | `VerguetungAufNull` | Abs. 1 Nr. 4: §27a Eigenversorgung (Ausschreibung) | **EUR 0** |
598 /// | `VerguetungAufMarktwert` | Abs. 2 Nr. 1: §9 Abs. 1/2/5 Fernsteuerbarkeit | **→ EPEX Marktwert** |
599 /// | `VerguetungAufMarktwert` | Abs. 2 Nr. 1a: §9 Abs. 8 Messeinrichtung | **→ EPEX Marktwert** |
600 /// | `VerguetungReduziert20Prozent` | Abs. 3 Nr. 1: MaStR partial/late | **× 0.80** |
601 ///
602 /// For EEG 2023 plants, use `pflichtverstoss` instead — §52 EEG 2023 charges
603 /// €10/kW/month without suspending Vergütung.
604 ///
605 /// `None` = no sanction (normal settlement).
606 pub sanktion: Option<SanktionAlt>,
607
608 /// §52 EEG 2023 — Pflichtverstöße (compliance violations).
609 ///
610 /// §52 applies to plants governed by **EEG 2023 rules** (commissioned after 01.01.2023,
611 /// or old plants for violations introduced in EEG 2023).
612 ///
613 /// Each violation results in a **separate payment obligation from the plant operator
614 /// to the NB** of €10/kW/month (§52 Abs. 2 EEG 2023). This is NOT a reduction of
615 /// the Vergütung — the operator still receives the full Vergütung AND must separately
616 /// pay the §52 penalty to the NB (§52 Abs. 6: the NB may net these).
617 ///
618 /// ## Common violation types
619 ///
620 /// | `SanktionsTyp` | §52 Abs. 1 Nr. | Trigger |
621 /// |---|---|---|
622 /// | `FernsteuerbarkeitFehlend` | Nr. 1 | §9 Abs. 1/2: no remote-control equipment |
623 /// | `SpeicherAnforderungNichtErfuellt` | Nr. 2 | §9 Abs. 5: missing storage requirement |
624 /// | `MastrNichtRegistriert` | Nr. 11 | Plant not registered in MaStR |
625 ///
626 /// Use [`crate::foerderdauer::calculate_pflichtzahlung`] to compute the penalty amount.
627 ///
628 /// When `pflichtverstoss` is `Some`, the settlement formula still computes the
629 /// **full Vergütung** — the penalty is returned separately in the output's
630 /// `pflichtzahlung_eur` field.
631 ///
632 /// Default: empty Vec (no violations).
633 pub pflichtverstoss: Vec<Pflichtverstoss>,
634
635 /// §§53b–54 EEG 2023 — reductions that act on the anzulegender Wert.
636 ///
637 /// §53b Regionalnachweise, §53c Stromsteuerbefreiung and §54 solar
638 /// first-segment auction defects. Each reduces the AW *before* the
639 /// settlement formula, which matters for the gleitende Marktprämie: its
640 /// `max(0, …)` floor must absorb the reduction rather than the result being
641 /// pushed negative afterwards. See [`crate::aw_reductions`].
642 pub aw_reductions: crate::aw_reductions::AwReductionContext,
643
644 /// §51a EEG 2023 — quarter-hours during which §51 reduced Vergütung to zero.
645 ///
646 /// When provided, the engine computes `SettleOutput.verlaengerungsanspruch_qh`:
647 /// Solar PV: `ceil(qh / 2)` · Others: 1:1 factor.
648 pub negative_price_quarter_hours: Option<u64>,
649
650 /// §13a EnWG (Redispatch 2.0) — kWh curtailed by the NB (Einspeisemanagement compensation).
651 ///
652 /// §51 Negativpreisregel does NOT apply to these kWh (§19 Abs. 2 EEG 2023).
653 pub einspeisemanagement_kwh: Option<Decimal>,
654
655 /// §§ 39i, 42–44 EEG 2023 — Biomass/biogas fuel composition for settlement
656 /// enforcement.
657 ///
658 /// When set for biomass or biogas plants, the engine enforces:
659 ///
660 /// - **§ 39i Abs. 1** — a plant holding a Zuschlag whose Getreide- und
661 /// Mais-Anteil exceeds the Höchstanteil for its Gebotstermin has no § 19
662 /// Abs. 1 claim, and the period settles as `KeinAnspruch` at EUR 0. A plant
663 /// without a Zuschlag is outside Abs. 1 and is unaffected.
664 /// - **§ 44 Güllekleinanlage** eligibility is recorded in the position label
665 /// for audit transparency — it does **not** change the formula here; the
666 /// caller supplies the § 44 Abs. 1 `verguetungssatz_ct`
667 /// (use [`crate::rates::guelle_lookup`]).
668 ///
669 /// Use [`crate::biomasse::BiomassSettlementData::new`] to derive from fuel
670 /// composition data. `None` = plant is not biomass/biogas.
671 pub biomasse: Option<crate::biomasse::BiomassSettlementData>,
672
673 /// **§25 Abs. 1 Satz 3 EEG** — Fraction of the billing month with entitlement.
674 ///
675 /// When `None`, the library auto-computes from `billing_date`, `inbetriebnahme`,
676 /// and `foerderendedatum` via `foerderdauer::compute_billing_days_fraction()`.
677 /// When `Some(x)`, the provided value is used directly (override).
678 ///
679 /// Set explicitly only when the auto-computed value would be wrong for your
680 /// settlement scenario (rare edge cases). For standard plant lifecycles,
681 /// leave as `None` and ensure `billing_date`, `inbetriebnahme`, and
682 /// `foerderendedatum` are set.
683 pub billing_days_fraction: Option<Decimal>,
684
685 /// §51 EEG — kWh produced during negative EPEX hours (to be excluded).
686 ///
687 /// Under §51 EEG 2023, for plants **≥100 kWp commissioned after 01.01.2016**,
688 /// EEG Vergütung is zero during hours when the hourly EPEX Spot price is
689 /// negative AND the consecutive run of negative hours meets the version-specific
690 /// threshold (§51 EEG 2023: any period; §51 EEG 2017: ≥6h; §51 EEG 2021: ≥4h).
691 ///
692 /// When `inbetriebnahme` and `leistung_kwp` are both set, the engine
693 /// automatically guards this rule based on `eeg_gesetz`.
694 ///
695 /// **Does NOT apply to §51b biogas Ausschreibungsanlagen** — those plants
696 /// use a different rule (AW = 0 when EPEX ≤ 2 ct/kWh).
697 ///
698 /// Default: `None` (rule not applied).
699 pub kwh_during_negative_epex: Option<Decimal>,
700
701 // ── Commissioning & Förderdauer ──────────────────────────────────────────
702 /// Plant commissioning date (Inbetriebnahmedatum).
703 ///
704 /// When set, enables automatic EEG-version-aware rule enforcement:
705 /// - **§51 EEG Negativpreisregel**: threshold and kW exemption depend on EEG version
706 /// derived from commissioning year (see `eeg_gesetz`).
707 /// Key boundary: §100 Abs. 1 Satz 4 EEG 2017 exempts plants commissioned **before 01.01.2016**.
708 /// Plants from 2016-01-01 onwards are subject to §51 EEG 2017 (6h, 500 kW/3 MW).
709 /// - **Audit position labels**: include the commissioning year for traceability.
710 ///
711 /// For multi-block plants (§24 Anlagenerweiterung), the commissioning dates
712 /// live on each `CapacityBlock` instead.
713 pub inbetriebnahme: Option<Date>,
714
715 /// Type of commissioning event — for audit trail and Förderdauer rules.
716 ///
717 /// Determines whether the Förderdauer clock resets (only `Repowering` resets it)
718 /// and which lifecycle state the plant is in. Stored in `einsd`'s
719 /// `eeg_anlagen.inbetriebnahme_typ` column.
720 ///
721 /// | `InbetriebnahmeTyp` | Förderdauer | Audit relevance |
722 /// |---|---|---|
723 /// | `Erstinbetriebnahme` (default) | starts at `inbetriebnahme` | Normal plant |
724 /// | `Wiederinbetriebnahme` | continues from original | Restart after shutdown |
725 /// | `Modernisierung` | continues from original | Equipment replacement |
726 /// | `Repowering` | **resets** to repowering date | New 20-year clock |
727 /// | `Zusammenlegung` | oldest component date | §24 merger |
728 /// | `Erweiterung` | new block from extension date | §24 capacity add |
729 ///
730 /// The engine records this in position descriptions for full audit traceability.
731 /// Default: `InbetriebnahmeTyp::Erstinbetriebnahme`.
732 pub inbetriebnahme_typ: crate::technology::InbetriebnahmeTyp,
733
734 /// Installed peak power in kWp (or kW_el for KWKG).
735 ///
736 /// Used for:
737 /// - §27 EEG guard (threshold: 100 kWp)
738 /// - §51 Abs. 2 kW exemption (aggregated per §24 when `capacity_blocks` is set)
739 ///
740 /// Ignored when `capacity_blocks` is non-empty.
741 pub leistung_kwp: Option<Decimal>,
742
743 /// EEG subsidy end date from the plant registry.
744 ///
745 /// When set together with `billing_date`, the engine automatically returns
746 /// `FoerderungBeendet` when `billing_date > foerderendedatum`.
747 ///
748 /// For KWKG plants, this is the **calendar-year** fallback (§8 Abs. 4 KWKG):
749 /// Förderung ends at `min(kwk_hour_limit, inbetriebnahme + 15y)`.
750 pub foerderendedatum: Option<Date>,
751
752 /// First day of the billing period (ISO 8601 month-start, e.g. 2026-07-01).
753 ///
754 /// Used together with `foerderendedatum` for automatic `FoerderungBeendet`
755 /// detection. When omitted, the caller must check FoerderungBeendet manually.
756 pub billing_date: Option<Date>,
757
758 // ── §24 Anlagenerweiterung / Zusammenlegung ───────────────────────────────
759 /// Additional capacity blocks for §24 EEG Anlagenerweiterung / Zusammenlegung.
760 ///
761 /// When non-empty, the engine performs multi-block settlement:
762 /// 1. Each block receives a proportional share of `einspeisemenge_kwh`
763 /// (proportional to `leistung_kwp` of each block).
764 /// 2. The primary block uses `SettleInput.verguetungssatz_ct` and
765 /// `SettleInput.inbetriebnahme` / `foerderendedatum`.
766 /// 3. Blocks whose `foerderendedatum < billing_date` are expired (EUR 0).
767 /// 4. The §27 Negativpreisregel is applied per-block based on each block's
768 /// commissioning date and capacity.
769 ///
770 /// Leave empty for single-block plants (the vast majority).
771 pub capacity_blocks: Vec<CapacityBlock>,
772
773 /// EEG law year applicable to this plant (Gesetz-Jahr des anzuwendenden EEG).
774 ///
775 /// Determines which version-specific rules the engine applies:
776 ///
777 /// EEG law version governing this plant — the §52 Pflichtverstoß regime and
778 /// the §100 Übergangsbestimmungen.
779 ///
780 /// **Not** the source of the §51 rules: those are keyed on the commissioning
781 /// date (see [`SettleInput::negativpreis_regime`]), because the
782 /// Solarspitzengesetz rewrote §51 with effect from 25.02.2025 — inside the
783 /// EEG 2023 range.
784 ///
785 /// Use [`EegGesetz::from_db_year`] to convert the `eeg_gesetz` DB column, or
786 /// [`EegGesetz::from_inbetriebnahme_year`] as a fallback.
787 pub eeg_gesetz: EegGesetz,
788
789 /// Plant technology type (optional, used for §51 EEG 2017 wind exemption).
790 ///
791 /// Under **EEG 2017**, wind turbines get a separate 3 MW kW exemption
792 /// (§51 Abs. 3 Nr. 1); other plants get the 500 kW exemption (Nr. 2).
793 /// Derive from `einsd` `erzeugungsart` column via [`ErzeugungsArt::from_db_str`].
794 ///
795 /// `None` is treated as non-wind (conservative: 500 kW exemption under EEG 2017).
796 pub erzeugungsart: Option<ErzeugungsArt>,
797
798 /// §53 Abs. 1 EEG 2023 — whether the Einspeisevergütung rate supplied in the
799 /// scheme is the **gross** anzulegender Wert (as published in §48/BNetzA
800 /// bulletins) rather than the net Vergütungssatz.
801 ///
802 /// When `true`, the engine subtracts the §53 Abs. 1 deduction
803 /// (0.4 ct/kWh Solar/Wind, 0.2 ct/kWh Wasserkraft/Biomasse/Geothermie/Gas)
804 /// keyed on `erzeugungsart` for [`SettlementScheme::FeedInTariff`]. Default
805 /// `false`: the rate is already net (einsd's `eeg_verguetungssaetze` stores
806 /// net rates), so nothing is deducted — this prevents a double deduction.
807 #[cfg_attr(feature = "serde", serde(default))]
808 pub aw_is_gross: bool,
809
810 /// **§44b Abs. 1 EEG 2023** — Biogas >100 kW: annual 45% Bemessungsleistung cap.
811 ///
812 /// For Biogas plants (fermentation biogas, **excluding** fermentation-biomass §44 plants
813 /// and Ausschreibungsanlagen §39) with installed capacity >100 kW, the EEG payment
814 /// is limited to the share of annual production corresponding to 45% of installed kW:
815 ///
816 /// `annual_quota_kwh = leistung_kw × 0.45 × <§3 Nr. 6 hours of the year>`
817 ///
818 /// The hour count is **not** a flat 8 760: it is the actual hours of the
819 /// calendar year (8 784 in a leap year) less the hours before the plant's
820 /// first generation. See [`crate::sect44b_jahreskontingent_kwh`], which is
821 /// what both the settlement and the `check_sect44b_quota` MCP tool call.
822 ///
823 /// When set, this field is the **eligible kWh** for the current billing period (the
824 /// caller tracks cumulative annual production and passes `min(kwh, remaining_quota)`):
825 /// - Eligible part: normal remuneration (this field's value)
826 /// - Excess part (`einspeisemenge_kwh - eligible`):
827 /// - `MarketPremium`: AW reduces to zero, Marktprämie = 0 (§44b Abs. 1 Satz 2)
828 /// - `FeedInTariff`: paid at EPEX Marktwert (`epex_avg_ct_kwh`), requires EPEX price
829 ///
830 /// `None` = cap does not apply (plant ≤100 kW, fermentation biomass §44, Ausschreibung §39,
831 /// or non-Biogas technology).
832 ///
833 /// Legal basis: §44b Abs. 1 EEG 2023 (BGBl. I Nr. 28, 10.01.2023).
834 pub biogas_sect44b_eligible_kwh: Option<Decimal>,
835
836 /// §51 Abs. 2 Nr. 1 EEG 2023 — whether the sub-100-kW exemption has lapsed.
837 ///
838 /// The exemption is transitional: §51 Abs. 2 Nr. 1 grants it only „für
839 /// Zeiträume vor dem **Ablauf des Kalenderjahres**, in dem die Anlage mit
840 /// einem intelligenten Messsystem ausgestattet wird". It therefore survives
841 /// the whole installation year and lapses on 1 January of the year after,
842 /// from when a 30 kWp plant is subject to §51 like any other. Derive it with
843 /// [`crate::negativpreis::imesys_befreiung_entfallen`] rather than comparing
844 /// the installation date with the settlement period.
845 ///
846 /// It lifts **only** that exemption. The 2 kW floor of Abs. 2 Nr. 2 stands
847 /// until the Bundesnetzagentur's §85 Abs. 2 Nr. 12 Festlegung, and the
848 /// exemptions of the older Fassungen (400 kW, 500 kW, 3 MW) are unaffected —
849 /// they have no iMSys condition.
850 ///
851 /// Default: `false` (conservative — retains the exemption when unknown).
852 pub has_imesys: bool,
853
854 /// Technology-specific Jahresmarktwert category (§20 Abs. 2 + Anlage 1 EEG 2023).
855 ///
856 /// Documents which ÜNB technology category `marktwert_ct_kwh` was sourced from.
857 /// The library uses `marktwert_ct_kwh` directly — this field is informational only
858 /// (validation aid and audit label).
859 pub marktwert_kategorie: Option<crate::scheme::MarktpreisKategorie>,
860
861 /// §100 EEG — the date a Bestandsanlage's opt-in into the Solarspitzengesetz
862 /// regime takes effect.
863 ///
864 /// The operator declares in Textform to the Netzbetreiber that §§ 51 and 51a
865 /// shall apply; the declaration runs at the earliest from the end of the
866 /// calendar year in which the plant is fitted with an iMSys. Derive it with
867 /// [`crate::negativpreis::optin_wirksam_ab`]. From that date the plant is
868 /// under the Solarspitzengesetz regime and its anzulegender Wert rises by
869 /// [`crate::negativpreis::SECT51_OPTIN_ZUSCHLAG_CT_KWH`].
870 ///
871 /// `None` — the usual case — leaves the plant on its commissioning vintage.
872 pub sect51_optin_wirksam_ab: Option<Date>,
873
874 /// §51 Abs. 3 EEG — calendar days of an unreported negative-price period,
875 /// for a plant on the **Ausfallvergütung**.
876 ///
877 /// An operator on the Ausfallvergütung must report, with the §71 Abs. 1 Nr. 1
878 /// data, the quantity it fed in while the Spotmarktpreis was continuously
879 /// negative. Where it does not, the claim for that calendar month falls by
880 /// **5 % per calendar day** on which such a period fell, wholly or partly.
881 ///
882 /// Set this to the number of those days when the figure is missing, and `0`
883 /// when it was reported (or the month had no negative period). Applies only
884 /// to [`SettlementScheme::TemporaryFeedInTariff`]; ignored elsewhere.
885 pub sect51_abs3_unreported_days: u32,
886
887 /// §3 Nr. 37 EEG 2023 — **Pilotwindenergieanlage an Land**.
888 ///
889 /// Every Fassung of §51 carves these out of the Negativpreisregel, whatever
890 /// their size. The status is a BNetzA/FGW certification fact about the
891 /// turbine, so it is declared rather than derived.
892 ///
893 /// Default: `false`.
894 pub ist_pilotwindanlage: bool,
895}
896
897impl SettleInput {
898 /// Effective EEG law version for §51/§52 calculation.
899 ///
900 /// When `tariff_source = Transitional(rule)` and the rule implies a specific
901 /// `EegGesetz`, that implied version is returned instead of `self.eeg_gesetz`.
902 ///
903 /// This prevents silent miscalculation when a §100 Transitional rule is set
904 /// without the corresponding `eeg_gesetz` being updated by the caller.
905 ///
906 /// | `tariff_source` | Returns |
907 /// |---|---|
908 /// | `Statutory` / `Auction(_)` | `self.eeg_gesetz` (caller-supplied) |
909 /// | `Transitional(Pre2016Bestandsschutz)` | `EegGesetz::Eeg2012` — §51 never applies |
910 /// | `Transitional(Eeg2017Negativpreis6h)` | `EegGesetz::Eeg2017` — 6h threshold |
911 /// | `Transitional(OldPlantBeforeEeg2023)` | `EegGesetz::Eeg2021` — 4h threshold |
912 /// | `Transitional(_)` other | `self.eeg_gesetz` |
913 ///
914 /// # Example
915 ///
916 /// ```rust
917 /// use eeg_billing::{SettleInput, EegGesetz};
918 /// use eeg_billing::scheme::{TariffSource, Paragraph100Rule};
919 ///
920 /// // Pre-2016 plant: §51 must never apply, regardless of what eeg_gesetz says.
921 /// let input = SettleInput {
922 /// tariff_source: TariffSource::Transitional(Paragraph100Rule::Pre2016Bestandsschutz),
923 /// eeg_gesetz: EegGesetz::Eeg2017, // deliberately wrong — overridden
924 /// ..SettleInput::default()
925 /// };
926 /// assert_eq!(input.effective_eeg_gesetz(), EegGesetz::Eeg2012);
927 /// ```
928 #[must_use]
929 pub fn effective_eeg_gesetz(&self) -> EegGesetz {
930 if let crate::scheme::TariffSource::Transitional(rule) = &self.tariff_source
931 && let Some(implied) = rule.implied_eeg_gesetz()
932 {
933 return implied;
934 }
935 self.eeg_gesetz
936 }
937
938 /// The §51 Negativpreisregel version governing this plant.
939 ///
940 /// Derived from `inbetriebnahme`, because the Solarspitzengesetz boundary
941 /// (25.02.2025) falls inside a calendar year and inside the EEG 2023 range.
942 /// When the commissioning date is unknown the plant's law version supplies a
943 /// coarse fallback, and a §100 `Transitional` rule overrides both — a rule
944 /// that pins a plant to a pre-2016 vintage must keep §51 off it.
945 #[must_use]
946 pub fn negativpreis_regime(&self) -> crate::negativpreis::NegativpreisRegime {
947 use crate::negativpreis::NegativpreisRegime as R;
948 // A §100 rule is an explicit statement about which vintage governs, so it
949 // wins over the date on the record.
950 if let crate::scheme::TariffSource::Transitional(rule) = &self.tariff_source
951 && let Some(implied) = rule.implied_eeg_gesetz()
952 {
953 return match implied {
954 EegGesetz::Eeg2017 => R::Eeg2017,
955 EegGesetz::Eeg2021 => R::Eeg2021,
956 EegGesetz::Eeg2023 => self.inbetriebnahme.map_or(R::Solarspitzen, |ibn| {
957 R::fuer_periode(ibn, self.sect51_optin_wirksam_ab, self.billing_date)
958 }),
959 _ => R::Keine,
960 };
961 }
962 if let Some(ibn) = self.inbetriebnahme {
963 return R::fuer_periode(ibn, self.sect51_optin_wirksam_ab, self.billing_date);
964 }
965 match self.eeg_gesetz {
966 EegGesetz::Eeg2017 => R::Eeg2017,
967 EegGesetz::Eeg2021 => R::Eeg2021,
968 // No date on the record: assume current law rather than a lapsed one.
969 EegGesetz::Eeg2023 => R::Solarspitzen,
970 _ => R::Keine,
971 }
972 }
973}
974
975// ── SettleOutput ──────────────────────────────────────────────────────────────
976
977/// Output of a settlement calculation.
978///
979/// [`Default`] is "nothing settled": `NoData`, no amount, no positions. Every
980/// early exit in the engine builds on it with `..SettleOutput::default()`
981/// rather than restating ten fields — which is how a field added to this struct
982/// stays correct at the twenty-eight places that return one.
983#[derive(Debug, Clone, PartialEq, Eq, Default)]
984#[non_exhaustive]
985#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
986pub struct SettleOutput {
987 /// Total settlement amount in EUR (sum of all `positions`).
988 ///
989 /// `None` when `status` is [`NoData`] or [`PriceMissing`].
990 /// `Some(Decimal::ZERO)` when `status` is [`Sanctioned`] or `Eigenverbrauch`.
991 ///
992 /// [`NoData`]: SettlementStatus::NoData
993 /// [`PriceMissing`]: SettlementStatus::PriceMissing
994 /// [`Sanctioned`]: SettlementStatus::Sanctioned
995 pub settlement_eur: Option<Decimal>,
996
997 /// Effective kWh used in the calculation.
998 ///
999 /// - May be less than `einspeisemenge_kwh` when KWKG hour-limit is approached.
1000 /// - Excludes `kwh_during_negative_epex` when the §27 negative-price rule applies.
1001 pub eligible_kwh: Option<Decimal>,
1002
1003 /// Individual billing positions that make up `settlement_eur`.
1004 ///
1005 /// Empty when `status` is `NoData`, `PriceMissing`, `Sanctioned`, or `Eigenverbrauch`.
1006 ///
1007 /// Multi-component models produce multiple positions:
1008 /// - `Mieterstrom`: base Vergütung + §21 Abs. 3 Zuschlag
1009 /// - `Direktvermarktung`/`Ausschreibung`: Gleitende Marktprämie + §§53b–54 AW-Abzüge
1010 /// - `Flexibilitaet`: base Vergütung + §50 Flex-Prämie
1011 /// - Multi-block plants (§24 Anlagenerweiterung): one position per active block
1012 ///
1013 /// Use `.to_line_item()` on each position to convert to [`billing::LineItem`]
1014 /// for invoice / `BillingDocument` generation.
1015 pub positions: Vec<SettlePosition>,
1016
1017 /// Computation outcome.
1018 pub status: SettlementStatus,
1019
1020 /// §52 EEG 2023 penalty amount owed by plant operator to NB (separate from Vergütung).
1021 ///
1022 /// `None` when `input.pflichtverstoss` was not set.
1023 /// `Some(Decimal::ZERO)` when there is no violation.
1024 /// Positive = operator owes NB (the NB may net this against Vergütung per §52 Abs. 6).
1025 ///
1026 /// This amount is NOT deducted from `settlement_eur`.
1027 pub pflichtzahlung_eur: Option<Decimal>,
1028
1029 /// **§52 Abs. 6 Satz 1 EEG 2023** — Fälligkeitsdatum for the §52 penalty payment.
1030 ///
1031 /// The 15th calendar day of the month following the billing month — same
1032 /// formula as `faelligkeitsdatum` (§26 Abs. 1), but legally distinct.
1033 ///
1034 /// > „Die Zahlungen werden zum 15. Kalendertag des Kalendermonats fällig, der auf
1035 /// > den nach den Absätzen 2 und 4 jeweils maßgeblichen Kalendermonat folgt.“
1036 ///
1037 /// For violations with §52 Abs. 4 extra months (Nr. 5, 7: +3m; Nr. 9: +1m;
1038 /// Nr. 12: +6m), the Fälligkeitsdatum is the 15th after the **last relevant month**.
1039 /// This field computes the 15th after the billing month as the base date.
1040 ///
1041 /// `None` when `billing_date` is not set or `pflichtzahlung_eur` is `None`.
1042 pub pflichtzahlung_faelligkeitsdatum: Option<Date>,
1043
1044 /// §51a EEG 2023 — quarter-hours by which the Vergütungszeitraum is extended.
1045 ///
1046 /// Non-zero only when `input.negative_price_quarter_hours` was provided AND
1047 /// §51 actually reduced the Vergütung in this period.
1048 /// Solar PV: `ceil(lost_qh / 2)` · Others: `lost_qh` (1:1 factor).
1049 pub verlaengerungsanspruch_qh: u64,
1050
1051 /// **§52 Abs. 7 EEG 2023** — whether violations cause loss of dezentrale Einspeisung entgelt.
1052 ///
1053 /// When `true`, the operator loses the entitlement to the Entgelt für dezentrale Einspeisung
1054 /// under §18 StromNEV for the **entire calendar year** in which any §52 violation occurred.
1055 ///
1056 /// Legal basis: §52 Abs. 7 EEG 2023:
1057 /// *„Bei Pflichtverstößen nach Absatz 1 verlieren die Anlagenbetreiber zusätzlich
1058 /// für das gesamte Kalenderjahr den Anspruch auf ein Entgelt für dezentrale
1059 /// Einspeisung nach §18 der Stromnetzentgeltverordnung.“*
1060 ///
1061 /// `true` when `pflichtzahlung_eur.is_some_and(|p| p > 0)`.
1062 /// The NB should also withhold the §18 StromNEV payment for this plant for the year.
1063 pub dezentrale_einspeisung_anspruch_verloren: bool,
1064
1065 /// **§25 Abs. 1 Satz 3 EEG** — billing_days_fraction that was actually applied.
1066 ///
1067 /// The fraction applied to `settlement_eur` and position amounts in this settlement.
1068 /// Either the value provided in `SettleInput.billing_days_fraction` or the
1069 /// auto-computed value from `billing_date`, `inbetriebnahme`, and `foerderendedatum`.
1070 ///
1071 /// `None` when the fraction is 1.0 (full month, no proration applied).
1072 /// `Some(f)` when partial-month proration was applied.
1073 ///
1074 /// Store in the settlement receipt for § 147 AO / GoBD audit trail.
1075 pub billing_days_fraction_applied: Option<Decimal>,
1076
1077 /// **§26 Abs. 1 EEG 2023** — Fälligkeitsdatum for this period's advance payment.
1078 ///
1079 /// The **15th calendar day of the month following the billing month**.
1080 /// Per §26 Abs. 1 EEG 2023:
1081 /// *„Auf die zu erwartenden Zahlungen nach §19 Abs. 1 sind monatlich jeweils zum
1082 /// 15. Kalendertag für den Vormonat Abschläge in angemessenem Umfang zu leisten."*
1083 ///
1084 /// | Billing month | Fälligkeitsdatum |
1085 /// |---|---|
1086 /// | June 2024 | **2024-07-15** |
1087 /// | December 2024 | **2025-01-15** (year rolls over) |
1088 /// | February 2025 | **2025-03-15** |
1089 ///
1090 /// Populated when `SettleInput.billing_date` is provided. `None` otherwise.
1091 ///
1092 /// **Note**: This is the statutory *latest* due date for monthly advance payments.
1093 /// The final annual settlement (Endabrechnung) falls under §26 Abs. 2, whose
1094 /// due date depends on the operator's §71 data submission obligations.
1095 pub faelligkeitsdatum: Option<Date>,
1096}
1097
1098// ── SettlePosition ────────────────────────────────────────────────────────────
1099
1100/// A single billing component of a settlement calculation.
1101///
1102/// Each position represents one regulatory charge line:
1103/// `net_eur = kwh × rate_ct_kwh / 100`.
1104///
1105/// Convert to a [`billing::LineItem`] for invoice generation via [`.to_line_item()`].
1106///
1107/// [`.to_line_item()`]: SettlePosition::to_line_item
1108#[derive(Debug, Clone, PartialEq, Eq)]
1109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1110pub struct SettlePosition {
1111 /// Human-readable description of this charge line.
1112 pub description: String,
1113
1114 /// Legal basis for audit trail (e.g. `"§21 EEG 2023"`, `"§23a EEG 2023 i.V.m. Anlage 1"`).
1115 pub legal_basis: String,
1116
1117 /// Energy quantity this position applies to (kWh).
1118 pub kwh: Decimal,
1119
1120 /// Rate in ct/kWh. May be negative (e.g. `PostEegSpot` at negative EPEX).
1121 pub rate_ct_kwh: Decimal,
1122
1123 /// Net amount in EUR (`kwh × rate_ct_kwh / 100`, rounded to 5dp).
1124 ///
1125 /// Positive = NB owes Anlagenbetreiber (typical).
1126 /// Negative = Anlagenbetreiber owes NB (post-EEG at negative EPEX).
1127 pub eur: Decimal,
1128}
1129
1130impl SettlePosition {
1131 /// Convert this position to a [`billing::LineItem`] for use in
1132 /// [`billing::BillingDocument`] generation (invoice, settlement receipt).
1133 ///
1134 /// Uses `billing::LineItem::for_usage()` with the signed rate — negative
1135 /// EPEX prices produce a negative `net_amount` on a `Sign::Debit` item,
1136 /// correctly modelling the post-EEG scenario where the plant owes the NB.
1137 pub fn to_line_item(&self) -> billing::LineItem {
1138 use billing::{LineItem, Quantity, RoundingStrategy, UnitPrice};
1139
1140 let rate_eur = self.rate_ct_kwh / rust_decimal::Decimal::from(100);
1141 // Typed `Quantity`/`UnitPrice` keep the two unit labels apart, so a
1142 // quantity unit cannot be passed where a price unit belongs.
1143 // `.with_code("KWH")` stamps EN 16931 BT-130 (UN/ECE Rec 20), so the
1144 // `billing::BillingDocument` is a complete EN-16931 source rather than
1145 // leaving a downstream mapper to guess the unit code from "kWh".
1146 // `UnitPrice::rounded(6, …)` prevents silent precision drift when
1147 // rate_ct_kwh is derived from integer arithmetic (ct/100); BO4E Preis.wert
1148 // is 6 decimal places, keeping the stored unit_price consistent with the
1149 // rendered output.
1150 let mut builder = LineItem::for_usage(
1151 &self.description,
1152 Quantity::new(self.kwh, "kWh").with_code("KWH"),
1153 UnitPrice::new(rate_eur, "EUR/kWh").rounded(6, RoundingStrategy::MidpointAwayFromZero),
1154 )
1155 .meta("legal_basis", self.legal_basis.as_str());
1156
1157 // Category tags for ERP filtering
1158 if self.legal_basis.contains("EEG") || self.legal_basis.contains("post-F\u{00f6}rderung") {
1159 builder = builder.tag("eeg");
1160 }
1161 if self.legal_basis.contains("KWKG") {
1162 builder = builder.tag("kwkg");
1163 }
1164 builder = match self.legal_basis.as_str() {
1165 b if b.starts_with("\u{00a7}23a") || b.starts_with("\u{00a7}\u{00a7}22a") => {
1166 builder.tag("marktpraemie")
1167 }
1168 "\u{00a7}21 Abs. 3 EEG 2023" => builder.tag("mieterstrom"),
1169 b if b == "\u{00a7}50b EEG 2023" || b == "\u{00a7}50 EEG 2023" => {
1170 builder.tag("flexibilitaet")
1171 }
1172 b if b.contains("post-F\u{00f6}rderung") => builder.tag("post-eeg-spot"),
1173 "\u{00a7}7 KWKG 2023" => builder.tag("kwk-zuschlag"),
1174 "\u{00a7}21 EEG 2023" => builder.tag("verguetung"),
1175 _ => builder,
1176 };
1177
1178 builder
1179 .build()
1180 .expect("SettlePosition always has a non-empty static description")
1181 }
1182}
1183
1184// ── SettlementStatus ──────────────────────────────────────────────────────────
1185
1186/// Outcome of a settlement calculation.
1187///
1188/// The [`Default`] is [`NoData`](Self::NoData) — the outcome that pays nothing.
1189/// A default that fell into `Calculated` would make a partially-constructed
1190/// [`SettleOutput`] read as a settled figure of zero, which downstream is
1191/// indistinguishable from "correctly settled, nothing due".
1192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1193#[non_exhaustive]
1194#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1195#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
1196pub enum SettlementStatus {
1197 /// Amount calculated successfully.
1198 Calculated,
1199 /// No meter data for the billing period. Try again once data arrives.
1200 #[default]
1201 NoData,
1202 /// Required price data (EPEX monthly average) is missing.
1203 PriceMissing,
1204 /// Förderdauer has ended (KWKG hour-limit exhausted or EEG 20-year period expired).
1205 FoerderungBeendet,
1206 /// § 8 Abs. 4 KWKG — this calendar year's Vollbenutzungsstunden are used up.
1207 ///
1208 /// „Der Zuschlag wird pro Kalenderjahr gezahlt für bis zu […]
1209 /// Vollbenutzungsstunden." The year's quota bounds the year and nothing
1210 /// else: the plant's § 8 Abs. 1–3 lifetime hours are untouched and the
1211 /// Zuschlag resumes on 1 January with a fresh contingent. Reporting
1212 /// [`FoerderungBeendet`](Self::FoerderungBeendet) here would retire a plant
1213 /// that is still owed most of its Förderung.
1214 JahreskontingentErschoepft,
1215 /// §25 / §47 EEG: MaStR registration missing — payment suspended.
1216 Sanctioned,
1217 /// The statute leaves this plant no §19 Abs. 1 claim for this period — not
1218 /// penalised, owed nothing.
1219 ///
1220 /// Two provisions reach it:
1221 ///
1222 /// - **§21 Abs. 1 Satz 1 Nr. 1** — the Einspeisevergütung mit gesetzlich
1223 /// bestimmtem anzulegenden Wert exists only „für Strom aus Anlagen mit
1224 /// einer installierten Leistung von bis zu 100 Kilowatt", so a larger plant
1225 /// assigned to it is owed nothing. Which Veräußerungsform a plant is
1226 /// actually assigned to is register data, so the service that holds it
1227 /// decides this one before calling the engine —
1228 /// [`SettlementScheme`](crate::SettlementScheme) names only a formula.
1229 /// - **§39i Abs. 1** — a bezuschlagte Biogasanlage over its Getreide- und
1230 /// Mais-Höchstanteil. That is a property of the fuel composition the engine
1231 /// is handed, so [`crate::calculate_settlement`] reports it itself.
1232 ///
1233 /// It is a **terminal** outcome for the period: re-running the settlement on
1234 /// the same facts will not change it.
1235 KeinAnspruch,
1236}