Skip to main content

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}