Skip to main content

eeg_billing/
scheme.rs

1//! Clean settlement scheme architecture — the "what" and "where" of EEG remuneration.
2//!
3//! This module separates three orthogonal dimensions that EEG billing depends on:
4//!
5//! | Dimension | Type | Question |
6//! |---|---|---|
7//! | **Scheme** | [`SettlementScheme`] | *How* is remuneration determined? |
8//! | **Tariff source** | [`TariffSource`] | *Where* does the anzulegender Wert come from? |
9//! | **Settlement type** | [`SettlementType`] | *Is this* initial, correction, or reversal? |
10//!
11//! ## Why this separation matters
12//!
13//! The `SettlementScheme + TariffSource` split separates these dimensions:
14//!
15//! - `Ausschreibung` is not a settlement *scheme* — it determines the AW via BNetzA tender.
16//!   The *scheme* is still Marktprämie (§20 EEG); only the AW source changes.
17//! - `Flexibilitaet`/`FlexibilitaetZuschlag` are *adjustments* layered on top of
18//!   the main scheme, not independent settlement schemes.
19//!
20//! The new architecture models these dimensions separately and independently.
21
22use crate::version::EegGesetz;
23use rust_decimal::Decimal;
24use time::Date;
25
26// ── SettlementScheme ──────────────────────────────────────────────────────────
27
28/// Settlement scheme with **embedded parameters** — the formula *and* its inputs.
29///
30/// Each variant carries exactly the parameters meaningful for that scheme.
31/// Shared context (plant data, sanctions, metering) lives in [`crate::model::SettleInput`].
32///
33/// ## Design rationale
34///
35/// The data-bearing enum eliminates an entire class of bugs: it is now impossible to
36/// construct a `SettleInput` with `kwk_max_kwh` set for a `FeedInTariff` plant,
37/// or with `direktverm_aw_ct` absent for a `MarketPremium` plant. The compiler
38/// enforces scheme-parameter consistency at build time.
39///
40/// `marktwert_ct_kwh` remains a context field on `SettleInput` because it is
41/// cross-scheme: used in `MarketPremium` spread, `PostEeg` payment,
42/// `SanktionAlt::VerguetungAufMarktwert`, and `§44b` excess pricing.
43#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(
47    feature = "serde",
48    serde(tag = "scheme", rename_all = "SCREAMING_SNAKE_CASE")
49)]
50pub enum SettlementScheme {
51    /// §21 Abs. 1 Satz 1 Nr. 1 EEG — the **Einspeisevergütung** mit gesetzlich
52    /// bestimmtem anzulegenden Wert, paid by the NB to the Anlagenbetreiber.
53    ///
54    /// Formula: `kwh × verguetungssatz_ct / 100`
55    ///
56    /// **The claim ends at 100 kW installierter Leistung.** Nr. 1 grants it only
57    /// „für Strom aus Anlagen mit einer installierten Leistung von bis zu 100
58    /// Kilowatt", so a larger plant assigned to this scheme is owed nothing — see
59    /// [`crate::direktverm::direktvermarktungspflicht`]. The engine does not
60    /// enforce it, because this enum names a *formula* and the Veräußerungsform a
61    /// plant is actually assigned to is register data: the caller checks it and
62    /// reports [`SettlementStatus::KeinAnspruch`](crate::SettlementStatus::KeinAnspruch).
63    FeedInTariff {
64        /// Net feed-in tariff rate in ct/kWh (gross AW − §53 EEG deduction).
65        /// Fixed at commissioning for the full 20-year Förderdauer.
66        verguetungssatz_ct: Decimal,
67    },
68
69    /// §21 Abs. 1 Satz 1 Nr. 3 EEG — **Ausfallvergütung**.
70    ///
71    /// The fallback a plant above 100 kW falls back to when its Direktvermarkter
72    /// drops out: the same formula as [`FeedInTariff`](Self::FeedInTariff), but
73    /// §53 Abs. 3 reduces the anzulegender Wert by **20 %**.
74    ///
75    /// Supply the plant's **ordinary** rate — the engine applies the reduction
76    /// and rounds to two decimals. Left to the caller, the ordinary rate passes
77    /// straight through and the one scheme that exists for a plant in trouble
78    /// pays it 25 % more than the statute allows.
79    ///
80    /// §21 Abs. 1 Satz 1 Nr. 3 also caps the Inanspruchnahme at **three
81    /// consecutive calendar months and six calendar months per calendar year**;
82    /// exceeding either is a §52 Abs. 1 Nr. 5 Pflichtverstoß, which the caller
83    /// detects (it needs the settlement history) and passes as a
84    /// [`Pflichtverstoss`](crate::Pflichtverstoss).
85    TemporaryFeedInTariff {
86        /// The plant's ordinary rate in ct/kWh, **before** the §53 Abs. 3 cut.
87        verguetungssatz_ct: Decimal,
88    },
89
90    /// §23a EEG i.V.m. Anlage 1 — **Gleitende Marktprämie**.
91    ///
92    /// Formula (Anlage 1 Nr. 3.1.2 / 4.1.2): `MP = max(0, AW − MW)`, settled as
93    /// `MP × kwh / 100`, where `AW = direktverm_aw_ct × wind_korrekturfaktor`.
94    ///
95    /// There is **no additive Managementprämie**. Anlage 1 defines `MP = AW – MW`
96    /// and nothing else; §20 EEG 2023 has no Absätze at all, let alone the
97    /// "+0,4 ct" one. Since EEG 2014 the marketing cost is folded *into* the
98    /// anzulegender Wert — its mirror image is the §53 Abs. 1 deduction of
99    /// 0,4 / 0,2 ct that the Einspeisevergütung route takes off the same AW.
100    ///
101    /// `marktwert_ct_kwh` (context field on `SettleInput`) provides the market reference
102    /// price. Use `TariffSource::Auction(…)` for BNetzA tender plants — same formula,
103    /// different AW source and billing-position label.
104    MarketPremium {
105        /// Anzulegender Wert in ct/kWh — statutory or BNetzA-tendered.
106        /// For Ausschreibungsanlagen: the tender-awarded value.
107        direktverm_aw_ct: Decimal,
108
109        /// §36h EEG — certified wind-onshore Korrekturfaktor.
110        /// Multiplied into `direktverm_aw_ct` before computing the spread.
111        /// Takes precedence over `wind_standort` when both are set.
112        wind_korrekturfaktor: Option<Decimal>,
113
114        /// §36h EEG — wind site quality model for auto-deriving `korrekturfaktor`.
115        /// Ignored when `wind_korrekturfaktor` is explicitly set.
116        wind_standort: Option<crate::wind::WindStandort>,
117    },
118
119    /// §21 Abs. 3 EEG 2023 — **Mieterstrom** surcharge on top of FeedInTariff.
120    ///
121    /// Formula: `kwh × (verguetungssatz_ct + mieter_zuschlag_ct) / 100`
122    TenantElectricity {
123        /// Base Vergütung rate in ct/kWh.
124        verguetungssatz_ct: Decimal,
125        /// §21 Abs. 3 Mieterstrom-Zuschlag in ct/kWh (on top of base rate).
126        mieter_zuschlag_ct: Option<Decimal>,
127    },
128
129    /// §21 EEG post-Förderung — plant fed in at **market spot reference price**.
130    ///
131    /// Formula: `kwh × marktwert_ct_kwh / 100` (no floor; negative EPEX → plant pays).
132    /// §23b EEG 2023 cap: market price capped at 10 ct/kWh for ausgeförderte Anlagen.
133    ///
134    /// `marktwert_ct_kwh` (context field on `SettleInput`) provides the EPEX spot price.
135    PostEeg {
136        /// Optional price floor in ct/kWh. Contract-defined; not a statutory rule.
137        /// `None` = full market exposure.
138        /// `Some(0)` = operator cannot be charged for negative EPEX.
139        /// `Some(x)` = contract-defined minimum (e.g. bilateral agreement).
140        price_floor: Option<Decimal>,
141    },
142
143    /// § 7 KWKG — **KWK-Zuschlag** for combined heat-and-power plants.
144    ///
145    /// Formula: `eligible_kwh × verguetungssatz_ct / 100`, where `eligible_kwh`
146    /// is bounded by both § 8 limits: the lifetime Vollbenutzungsstunden of
147    /// Abs. 1–3 and the Abs. 4 cap on the calendar year.
148    KwkSurcharge {
149        /// KWK-Zuschlag rate in ct/kWh.
150        ///
151        /// § 7 prices per Leistungsanteil, so this is the blended Mischsatz from
152        /// [`crate::kwkg::zuschlag_ct_kwh`], not one band's rate.
153        verguetungssatz_ct: Decimal,
154        /// Cumulative kWh already paid over the plant's life (§ 8 Abs. 1–3).
155        /// `None` → no lifetime limit enforced.
156        kwh_paid_gesamt: Option<Decimal>,
157        /// Lifetime kWh limit = `kwk_leistung_kw × Vollbenutzungsstunden`
158        /// (§ 8 Abs. 1–3). `None` → no lifetime cap applied.
159        max_kwh: Option<Decimal>,
160        /// § 8 Abs. 4 — the kWh still payable in this calendar year: the year's
161        /// `kwk_leistung_kw × Jahreshöchstbetrag` less what the year has already
162        /// been paid for.
163        ///
164        /// `None` → the annual cap is not enforced. It binds independently of the
165        /// lifetime limit, and it is the one that decides what a single year can
166        /// be paid.
167        jahres_restkontingent_kwh: Option<Decimal>,
168    },
169
170    /// §50b EEG 2023 — **Flexibilitätsprämie** for *existing* biomass plants.
171    ///
172    /// Formula: `kwh × (verguetungssatz_ct + flex_praemie_ct_kwh) / 100`
173    FlexibilityPremium {
174        /// Base Vergütung rate in ct/kWh.
175        verguetungssatz_ct: Decimal,
176        /// Flexibilitätsprämie rate in ct/kWh (§50b EEG 2023 + Anlage 3).
177        flex_praemie_ct_kwh: Option<Decimal>,
178    },
179
180    /// §50a EEG 2023 — **Flexibilitätszuschlag** for *new* biomass plants.
181    ///
182    /// Capacity-based payment: `€100/kW/year ÷ 12` per month (kWh-independent).
183    /// Formula: `leistung_kwp_flex × rate_eur_per_kw_year / 12`
184    FlexibilitySurcharge {
185        /// Annual capacity payment rate in EUR/kW/year (statutory: 100 EUR/kW/year).
186        /// Note: this is EUR/kW/year, NOT ct/kWh.
187        rate_eur_per_kw_year: Decimal,
188    },
189
190    /// §21 Abs. 3 EEG — **Eigenverbrauch**: self-consumption, no grid feed-in payment.
191    ///
192    /// Formula: EUR 0 always. No NB payment.
193    Eigenverbrauch,
194
195    /// §21a EEG 2023 — **Sonstige Direktvermarktung**: direct third-party sale.
196    ///
197    /// No EEG payment from NB. Records the period in settlement history.
198    SonstigeDirektvermarktung,
199}
200
201impl Default for SettlementScheme {
202    fn default() -> Self {
203        Self::FeedInTariff {
204            verguetungssatz_ct: Decimal::ZERO,
205        }
206    }
207}
208
209impl SettlementScheme {
210    /// Returns `true` for schemes that require a market reference price (`marktwert_ct_kwh`).
211    #[must_use]
212    pub fn requires_marktwert(&self) -> bool {
213        matches!(self, Self::MarketPremium { .. } | Self::PostEeg { .. })
214    }
215
216    /// Returns `true` for schemes that pay remuneration based on feed-in kWh.
217    #[must_use]
218    pub fn is_kwh_based(&self) -> bool {
219        !matches!(
220            self,
221            Self::FlexibilitySurcharge { .. }
222                | Self::Eigenverbrauch
223                | Self::SonstigeDirektvermarktung
224        )
225    }
226
227    /// Returns `true` when §51 Negativpreisregel potentially applies to this scheme.
228    ///
229    /// §51 Abs. 1 reduces *the anzulegender Wert* to zero, and the AW is what
230    /// Anlage 1 Nr. 1 feeds into `MP = AW − MW` ("der anzulegende Wert unter
231    /// Berücksichtigung der §§ 19 bis 54"). The Marktprämie is therefore §51's
232    /// primary object, not an exception to it.
233    ///
234    /// Does NOT apply to `PostEeg` (no AW left to reduce — the plant is
235    /// ausgefördert), `KwkSurcharge` (KWKG, a different law), `Eigenverbrauch`,
236    /// `SonstigeDirektvermarktung` (§21a: no EEG payment at all), or
237    /// `FlexibilitySurcharge` (§50a is capacity- not energy-based).
238    #[must_use]
239    pub fn negativpreis_rule_applicable(&self) -> bool {
240        matches!(
241            self,
242            Self::FeedInTariff { .. }
243                | Self::MarketPremium { .. }
244                | Self::TenantElectricity { .. }
245                | Self::TemporaryFeedInTariff { .. }
246                | Self::FlexibilityPremium { .. }
247        )
248    }
249
250    /// Return the `verguetungssatz_ct` for schemes that have a fixed tariff rate.
251    /// Returns `None` for market-based or capacity-based schemes.
252    #[must_use]
253    pub fn verguetungssatz_ct(&self) -> Option<Decimal> {
254        match self {
255            Self::FeedInTariff { verguetungssatz_ct }
256            | Self::TemporaryFeedInTariff { verguetungssatz_ct }
257            | Self::TenantElectricity {
258                verguetungssatz_ct, ..
259            }
260            | Self::KwkSurcharge {
261                verguetungssatz_ct, ..
262            }
263            | Self::FlexibilityPremium {
264                verguetungssatz_ct, ..
265            } => Some(*verguetungssatz_ct),
266            _ => None,
267        }
268    }
269}
270
271// ── TariffSource ──────────────────────────────────────────────────────────────
272
273/// How the **Anzulegender Wert (AW)** was determined for a plant.
274///
275/// The AW is the statutory or tendered rate that drives the Marktprämie spread
276/// and serves as the reference for all other payment types.
277///
278/// This is *orthogonal* to [`SettlementScheme`]: the same `MarketPremium` scheme
279/// can be used for both statutory-AW plants (`Statutory`) and BNetzA tender plants
280/// (`Auction`). Only the AW source — and the billing position label — differ.
281#[derive(Debug, Clone, PartialEq)]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283#[cfg_attr(
284    feature = "serde",
285    serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "source")
286)]
287pub enum TariffSource {
288    /// §21 EEG — Statutory AW, set by law at commissioning date (§48 EEG 2023).
289    ///
290    /// Rate is fixed for the 20-year Förderdauer. For solar, the §49 EEG 2023
291    /// semi-annual degression selects the value from the commissioning date —
292    /// see [`crate::rates::solar_pv_ueberschuss_aw_ct`]. The caller supplies the
293    /// resolved rate in `direktverm_aw_ct` / `verguetungssatz_ct`.
294    Statutory,
295
296    /// §§22a, 28 EEG — BNetzA **tender award**: AW set by sealed-bid auction.
297    ///
298    /// The award value (`award_ct` in `AusschreibungMetadata`) replaces the
299    /// statutory AW for the full Förderdauer. Subsequent statutory degression
300    /// does NOT apply to Ausschreibungsanlagen.
301    Auction(AusschreibungMetadata),
302
303    /// §100 EEG — **Transitional rule**: old plant uses old AW from prior EEG version.
304    ///
305    /// Plants commissioned before 01.01.2023 may settle under the rules of the
306    /// EEG version in force at commissioning, not EEG 2023.
307    /// The specific rule is identified by [`Paragraph100Rule`].
308    Transitional(Paragraph100Rule),
309}
310
311#[allow(clippy::derivable_impls)]
312impl Default for TariffSource {
313    fn default() -> Self {
314        Self::Statutory
315    }
316}
317
318impl TariffSource {
319    /// Returns `true` for BNetzA tender plants.
320    #[must_use]
321    pub fn is_auction(&self) -> bool {
322        matches!(self, Self::Auction(_))
323    }
324
325    /// Returns `true` for plants using the §100 Übergangsregelung.
326    #[must_use]
327    pub fn is_transitional(&self) -> bool {
328        matches!(self, Self::Transitional(_))
329    }
330
331    /// Returns `true` for §51b biogas Ausschreibungsanlagen.
332    ///
333    /// When `true`, §51/§51a do NOT apply, and the AW is zero for periods
334    /// where `epex_avg_ct_kwh ≤ 2 ct/kWh` (§51b EEG 2023).
335    #[must_use]
336    pub fn is_biogas_sect51b(&self) -> bool {
337        matches!(self, Self::Auction(m) if m.is_biogas_sect51b)
338    }
339
340    /// Returns `true` for §39n Innovationsausschreibung awards.
341    ///
342    /// These plants receive a **fixed** market premium (feste Marktprämie =
343    /// the Zuschlagswert per kWh, §3 InnAusV) rather than the *gleitende*
344    /// Marktprämie `max(0, AW − Marktwert)` — so the payout does not shrink as
345    /// the Monatsmarktwert rises.
346    #[must_use]
347    pub fn is_innovation_auction(&self) -> bool {
348        matches!(self, Self::Auction(m) if m.innovation_auction)
349    }
350}
351
352// ── AusschreibungMetadata ─────────────────────────────────────────────────────
353
354/// BNetzA tender auction metadata for Ausschreibungsanlagen.
355///
356/// Stores the full lifecycle of the BNetzA award from tender to possible expiry.
357/// This data is needed because auction plants have special rules:
358///
359/// - The AW is the `award_ct`, NOT the statutory rate from §48 EEG.
360/// - A second tender is required when the first award expires (§33 EEG 2023).
361/// - Bürgerenergiegesellschaften have reduced requirements (§22b EEG 2023).
362/// - Innovationsausschreibungen (§39n EEG 2023) pay a fixed rather than a
363///   sliding market premium.
364/// - Biogas auction plants use §51b rules (AW = 0 when EPEX ≤ 2 ct/kWh).
365#[derive(Debug, Clone, PartialEq, Default)]
366#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
367pub struct AusschreibungMetadata {
368    /// BNetzA Zuschlag-ID (e.g. `"SEE-2024-001234"`).
369    pub zuschlag_id: Option<String>,
370    /// Tendered AW in ct/kWh (the legally binding value from the tender result).
371    pub award_ct: Option<Decimal>,
372    /// Date of the BNetzA tender award notification.
373    pub award_date: Option<Date>,
374    /// Whether the award has expired (§33 EEG 2023: unbuilt plant after N years).
375    pub award_expired: bool,
376    /// Innovationsausschreibung (§39n EEG 2023) — fixed market premium instead of
377    /// the sliding one, awarded for combinations of installation and storage.
378    pub innovation_auction: bool,
379    /// **§22b EEG 2023** — Bürgerenergiegesellschaft (§3 Nr. 15).
380    ///
381    /// Such a plant is exempt from the requirement of a *valid Zuschlag*
382    /// (§22 Abs. 2 Satz 2 Nr. 3 for Wind an Land, §22 Abs. 3 Satz 2 Nr. 2 for
383    /// Solaranlagen), so it is settled at the statutory rate despite falling in
384    /// an auction-eligible size class. The exemption is conditional on
385    /// notification to the Bundesnetzagentur within three weeks and on the
386    /// company having commissioned no other plant of the same kind in the
387    /// preceding three years; §22b Abs. 4 requires the status to be re-proven to
388    /// the Netzbetreiber every five years.
389    pub is_buergerenergie: bool,
390    /// **§51b EEG 2023** — Biogas Ausschreibungsanlage with slightly-positive price rule.
391    ///
392    /// For biogas plants (excluding biomethane) whose AW was determined by auction:
393    /// the AW reduces to **zero** when `epex_avg_ct_kwh ≤ 2 ct/kWh`.
394    /// **§51 and §51a do NOT apply** to these plants (§51b Satz 2 EEG 2023).
395    ///
396    /// Legal basis: §51b EEG 2023.
397    /// Source: EEG 2023, Clearingstelle EEG|KWKG Working Text 23.12.2025.
398    pub is_biogas_sect51b: bool,
399}
400
401// ── Paragraph100Rule ──────────────────────────────────────────────────────────
402
403/// §100 EEG 2023 — Übergangsbestimmungen (transition rules).
404///
405/// Plants commissioned before 01.01.2023 often settle under the rules of the
406/// EEG version in force when they were commissioned (§100 Abs. 1 EEG 2023).
407/// This enum identifies which specific §100 subparagraph applies.
408///
409/// ## Important caveat
410///
411/// §100 EEG 2023 has 36+ numbered subsections. This enum covers the most
412/// commonly encountered transition rules. For plant types not covered here,
413/// the caller must determine the applicable rule and supply the corresponding
414/// `verguetungssatz_ct` and `eeg_gesetz` directly.
415///
416/// Per §100 Abs. 1 EEG 2023, the applicable rules are determined by the
417/// transition provisions in force at the time — not a single universal rule.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
419#[non_exhaustive]
420#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
421#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
422pub enum Paragraph100Rule {
423    /// §100 Abs. 1 EEG 2023: old plants (commissioned before 01.01.2023) keep the
424    /// EEG rules as of 31.12.2022 (= EEG 2021 rules) for the remaining Förderdauer.
425    OldPlantBeforeEeg2023,
426
427    /// §100 Abs. 2 Nr. 13 EEG 2021: EEG 2017 plants keep the 6-hour §51 threshold
428    /// (not EEG 2021's 4-hour threshold) per §100 EEG 2021 Abs. 2 Nr. 13.
429    ///
430    /// Used for plants commissioned 2016-01-01 to 2020-12-31.
431    Eeg2017Negativpreis6h,
432
433    /// §100 Abs. 3 EEG 2023: biomass transition — plants with biomass fuel
434    /// changes after 01.01.2023 may use transitional fuel-class rules.
435    BiomassTransition,
436
437    /// §100 Abs. 9 EEG 2023: Solarpaket I transitional — plants whose legal
438    /// classification changed under the Solarpaket I (BGBl I 2024 Nr. 107)
439    /// amendments. Use for Balkonkraftwerk / Stecker-PV reclassifications.
440    SolarpaketITransition,
441
442    /// §100 Abs. 1 Satz 4 EEG 2017 Bestandsschutz: plants commissioned before 01.01.2016
443    /// are permanently exempt from §51 Negativpreisregel.
444    Pre2016Bestandsschutz,
445
446    /// §100 KWKG: KWKG plants use the transitional rule from KWKG 2017 → 2023.
447    KwkgTransition,
448
449    /// §100 Abs. 6 EEG 2023: biomass plants that received their EEG support under
450    /// old §42–§44 rules continue at their original rates and with original fuel-class
451    /// restrictions for the remainder of their Förderdauer.
452    ///
453    /// Commonly applies to solid-biomass and biogas plants commissioned 2012–2020.
454    BiomassOldFuelClassContinuation,
455
456    /// §100 Abs. 7 EEG 2023: hydropower plants that underwent ecological improvements
457    /// retain extended Förderdauer from the modernization date rather than the
458    /// original commissioning date.
459    HydropowerEcologicalModernization,
460
461    /// §100 Abs. 11 EEG 2023: small biomass plants (≤150 kW) that are not subject
462    /// to mandatory Direktvermarktung continue under old EEG 2017 feed-in tariff rules.
463    SmallBiomassBelow150kw,
464
465    /// §100 Abs. 15/16 EEG 2023: auction-built plants whose commissioning deadline
466    /// falls under transitional provisions receive extended Pönalen grace periods.
467    AuctionPoenalTransition,
468
469    /// §100 Abs. 26 EEG 2023: Solarpaket I — existing Mieterstrom buildings reclassified
470    /// to Gemeinschaftliche Gebäudeversorgung (§42b) may continue under the old
471    /// §21 Abs. 3 Mieterstrom rules for the remaining Förderdauer.
472    MieterstromToGgvTransition,
473
474    /// §100 Abs. 2 Nr. 4 EEG 2021: EEG 2012/2014 plants retain the old §23 Abs. 4
475    /// degression schedule (not EEG 2017 §49 quarterly degression).
476    Eeg2012DegressionSchedule,
477}
478
479impl Paragraph100Rule {
480    /// Returns the [`EegGesetz`] version implied by this §100 transition rule.
481    ///
482    /// When `Some`, `calculate_settlement` uses this version for §51/§52 dispatch
483    /// **instead of** the caller-supplied `SettleInput.eeg_gesetz`, preventing
484    /// silent miscalculation when a `Transitional` rule is set without the
485    /// matching `eeg_gesetz` being updated.
486    ///
487    /// Returns `None` for rules that do not imply a specific EEG version — the
488    /// caller's `eeg_gesetz` is then used as-is.
489    ///
490    /// | `Paragraph100Rule` | Implied `EegGesetz` | Reason |
491    /// |---|---|---|
492    /// | `Pre2016Bestandsschutz` | `Eeg2012` | §100 Abs. 1 Satz 4 EEG 2017 — §51 exempt forever |
493    /// | `Eeg2017Negativpreis6h` | `Eeg2017` | 6h threshold, 500kW/3MW exemption |
494    /// | `BiomassOldFuelClassContinuation` | `Eeg2017` | old §42–§44 fuel rules |
495    /// | `SmallBiomassBelow150kw` | `Eeg2017` | small biomass keeps EEG 2017 FiT |
496    /// | `OldPlantBeforeEeg2023` | `Eeg2021` | §100 Abs. 1 EEG 2023 → EEG 2021 rules |
497    /// | all others | `None` | caller's `eeg_gesetz` applies |
498    #[must_use]
499    pub fn implied_eeg_gesetz(self) -> Option<EegGesetz> {
500        match self {
501            // §100 Abs. 1 Satz 4 EEG 2017: plants commissioned before 01.01.2016 are
502            // permanently exempt from §51 Negativpreisregel.
503            Self::Pre2016Bestandsschutz => Some(EegGesetz::Eeg2012),
504            // EEG 2017 plants: 6h consecutive-hour threshold,
505            // wind <3 MW exempt / other <500 kW exempt (§51 Abs. 3 EEG 2017).
506            Self::Eeg2017Negativpreis6h
507            | Self::BiomassOldFuelClassContinuation
508            | Self::SmallBiomassBelow150kw => Some(EegGesetz::Eeg2017),
509            // §100 Abs. 1 EEG 2023: old plants keep rules as of 31.12.2022
510            // = EEG 2021 rules (4h threshold, 500 kW exemption, all types).
511            Self::OldPlantBeforeEeg2023 => Some(EegGesetz::Eeg2021),
512            // All other rules: caller's eeg_gesetz applies.
513            _ => None,
514        }
515    }
516}
517
518// ── SettlementType ────────────────────────────────────────────────────────────
519
520/// Whether this is an initial settlement, correction, or reversal.
521///
522/// DSOs perform settlement corrections and retroactive adjustments frequently:
523/// corrected meter readings, changed tariffs, regulatory reprocessing.
524/// Tracking the settlement type is essential for § 147 AO / GoBD-compliant bookkeeping.
525#[derive(Debug, Clone, PartialEq)]
526#[non_exhaustive]
527#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
528#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
529pub enum SettlementType {
530    /// First settlement for this plant / billing period.
531    Initial,
532    /// Correction of a prior settlement (replaces the original).
533    ///
534    /// `original_id` references the `settlement_receipt.id` being corrected.
535    Correction {
536        /// ID of the original settlement receipt being corrected.
537        original_id: String,
538        /// Reason for the correction (for audit trail).
539        reason: CorrectionReason,
540    },
541    /// Complete reversal of a prior settlement (cancels the original to EUR 0).
542    ///
543    /// Used for regulatory revocations, MaStR retroactive deregistrations, etc.
544    Reversal {
545        /// ID of the original settlement receipt to reverse.
546        original_id: String,
547    },
548}
549
550#[allow(clippy::derivable_impls)]
551impl Default for SettlementType {
552    fn default() -> Self {
553        Self::Initial
554    }
555}
556
557/// Reason for a settlement correction.
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
559#[non_exhaustive]
560#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
561#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
562pub enum CorrectionReason {
563    /// Corrected meter reading (Zählernachlesung).
564    MeterDataCorrected,
565    /// Tariff correction (wrong rate applied).
566    TariffCorrected,
567    /// MaStR registration retroactively confirmed (§52 sanction removed).
568    MastrRegistrationConfirmed,
569    /// Plant capacity correction (wrong kWp applied).
570    CapacityCorrected,
571    /// Regulatory reprocessing (BNetzA ruling changed billing basis).
572    RegulatoryReprocessing,
573    /// Foerderendedatum corrected (§25 Abs. 1 Satz 2 date recalculated).
574    FoerderendedatumCorrected,
575    /// Other/manual correction.
576    Other,
577}
578
579// ── Marktwertserie (Anlage 1 Nr. 2) ──────────────────────────────────────────
580
581/// Which of the two Marktwert series Anlage 1 Nr. 2 EEG 2023 gives a plant.
582///
583/// The Marktprämie is `max(0, AW − MW)`, and using the wrong `MW` misprices
584/// every kWh — so the choice is the plant's vintage, never the operator's.
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
586#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
587#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
588pub enum Marktwertserie {
589    /// Anlage 1 Nr. 3 — the energieträgerspezifische **Monats**marktwert. Final
590    /// when published, and per calendar month.
591    Monatsmarktwert,
592    /// Anlage 1 Nr. 4 — the energieträgerspezifische **Jahres**marktwert. It has
593    /// no month, and the binding figure exists only once the year is over; the
594    /// ÜNB publish a running estimate before that.
595    Jahresmarktwert,
596}
597
598impl Marktwertserie {
599    /// The stored/wire token.
600    #[must_use]
601    pub fn as_db_str(self) -> &'static str {
602        match self {
603            Self::Monatsmarktwert => "MONATSMARKTWERT",
604            Self::Jahresmarktwert => "JAHRESMARKTWERT",
605        }
606    }
607
608    /// Parse a stored/wire token.
609    #[must_use]
610    pub fn from_db_str(s: &str) -> Option<Self> {
611        match s {
612            "MONATSMARKTWERT" => Some(Self::Monatsmarktwert),
613            "JAHRESMARKTWERT" => Some(Self::Jahresmarktwert),
614            _ => None,
615        }
616    }
617}
618
619/// Anlage 1 Nr. 2 EEG 2023 — which Marktwert series a plant's Marktprämie is
620/// computed from.
621///
622/// Satz 1 sends plants „die vor dem 1. Januar 2023 in Betrieb genommen worden
623/// sind **oder** deren Zuschlag vor dem 1. Januar 2023 erteilt worden ist" to
624/// the Monatsmarktwert; Satz 2 sends „Strom aus anderen Anlagen" to the
625/// Jahresmarktwert. Satz 3 then moves a Satz-1 plant onto the Jahresmarktwert
626/// too, „wenn der Anspruch nach der Abgrenzungs- oder der Pauschaloption nach
627/// § 19 Absatz 3b oder 3c geltend gemacht wird".
628///
629/// `zuschlag_datum` is the BNetzA award date where the plant has one; a plant
630/// commissioned in 2024 on a 2022 award takes the **Monats**marktwert, which is
631/// the case a bare Inbetriebnahme test gets wrong.
632///
633/// # Example
634///
635/// ```rust
636/// use eeg_billing::{Marktwertserie, marktwertserie};
637/// use time::macros::date;
638///
639/// // Commissioned 2021 — Satz 1.
640/// assert_eq!(
641///     marktwertserie(date!(2021-06-01), None, false),
642///     Marktwertserie::Monatsmarktwert
643/// );
644/// // Commissioned 2024 on a 2022 Zuschlag — still Satz 1.
645/// assert_eq!(
646///     marktwertserie(date!(2024-06-01), Some(date!(2022-11-01)), false),
647///     Marktwertserie::Monatsmarktwert
648/// );
649/// // Commissioned 2024, no earlier award — Satz 2.
650/// assert_eq!(
651///     marktwertserie(date!(2024-06-01), None, false),
652///     Marktwertserie::Jahresmarktwert
653/// );
654/// // A Satz-1 plant claiming under §19 Abs. 3b/3c — Satz 3.
655/// assert_eq!(
656///     marktwertserie(date!(2021-06-01), None, true),
657///     Marktwertserie::Jahresmarktwert
658/// );
659/// ```
660#[must_use]
661pub fn marktwertserie(
662    inbetriebnahme: Date,
663    zuschlag_datum: Option<Date>,
664    speicher_abgrenzungs_oder_pauschaloption: bool,
665) -> Marktwertserie {
666    if speicher_abgrenzungs_oder_pauschaloption {
667        return Marktwertserie::Jahresmarktwert;
668    }
669    let vor_2023 = |d: Date| d < ANLAGE1_NR2_STICHTAG;
670    if vor_2023(inbetriebnahme) || zuschlag_datum.is_some_and(vor_2023) {
671        Marktwertserie::Monatsmarktwert
672    } else {
673        Marktwertserie::Jahresmarktwert
674    }
675}
676
677/// Anlage 1 Nr. 2 Satz 1 EEG 2023 — „vor dem 1. Januar 2023".
678pub const ANLAGE1_NR2_STICHTAG: Date = time::macros::date!(2023 - 01 - 01);
679
680// ── MarktpreisKategorie ───────────────────────────────────────────────────────
681
682/// Technology-specific EPEX monthly market value (Marktwert) category.
683///
684/// The BNetzA publishes separate Marktwert tables per technology type each month.
685/// For Direktvermarktung, the correct Marktwert must be used — using the wrong
686/// category produces incorrect Marktprämie calculations.
687///
688/// ## Source
689/// BNetzA Marktwert data portal: <https://www.bundesnetzagentur.de/EEG-Marktwerte>
690///
691/// ## Billing note
692/// The EPEX monthly average (`epex_avg_ct_kwh`) in `SettleInput` should match
693/// the Marktwert category appropriate for the plant's `ErzeugungsArt`.
694/// This enum serves as documentation and validation aid.
695#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
696#[non_exhaustive]
697#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
698#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
699pub enum MarktpreisKategorie {
700    /// Marktwert Solar (PV) — published monthly by BNetzA.
701    Solar,
702    /// Marktwert Wind onshore — published monthly by BNetzA.
703    WindOnshore,
704    /// Marktwert Wind offshore — published monthly by BNetzA.
705    WindOffshore,
706    /// Marktwert Biomasse (biogenic feedstocks including biogas, biomethane).
707    Biomasse,
708    /// Marktwert Wasserkraft.
709    Wasserkraft,
710    /// Marktwert Geothermie / sonstige EE.
711    Sonstige,
712    /// EPEX Day-Ahead monthly average (used for PostEEG ausgeförderte Anlagen).
713    EpexDayAhead,
714}