Skip to main content

grid_billing/
types.rs

1//! Input/output types for the settlement calculation engine.
2//!
3//! ## Architecture
4//!
5//! The preferred calculation flow is:
6//!
7//! ```text
8//! Input → Validation → Settlement Engine → SettlementResult → InvoiceDocument → BO4E → EDIFACT
9//! ```
10//!
11//! [`SettlementResult`] is the canonical output. It carries every position
12//! alongside its [`CalculationTrace`], applicable [`LegalReference`]s, the
13//! [`TariffSource`] that justified each rate, and any [`SettlementWarning`]s.
14//!
15//! The service layer (`netzbilanzd`, `invoicd`) adapts `SettlementResult` into
16//! `rubo4e::current::Rechnung` via a local `into_rechnung()` helper — keeping
17//! BO4E as a purely rendering concern outside this crate.
18//!
19//! ## No float money
20//!
21//! All monetary amounts use [`rust_decimal::Decimal`]. The `crate::EuroAmount`
22//! newtype provides overflow-safe EUR arithmetic. No `f32`/`f64` appears anywhere
23//! in settlement calculations.
24
25use crate::rounding::RoundMoney;
26use rust_decimal::Decimal;
27
28// ── Sparte ────────────────────────────────────────────────────────────────────
29
30/// Commodity — Strom (electricity) or Gas.
31///
32/// Controls which legal references are applied to each settlement position:
33/// - `Strom` → `StromNEV`, BK6 Festlegungen
34/// - `Gas` → `GasNEV`, BK7 Festlegungen
35#[derive(
36    Debug,
37    Clone,
38    Copy,
39    PartialEq,
40    Eq,
41    PartialOrd,
42    Ord,
43    Hash,
44    Default,
45    serde::Serialize,
46    serde::Deserialize,
47)]
48pub enum Sparte {
49    /// Electricity (Strom). Default.
50    #[default]
51    Strom,
52    /// Natural gas (Gas).
53    Gas,
54}
55
56// ── Konzessionsabgabe (KAV §2) ────────────────────────────────────────────────
57
58/// Municipality size band for Konzessionsabgabe, per **KAV §2 Abs. 2**.
59///
60/// KAV bands Tarifkunden rates by the municipality's **inhabitant count**, not by
61/// the customer's annual consumption.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
63pub enum GemeindeGroesse {
64    /// bis 25 000 Einwohner.
65    Bis25k,
66    /// bis 100 000 Einwohner.
67    Bis100k,
68    /// bis 500 000 Einwohner.
69    Bis500k,
70    /// über 500 000 Einwohner.
71    Ueber500k,
72}
73
74/// Konzessionsabgabe customer group per **KAV §2**.
75///
76/// The Tarifkunde/Sondervertragskunde split is a **contract-type** test, not a
77/// consumption threshold: KAV §2 Abs. 3 applies to Sondervertragskunden whatever
78/// they consume, and Abs. 2 bands Tarifkunden by municipality size.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
80pub enum KaKundengruppe {
81    /// Tarifkunde — KAV §2 Abs. 2. Rate depends on [`GemeindeGroesse`].
82    ///
83    /// For gas, `nur_kochen_warmwasser` selects between the two Abs. 2 columns:
84    /// supply limited to cooking and hot water, or all other Tariflieferungen.
85    Tarifkunde {
86        /// Municipality size band.
87        gemeinde: GemeindeGroesse,
88        /// Gas only: supply limited to cooking/hot water. Ignored for Strom.
89        nur_kochen_warmwasser: bool,
90    },
91    /// Schwachlaststrom — KAV §2 Abs. 2. **Strom only**; gas has no such tier.
92    Schwachlast,
93    /// Sondervertragskunde — KAV §2 Abs. 3. Flat, independent of municipality size.
94    Sondervertragskunde,
95    /// No Konzessionsabgabe may be agreed or paid — KAV §2 Abs. 4 (Strom) resp.
96    /// Abs. 5 (Gas).
97    ///
98    /// The prohibitions are the Grenzpreisvergleich for Strom-Sondervertrags-
99    /// kunden (Abs. 4) and, for Gas, a Liefermenge über 5 Millionen kWh je Jahr
100    /// und Abnahmefall or a Durchschnittspreis unter dem Grenzpreis (Abs. 5).
101    /// §2 Abs. 7 is **not** one of them: it decides whether a
102    /// Niederspannungslieferung counts as a Tarif- or a Sondervertragslieferung
103    /// (30 kW in mindestens zwei Monaten **und** mehr als 30 000 kWh im Jahr),
104    /// and never that no Konzessionsabgabe is payable.
105    Exempt,
106}
107
108impl KaKundengruppe {
109    /// The KAV §2 **Höchstbetrag** in ct/kWh for this group and Sparte.
110    ///
111    /// Returns `None` for [`KaKundengruppe::Exempt`], and for
112    /// [`KaKundengruppe::Schwachlast`] on gas, which KAV does not provide.
113    ///
114    /// These are statutory **maxima**, not the agreed rate — a concession contract
115    /// may set anything up to them.
116    #[must_use]
117    pub fn hoechstsatz_ct_per_kwh(self, sparte: Sparte) -> Option<Decimal> {
118        let pick = |a: &str| Decimal::from_str_exact(a).ok();
119        match (self, sparte) {
120            (Self::Exempt, _) => None,
121            (Self::Schwachlast, Sparte::Strom) => pick("0.61"),
122            (Self::Schwachlast, Sparte::Gas) => None,
123            (Self::Sondervertragskunde, Sparte::Strom) => pick("0.11"),
124            (Self::Sondervertragskunde, Sparte::Gas) => pick("0.03"),
125            (Self::Tarifkunde { gemeinde, .. }, Sparte::Strom) => pick(match gemeinde {
126                GemeindeGroesse::Bis25k => "1.32",
127                GemeindeGroesse::Bis100k => "1.59",
128                GemeindeGroesse::Bis500k => "1.99",
129                GemeindeGroesse::Ueber500k => "2.39",
130            }),
131            (
132                Self::Tarifkunde {
133                    gemeinde,
134                    nur_kochen_warmwasser: true,
135                },
136                Sparte::Gas,
137            ) => pick(match gemeinde {
138                GemeindeGroesse::Bis25k => "0.51",
139                GemeindeGroesse::Bis100k => "0.61",
140                GemeindeGroesse::Bis500k => "0.77",
141                GemeindeGroesse::Ueber500k => "0.93",
142            }),
143            (
144                Self::Tarifkunde {
145                    gemeinde,
146                    nur_kochen_warmwasser: false,
147                },
148                Sparte::Gas,
149            ) => pick(match gemeinde {
150                GemeindeGroesse::Bis25k => "0.22",
151                GemeindeGroesse::Bis100k => "0.27",
152                GemeindeGroesse::Bis500k => "0.33",
153                GemeindeGroesse::Ueber500k => "0.40",
154            }),
155        }
156    }
157
158    /// Short label for the invoice position text.
159    ///
160    /// `sparte` is read only for [`Self::Exempt`], whose prohibition sits in a
161    /// different Absatz for each Sparte.
162    #[must_use]
163    pub const fn label(self, sparte: Sparte) -> &'static str {
164        match self {
165            Self::Tarifkunde { .. } => "KAV §2 Abs. 2 Tarifkunde",
166            Self::Schwachlast => "KAV §2 Abs. 2 Schwachlast",
167            Self::Sondervertragskunde => "KAV §2 Abs. 3 Sondervertragskunde",
168            Self::Exempt => match sparte {
169                Sparte::Strom => "KAV §2 Abs. 4 — keine Konzessionsabgabe zulässig",
170                Sparte::Gas => "KAV §2 Abs. 5 — keine Konzessionsabgabe zulässig",
171            },
172        }
173    }
174
175    /// The KAV paragraph that governs this group.
176    ///
177    /// Cited on the position, so the invoice states the rule it was actually
178    /// billed under: §2 Abs. 2 for a Tarifkunde, Abs. 3 for a
179    /// Sondervertragskunde, and — where no Konzessionsabgabe may be charged at
180    /// all — Abs. 4 for Strom, Abs. 5 for Gas.
181    #[must_use]
182    pub const fn kav_paragraph(self, sparte: Sparte) -> &'static str {
183        match self {
184            Self::Tarifkunde { .. } | Self::Schwachlast => "§2 Abs. 2",
185            Self::Sondervertragskunde => "§2 Abs. 3",
186            Self::Exempt => match sparte {
187                Sparte::Strom => "§2 Abs. 4",
188                Sparte::Gas => "§2 Abs. 5",
189            },
190        }
191    }
192}
193
194// ── QuantityUnit ──────────────────────────────────────────────────────────────
195
196/// Unit of measure for a settlement position quantity.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
198pub enum QuantityUnit {
199    /// Kilowatt-hours (active energy).
200    Kwh,
201    /// Kilowatts (demand / peak load).
202    Kw,
203    /// Reactive energy (Blindarbeit) — kilovolt-ampere reactive hours.
204    ///
205    /// Used for reactive energy settlement positions per StromNEV §18.
206    Kvarh,
207    /// Reactive power (Blindleistung) — kilovolt-ampere reactive.
208    Kvar,
209    /// Calendar months.
210    Monat,
211    /// Calendar years — the unit of a charge whose published rate is annual and
212    /// whose quantity is the fraction of a year the settlement period covers.
213    Jahr,
214}
215
216// ── Sect14aModule ─────────────────────────────────────────────────────────────
217
218/// §14a EnWG module for steuerbare Verbrauchseinrichtungen (controllable loads).
219///
220/// Source: BNetzA BK8-22/010-A, „Festlegung von Netzentgelten für steuerbare
221/// Anschlüsse und Verbrauchseinrichtungen (NSAVER) nach § 14a EnWG"
222/// (Beschluss 23.11.2023). Modul 1 and Modul 2 apply ab 01.01.2024 (Tenor 1. d)
223/// and 2. c)); Modul 3 is billed ab 01.04.2025 (Tenor 3. d)).
224///
225/// BK6-22-300 is the companion Festlegung — it governs the netzorientierte
226/// Steuerung a Betreiber must take part in, not the Netzentgelt modules.
227///
228/// All three modules are **mandatory** for eligible controllable loads (heat pumps,
229/// EV chargers, battery storage ≥ 4.2 kW) registered with the NB. The LF/NB
230/// must offer at least Modul 1 to all eligible customers.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
232pub enum Sect14aModule {
233    /// Modul 1 — **pauschale Reduzierung des Netzentgelts**.
234    ///
235    /// A flat reduction applied for the whole billing period, published by the
236    /// NB either as an annual EUR amount or as a factor on the rate. It needs no
237    /// additional metering, which is why it is the default where the connection
238    /// holder makes no choice. It is the one base module Modul 3 may be added to.
239    Modul1,
240    /// Modul 2 — **prozentuale Reduzierung des Arbeitspreises**.
241    ///
242    /// The Arbeitspreis of the Netzentgelt is reduced by a percentage for the
243    /// controllable device, which therefore needs its **own metering** — the
244    /// reduction attaches to that device's energy, not to the whole connection.
245    ///
246    /// An **alternative to Modul 1**, not an addition to it, and it takes no
247    /// Modul 3 (see [`Sect14aModule::combinable_with`]).
248    Modul2,
249    /// Modul 3 — **zeitvariable Netzentgelte**, available from 01.04.2025.
250    ///
251    /// Three Tarifstufen — Hochtarif, Standardtarif and Niedertarif — whose
252    /// windows the NB publishes in the UTILTS Zählzeitdefinition. Requires an
253    /// intelligent metering system. It may be combined with Modul 1 but **not**
254    /// with Modul 2.
255    Modul3,
256}
257
258impl Sect14aModule {
259    /// Canonical BNetzA decision reference for this module.
260    #[must_use]
261    pub fn bnentza_reference(self) -> &'static str {
262        "BK8-22/010-A"
263    }
264
265    /// Whether two **different** modules may be held at once.
266    ///
267    /// BK8-22/010-A offers one base module and one optional addition. Modul 1 and
268    /// Modul 2 are the two forms the base takes — a pauschale reduction needing
269    /// no metering, or a percentage on the device's own Arbeitspreis — and the
270    /// Anschlussnutzer picks one. Modul 3 re-prices the Arbeitspreis over time,
271    /// so it composes with the pauschale Modul 1 and not with Modul 2, which
272    /// would reduce the same Arbeitspreis twice.
273    ///
274    /// `Modul 1 + Modul 3` is therefore the only pair, in either order. A module
275    /// paired with itself is not a combination and answers `false`.
276    #[must_use]
277    pub fn combinable_with(self, other: Self) -> bool {
278        matches!(
279            (self, other),
280            (Self::Modul1, Self::Modul3) | (Self::Modul3, Self::Modul1)
281        )
282    }
283
284    /// Display label for the module.
285    #[must_use]
286    pub fn label(self) -> &'static str {
287        match self {
288            Self::Modul1 => "§14a EnWG Modul 1 (pauschale Reduzierung)",
289            Self::Modul2 => "§14a EnWG Modul 2 (prozentuale Arbeitspreisreduzierung)",
290            Self::Modul3 => "§14a EnWG Modul 3 (zeitvariable Netzentgelte)",
291        }
292    }
293}
294
295// ── SettlementType ────────────────────────────────────────────────────────────
296
297/// Which regulated settlement process produced this result.
298///
299/// Determines which BDEW PIDs are applicable and which regulatory references apply.
300#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
301pub enum SettlementType {
302    /// Abschlagsrechnung Netznutzung — PID 31001 (NB → LF).
303    ///
304    /// A payment on account, not a settled period: it prices no energy and
305    /// carries **exactly one** Positionszeile (INVOIC AHB 1.0b, Änd-ID 26817 —
306    /// "Eine Abschlagsrechnung kann und muss genau eine Positionszeile
307    /// enthalten"). What settles it is the Abschlussrechnung that follows,
308    /// which deducts it by invoice number.
309    NneAbschlag,
310    /// Netznutzungsentgelt (NNE) Strom — PID 31002 (NN-Rechnung, NB → LF).
311    NneStrom,
312    /// Netznutzungsentgelt (NNE) Gas — PID 31002 (NN-Rechnung, NB → LF, GasNEV).
313    ///
314    /// NNE Strom and Gas share the INVOIC Prüfidentifikator 31002 (NN-Rechnung);
315    /// the Sparte is carried in the message content, not the PID. Keeping a
316    /// separate variant preserves the correct legal references (StromNEV vs
317    /// GasNEV) without conditional logic in call sites.
318    NneGas,
319    /// Mehr-/Mindermengen settlement Strom — PID 31005 (NB → LF, GPKE (BK6-24-174) Teil 1 Kap. 8.4).
320    MmmStrom,
321    /// Mehr-/Mindermengen settlement Gas — PID 31005 (NB → LF, GaBi Gas 2.1 (BK7-24-01-008)).
322    ///
323    /// Gas MMM settlement uses different legal references from Strom MMM:
324    /// `GaBi Gas 2.1 (BK7-24-01-008)` and `GeLi Gas 3.0 (BK7-24-01-009)`. Using a separate variant
325    /// ensures correct audit traces without conditional logic in call sites.
326    MmmGas,
327    /// Mehr-/Mindermengen Mehrmenge, selbst ausgestellte Rechnung (Lieferung) — PID 31006.
328    ///
329    /// Per INVOIC AHB §3.x, PID 31006 covers the Mehrmenge leg when the Mehr-/
330    /// Mindermenge is treated as a „Lieferung“ and the invoice is self-issued.
331    MmmSelbstausstellt,
332    /// Messstellenbetrieb settlement — PID 31009 (MSB → NB / LF / ESA).
333    MsbRechnung,
334    /// GaBi Gas AWH Sperrprozesse settlement — PID 31011 (NB → LF, BK7-24-01-009 §5.4).
335    ///
336    /// Rechnung sonstige Leistung: bills the LF (LFG/LFA) for abrechnungswürdige
337    /// Handlungen (AWH) performed by the GNB/VNB during Sperrung/Entsperrung.
338    GasAwhSperrung,
339    /// Redispatch 2.0 Einsatzkosten (NB → ÜNB, BK6-20-061).
340    RedispatchKostenblatt,
341    /// Entgelt für dezentrale Erzeugung — §18 StromNEV, NB → Anlagenbetreiber.
342    ///
343    /// A bilateral payment relationship, not an EDIFACT market process: it has
344    /// no Prüfidentifikator and is rendered as an ordinary commercial credit.
345    DezentraleEinspeisung,
346}
347
348impl SettlementType {
349    /// Default BDEW PID for this settlement type.
350    ///
351    /// Callers may override the PID after construction if needed.
352    #[must_use]
353    pub fn default_pid(self) -> u32 {
354        match self {
355            Self::NneAbschlag => 31001,
356            Self::NneStrom => 31002,
357            Self::NneGas => 31002,
358            Self::MmmStrom => 31005,
359            Self::MmmGas => 31005,
360            Self::MmmSelbstausstellt => 31006,
361            Self::MsbRechnung => 31009,
362            Self::GasAwhSperrung => 31011,
363            Self::RedispatchKostenblatt => 0, // no standard PID
364            // Bilateral NB → Anlagenbetreiber payment; not an EDIFACT process.
365            Self::DezentraleEinspeisung => 0,
366        }
367    }
368}
369
370// ── SettlementStatus ──────────────────────────────────────────────────────────
371
372/// Lifecycle status of a settlement result.
373///
374/// Settlements are never destroyed — a correction or cancellation produces a new
375/// result and leaves the original intact.
376///
377/// **Which document supersedes which is not recorded here.** The invoice numbers
378/// linking a correction to what it replaces live on
379/// [`InvoiceDocument::correction_of`], because the same pair of settlements can
380/// be presented under different invoice numbers. What *is* recorded here is
381/// [`SettlementResult::korrektur_grund`] — why the recalculation happened, which
382/// is a fact about the settlement rather than about the document.
383#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
384pub enum SettlementStatus {
385    /// Initial calculation — no prior settlement exists for this period.
386    Initial,
387    /// Correction of a prior settlement.
388    Correction,
389    /// Cancellation of a prior settlement — all positions are negated.
390    Reversal,
391    /// Final settlement — no further corrections expected.
392    Final,
393}
394
395// ── KorrekturGrund ────────────────────────────────────────────────────────────
396
397/// Why a settlement was recalculated.
398///
399/// A correction that cannot say why it happened is not an audit trail. The
400/// invoice numbers alone answer *what* was replaced; they never answer whether
401/// the meter was wrong, the tariff was wrong, or the law changed underneath —
402/// and those have different consequences. A retroactive regulatory change is a
403/// lawful recalculation; a Rechenfehler in the same period is a defect that
404/// should be counted and investigated.
405///
406/// Carried on every non-`Initial` [`SettlementResult`].
407#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
408#[cfg_attr(feature = "bo4e", derive(serde::Deserialize))]
409pub enum KorrekturGrund {
410    /// Corrected metering — a replaced or re-read value (§ 60 Abs. 1 MsbG:
411    /// the MSB owes the berechtigte Stellen *aufbereitete* data, and a
412    /// correction is how that duty is discharged once the first value was
413    /// wrong. Not Abs. 2, which only says where an iMS should do the
414    /// Aufbereitung and obliges nobody to form a value at all).
415    Messwertkorrektur,
416    /// The wrong tariff or price sheet version was applied.
417    Tarifkorrektur,
418    /// Master data was wrong — Netzebene, KA-Klasse, Konzessionsgemeinde.
419    Stammdatenkorrektur,
420    /// A regulatory change applies retroactively to a settled period.
421    RegulatorischeAenderung,
422    /// An arithmetic or logic error in the original settlement.
423    Rechenfehler,
424    /// A clearing result between the parties (Mehr-/Mindermengen, MaBiS).
425    Clearing,
426    /// Anything else — carry the detail in the settlement's warnings.
427    Sonstiges,
428}
429
430impl KorrekturGrund {
431    /// Stable machine-readable code for structured records and reporting.
432    #[must_use]
433    pub const fn code(self) -> &'static str {
434        match self {
435            Self::Messwertkorrektur => "MESSWERTKORREKTUR",
436            Self::Tarifkorrektur => "TARIFKORREKTUR",
437            Self::Stammdatenkorrektur => "STAMMDATENKORREKTUR",
438            Self::RegulatorischeAenderung => "REGULATORISCHE_AENDERUNG",
439            Self::Rechenfehler => "RECHENFEHLER",
440            Self::Clearing => "CLEARING",
441            Self::Sonstiges => "SONSTIGES",
442        }
443    }
444
445    /// Whether this reason indicates a defect in the original settlement rather
446    /// than a lawful recalculation.
447    ///
448    /// Separating the two is the point of recording the reason: a rising count
449    /// of `Rechenfehler` is an engineering signal, while a rising count of
450    /// `RegulatorischeAenderung` is not.
451    #[must_use]
452    pub const fn indicates_defect(self) -> bool {
453        matches!(self, Self::Rechenfehler | Self::Stammdatenkorrektur)
454    }
455}
456
457// ── LegalReference ────────────────────────────────────────────────────────────
458
459/// Regulatory citation that justifies a billing position or rate.
460///
461/// Every [`SettlementPosition`] should carry at least one `LegalReference`.
462/// This enables full auditability: any operator or regulator can trace
463/// exactly which paragraph, ruling, and version authorised each charge.
464#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
465pub enum LegalReference {
466    /// StromNEV — Stromnetzentgeltverordnung (grid usage charges, Strom).
467    ///
468    /// Example: `StromNev { paragraph: "§17" }` for Leistungspreise.
469    StromNev {
470        /// Paragraph reference, e.g. `"§17"`, `"§21"`.
471        paragraph: &'static str,
472    },
473    /// GasNEV — Gasnetzentgeltverordnung (grid usage charges, Gas).
474    GasNev {
475        /// Paragraph reference, e.g. `"§14"`.
476        paragraph: &'static str,
477    },
478    /// KAV — Konzessionsabgabenverordnung (municipal concession fee).
479    ///
480    /// Example: `Kav { paragraph: "§2 Abs. 2" }`.
481    Kav {
482        /// Paragraph reference, e.g. `"§2 Abs. 2"`.
483        paragraph: &'static str,
484    },
485    /// KWKG — Kraft-Wärme-Kopplungsgesetz.
486    ///
487    /// Example: `Kwkg { paragraph: "§26" }` for the KWKG-Umlage.
488    Kwkg {
489        /// Paragraph citation, e.g. `"§26"`.
490        paragraph: &'static str,
491    },
492    /// EnFG — Energiefinanzierungsgesetz.
493    ///
494    /// Governs which Letztverbrauchergruppe an Entnahmestelle falls into and so
495    /// which rate of a network levy applies.
496    EnFG {
497        /// Paragraph citation, e.g. `"§§21 ff."`.
498        paragraph: &'static str,
499    },
500    /// UStG — Umsatzsteuergesetz.
501    ///
502    /// Cited where the tax treatment is itself part of what the position claims:
503    /// an Anzahlung under §14 Abs. 5, a reverse charge under §13b.
504    Ustg {
505        /// Paragraph citation, e.g. `"§14 Abs. 5"`.
506        paragraph: &'static str,
507    },
508    /// §14a EnWG — Steuerbare Verbrauchseinrichtungen (controllable loads).
509    ///
510    /// Governs time-variable (ToU) NNE for heat pumps, EV chargers, etc.
511    Sect14aEnwg {
512        /// Module: Modul1 (flat reduction), Modul2 (HT/NT), or Modul3 (spot).
513        module: Sect14aModule,
514    },
515    /// MsbG — Messstellenbetriebsgesetz (metering point operation).
516    MsbG {
517        /// Paragraph citation, e.g. `"§§6–7"`.
518        paragraph: &'static str,
519    },
520    /// BNetzA decision (Beschluss).
521    ///
522    /// Example: `BnetzaDecision { reference: "BK8-22/010-A" }`.
523    BnetzaDecision {
524        /// Decision reference, e.g. `"BK8-22/010-A"`, `"BK6-24-174"`.
525        reference: &'static str,
526    },
527    /// BDEW application handbook (Anwendungshandbuch).
528    BdewAhb {
529        /// AHB reference, e.g. `"GPKE BK6-22-024"`.
530        reference: &'static str,
531    },
532    /// StromNZV — Stromnetzzugangsverordnung.
533    ///
534    /// **Außer Kraft mit Ablauf des 31.12.2025** (Art. 15 Abs. 4 des Gesetzes
535    /// v. 22.12.2023, BGBl. 2023 I Nr. 405). Valid only for Lieferzeiträume up
536    /// to that date; the successor competence is §20 Abs. 3 EnWG, exercised
537    /// through the BK6 Festlegungen. [`LegalReference::citation`] appends the
538    /// expiry so an archived invoice stays self-explanatory.
539    StromNzv {
540        /// Paragraph citation, e.g. `"§13 Abs. 3"`.
541        paragraph: &'static str,
542    },
543    /// GasNZV — Gasnetzzugangsverordnung 2010.
544    ///
545    /// **Außer Kraft mit Ablauf des 31.12.2025** (Art. 15 Abs. 6 des Gesetzes
546    /// v. 22.12.2023, BGBl. 2023 I Nr. 405). Succeeded by KARLA Gas 2.0
547    /// (BK7-24-01-007), GaBi Gas 2.1 (BK7-24-01-008), GeLi Gas 3.0
548    /// (BK7-24-01-009) and ZuBio (BK7-24-01-010), all in force 01.01.2026.
549    GasNzv {
550        /// Paragraph citation, e.g. `"§25"`.
551        paragraph: &'static str,
552    },
553    /// EnWG — Energiewirtschaftsgesetz (general energy law).
554    Enwg {
555        /// Paragraph citation, e.g. `"§14a"`.
556        paragraph: &'static str,
557    },
558    /// ARegV — Anreizregulierungsverordnung (incentive regulation).
559    ///
560    /// ARegV §§17–21 define the allowed NNE revenue caps and efficiency targets.
561    /// Relevant when documenting why a specific regulated tariff level was approved.
562    ARegV {
563        /// Paragraph citation, e.g. `"§17"`, `"§21"`.
564        paragraph: &'static str,
565    },
566}
567
568impl LegalReference {
569    /// Short human-readable citation string (German).
570    #[must_use]
571    pub fn citation(&self) -> String {
572        match self {
573            Self::StromNev { paragraph } => format!("StromNEV {paragraph}"),
574            Self::GasNev { paragraph } => format!("GasNEV {paragraph}"),
575            Self::Kav { paragraph } => format!("KAV {paragraph}"),
576            Self::Ustg { paragraph } => format!("UStG {paragraph}"),
577            Self::Kwkg { paragraph } => format!("KWKG {paragraph}"),
578            Self::EnFG { paragraph } => format!("EnFG {paragraph}"),
579            Self::Sect14aEnwg { module } => format!("§14a EnWG {}", module.label()),
580            Self::MsbG { paragraph } => format!("MsbG {paragraph}"),
581            Self::BnetzaDecision { reference } => format!("BNetzA {reference}"),
582            Self::BdewAhb { reference } => format!("BDEW {reference}"),
583            Self::StromNzv { paragraph } => {
584                format!("StromNZV {paragraph} (außer Kraft seit 01.01.2026)")
585            }
586            Self::GasNzv { paragraph } => {
587                format!("GasNZV {paragraph} (außer Kraft seit 01.01.2026)")
588            }
589            Self::Enwg { paragraph } => format!("EnWG {paragraph}"),
590            Self::ARegV { paragraph } => format!("ARegV {paragraph}"),
591        }
592    }
593}
594
595// ── TariffSource ──────────────────────────────────────────────────────────────
596
597/// Origin of the tariff rate applied in a settlement position.
598///
599/// Every rate used in a billing position must be traceable to a `TariffSource`.
600/// This enables operators and auditors to answer: *"Why was this rate used?"*
601#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
602pub enum TariffSource {
603    /// Rate from the published and approved `PreisblattNetznutzung` tariff sheet.
604    PublishedTariffSheet {
605        /// Tariff sheet identifier or version, e.g. `"Preisblatt 2025 Q1"`.
606        sheet_id: String,
607    },
608    /// Rate from a historical tariff (retroactive billing or correction).
609    HistoricalTariff {
610        /// Original valid_from date of the tariff.
611        valid_from: time::Date,
612    },
613    /// Regulatory rate mandated by a BNetzA decision.
614    RegulatoryTariff {
615        /// BNetzA decision reference.
616        decision_ref: &'static str,
617    },
618    /// Contract-specific rate negotiated between NB and customer.
619    ContractTariff {
620        /// Contract reference.
621        contract_ref: String,
622    },
623    /// Manual override by operator (requires documentation).
624    ManualOverride {
625        /// Reason for the override.
626        reason: String,
627    },
628}
629
630// ── CalculationTrace ──────────────────────────────────────────────────────────
631
632/// Full audit record for how one [`SettlementPosition`] was computed.
633///
634/// Answers the question: *"Why is this amount on the invoice?"*
635///
636/// Every `CalculationTrace` carries the input values, the applied legal rules,
637/// intermediate results, and the tariff source. This enables:
638/// - Regulator audits (BNetzA §20 EnWG)
639/// - Operator review
640/// - LF dispute resolution
641/// - AI-assisted invoice explainability (MCP tools)
642#[derive(Debug, Clone, serde::Serialize)]
643pub struct CalculationTrace {
644    /// Human-readable explanation of this position.
645    ///
646    /// Example: `"Arbeit 1500 kWh × 3.5 ct/kWh = 52.50 EUR"`
647    pub explanation: String,
648    /// Input quantity used (before rounding).
649    pub input_quantity: Decimal,
650    /// Input unit price in EUR (before rounding, already converted from ct).
651    pub input_unit_price_eur: Decimal,
652    /// Intermediate result before rounding (qty × price).
653    pub gross_eur: Decimal,
654    /// Applied legal references (at least one required).
655    pub legal_refs: Vec<LegalReference>,
656    /// Source of the tariff rate.
657    pub tariff_source: Option<TariffSource>,
658    /// Any §14a reductions applied, expressed as a fraction (0.0–1.0).
659    ///
660    /// `None` when no regulatory reduction applies.
661    /// Example: `Some(Decimal::new(85, 2))` = 85% of full rate (15% reduction).
662    pub regulatory_reduction_factor: Option<Decimal>,
663    /// Notes on rounding applied.
664    ///
665    /// Example: `"rounded to 5 dp per StromNEV §17"`.
666    pub rounding_note: Option<&'static str>,
667}
668
669// ── SettlementWarning ─────────────────────────────────────────────────────────
670
671/// A non-blocking validation issue found during settlement calculation.
672///
673/// Warnings do not prevent the invoice from being generated but should be
674/// reviewed before dispatch. The service layer may choose to block dispatch
675/// on `Severity::Error` warnings.
676#[derive(Debug, Clone, serde::Serialize)]
677pub struct SettlementWarning {
678    /// Severity: informational, warning, or error.
679    pub severity: WarningSeverity,
680    /// Machine-readable warning code.
681    pub code: &'static str,
682    /// Human-readable description.
683    pub message: String,
684}
685
686/// Severity level for [`SettlementWarning`].
687#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
688pub enum WarningSeverity {
689    /// Informational — no action required.
690    Info,
691    /// Potential issue — review recommended before dispatch.
692    Warning,
693    /// Definite issue — should be resolved before dispatch.
694    Error,
695}
696
697// ── InvoicePosition ───────────────────────────────────────────────────────────
698
699/// Semantic kind of a billing position — used by the service layer to derive
700/// the correct `BdewArtikelnummer` for the BO4E `Rechnungsposition`.
701///
702/// `grid-billing` has no `rubo4e` dependency, so this enum is the bridge:
703/// the service layer maps `BillingPositionKind` → `BdewArtikelnummer` in
704/// `into_rechnung()`. Every position in every `SettlementResult` must carry
705/// a `kind` so the INVOIC `Rechnungsposition.artikelnummer` is never missing.
706///
707/// ## BDEW INVOIC AHB requirement
708///
709/// BDEW INVOIC AHBs (FV2025-10-01) mandate `artikelnummer` in every
710/// `SG28 PIA` line item. Missing or wrong Artikelnummern cause counterparty
711/// APERAK rejection. The `invoic-checker` checks 6 plausibility rules;
712/// Artikelnummer matching is part of the tariff-found rule (check 5).
713///
714/// ## Mapping to `BdewArtikelnummer`
715///
716/// | `BillingPositionKind` | `BdewArtikelnummer` | INVOIC AHB ref |
717/// |---|---|---|
718/// | `NneArbeit` | `Wirkarbeit` | PID 31002 (NN-Rechnung) Arbeit |
719/// | `NneArbeitHt` | `Wirkarbeit` | PID 31002 §14a Modul 3 HT |
720/// | `NneArbeitSt` | `Wirkarbeit` | PID 31002 §14a Modul 3 ST |
721/// | `NneArbeitNt` | `Wirkarbeit` | PID 31002 §14a Modul 3 NT |
722/// | `NneArbeitModul2` | `Wirkarbeit` | PID 31002 §14a Modul 2 (rate reduced) |
723/// | `NneArbeitModul1` | `Wirkarbeit` | PID 31002 §14a Modul 1 (rate reduced) |
724/// | `NneLeistung` | `Leistung` | PID 31002 RLM kW charge |
725/// | `NneGasGrundpreis` | `Grundpreis` | PID 31002 Gas monthly base fee |
726/// | `Konzessionsabgabe` | `Konzessionsabgabe` | PID 31002 KAV §2 |
727/// | `Mehrmenge` | `Mehrmenge` | PID 31005 positive imbalance |
728/// | `Mindermenge` | `Mindermenge` | PID 31005 negative imbalance (credit) |
729/// | `MsbGrundgebuehr` | `EntgeltEinbauBetriebWartungMesstechnik` | PID 31009 MSB monthly fee |
730/// | `Messdienstleistung` | `EntgeltMessungAblesung` | PID 31009 reading service |
731/// | `GasAwhSperrung` | `Sperrkosten` | PID 31011 AWH disconnection |
732/// | `GasAwhEntsperrung` | `Entsperrkosten` | PID 31011 AWH reconnection |
733/// | `GasAwhSonstige` | `EntgeltAbrechnung` | PID 31011 other AWH |
734/// | `Blindmehrarbeit` | `Blindmehrarbeit` | Reactive energy excess |
735#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
736pub enum BillingPositionKind {
737    /// The single line of an Abschlagsrechnung — a payment on account.
738    ///
739    /// Carries an amount and nothing else: no quantity, no unit price, because
740    /// an Abschlag prices no energy. What it is *for* is the delivery period on
741    /// the settlement.
742    NneAbschlag,
743    /// Netznutzungsentgelt Arbeit — flat-rate active energy charge (kWh).
744    /// SLP or Gas. → `BdewArtikelnummer::Wirkarbeit`
745    NneArbeit,
746    /// §14a Modul 3 Hochtarif (HT) Arbeit — zeitvariables Netzentgelt, high band.
747    /// → `BdewArtikelnummer::Wirkarbeit`
748    NneArbeitHt,
749    /// §14a Modul 3 Standardtarif (ST) Arbeit — zeitvariables Netzentgelt, middle band.
750    /// → `BdewArtikelnummer::Wirkarbeit`
751    NneArbeitSt,
752    /// §14a Modul 3 Niedertarif (NT) Arbeit — zeitvariables Netzentgelt, low band.
753    /// → `BdewArtikelnummer::Wirkarbeit`
754    NneArbeitNt,
755    /// §14a Modul 1 Arbeit — pauschale Reduzierung applied to the Arbeitspreis.
756    /// → `BdewArtikelnummer::Wirkarbeit` (same article, different rate)
757    NneArbeitModul1,
758    /// §14a Modul 2 Arbeit — prozentuale Reduzierung of the device's Arbeitspreis.
759    /// → `BdewArtikelnummer::Wirkarbeit` (same article, different rate)
760    NneArbeitModul2,
761    /// §14a Modul 3 Spotpreis-NNE — per-dispatch-interval variable rate position.
762    ///
763    /// One `InvoicePosition` is generated per dispatch interval from
764    /// `NneInput::sect14a_modul3_intervals`. Each carries a
765    /// `lastvariable_preisposition_json` with the BO4E `LastvariablePreisposition`
766    /// COM data (pricing formula parameters) for ERP-side validation and portal
767    /// display of the per-interval tariff breakdown.
768    ///
769    /// Priced from the NB's own `PreisblattNetznutzung` formula, not from a
770    /// Festlegung: no § 14a module is spot-linked.
771    /// → `BdewArtikelnummer::Wirkarbeit`
772    NneArbeitModul3,
773    /// Netznutzungsentgelt Leistung — RLM peak demand charge (kW).
774    /// → `BdewArtikelnummer::Leistung`
775    NneLeistung,
776    /// Gas NNE monthly base fee (Grundpreis / Verrechnungspreis).
777    /// GasNEV §14. → `BdewArtikelnummer::Grundpreis`
778    NneGasGrundpreis,
779    /// Konzessionsabgabe — KAV §2 municipal concession fee.
780    /// → `BdewArtikelnummer::Konzessionsabgabe`
781    Konzessionsabgabe,
782    /// Mehrmengen — positive imbalance (actual > profiled).
783    /// PID 31005 GPKE (BK6-24-174) Teil 1 Kap. 8.4 / GaBi Gas 2.1 (BK7-24-01-008). → `BdewArtikelnummer::Mehrmenge`
784    Mehrmenge,
785    /// Mindermengen — negative imbalance credit note (actual < profiled).
786    /// PID 31005. → `BdewArtikelnummer::Mindermenge`
787    Mindermenge,
788    /// MSB Grundgebühr Messstellenbetrieb — monthly metering base fee.
789    /// MsbG §§6–7. → `BdewArtikelnummer::EntgeltEinbauBetriebWartungMesstechnik`
790    MsbGrundgebuehr,
791    /// Einbau und Betrieb einer Steuerungseinrichtung am Netzanschlusspunkt.
792    /// MsbG §30 Abs. 2. → `BdewArtikelnummer::EntgeltEinbauBetriebWartungMesstechnik`
793    MsbSteuereinrichtung,
794    /// Messdienstleistung — periodic reading service fee.
795    /// MsbG §2. → `BdewArtikelnummer::EntgeltMessungAblesung`
796    Messdienstleistung,
797    /// Gas AWH Sperrung — abrechnungswürdige Handlung disconnection.
798    /// BK7-24-01-009 §5.4. → `BdewArtikelnummer::Sperrkosten`
799    GasAwhSperrung,
800    /// Gas AWH Entsperrung — abrechnungswürdige Handlung reconnection.
801    /// BK7-24-01-009 §5.4. → `BdewArtikelnummer::Entsperrkosten`
802    GasAwhEntsperrung,
803    /// Gas AWH sonstige — other abrechnungswürdige Handlung.
804    /// BK7-24-01-009 §5.4. → `BdewArtikelnummer::EntgeltAbrechnung`
805    GasAwhSonstige,
806    /// Blindmehrarbeit — reactive energy beyond the free share.
807    ///
808    /// Charged from the Netzbetreiber's published Preisblatt; StromNEV §17
809    /// governs how those Netzentgelte are formed. **Not** §18, which is the
810    /// Entgelt für dezentrale Erzeugung, and not §19, which is Sonderformen der
811    /// Netznutzung. → `BdewArtikelnummer::Blindmehrarbeit`
812    Blindmehrarbeit,
813    /// Aufschlag für besondere Netznutzung (§19 StromNEV-Umlage).
814    ///
815    /// Funds the reduced individual network charges granted under §19 Abs. 2
816    /// StromNEV. Rate depends on the Letztverbrauchergruppe (EnFG).
817    Sect19StromNevUmlage,
818    /// Offshore-Netzumlage (§17f EnWG).
819    ///
820    /// Funds offshore connection cost and the compensation owed to offshore
821    /// wind farms for unavailable connections.
822    OffshoreNetzumlage,
823    /// KWKG-Umlage (§26 KWKG).
824    ///
825    /// Funds the KWK-Zuschlag paid to CHP operators.
826    KwkgUmlage,
827    /// Entgelt für dezentrale Erzeugung — §18 StromNEV, under Abschmelzung
828    /// (GBK-25-02-1#1). A payment out, so its `net_eur` is negative.
829    DezentraleEinspeisung,
830    /// §19 Abs. 2 StromNEV individual-charge reduction over the Netzentgelt.
831    /// Negative: it takes the published charge down to the agreed fraction.
832    Sect19IndividuellesEntgelt,
833    /// Gas Kapazitätsentgelt — booked capacity at the price sheet's annual
834    /// rate, pro-rated over the period. §15 GasNEV.
835    GasKapazitaetsentgelt,
836}
837
838impl BillingPositionKind {
839    /// Every variant, so a guard can walk the whole enum instead of a list that
840    /// drifts. A wildcard-free `match` in the crate's tests anchors it: adding a
841    /// variant without listing it here fails to compile.
842    pub const ALL: [Self; 26] = [
843        Self::NneAbschlag,
844        Self::NneArbeit,
845        Self::NneArbeitHt,
846        Self::NneArbeitSt,
847        Self::NneArbeitNt,
848        Self::NneArbeitModul1,
849        Self::NneArbeitModul2,
850        Self::NneArbeitModul3,
851        Self::NneLeistung,
852        Self::NneGasGrundpreis,
853        Self::Konzessionsabgabe,
854        Self::Mehrmenge,
855        Self::Mindermenge,
856        Self::MsbGrundgebuehr,
857        Self::MsbSteuereinrichtung,
858        Self::Messdienstleistung,
859        Self::GasAwhSperrung,
860        Self::GasAwhEntsperrung,
861        Self::GasAwhSonstige,
862        Self::Blindmehrarbeit,
863        Self::Sect19StromNevUmlage,
864        Self::OffshoreNetzumlage,
865        Self::KwkgUmlage,
866        Self::DezentraleEinspeisung,
867        Self::Sect19IndividuellesEntgelt,
868        Self::GasKapazitaetsentgelt,
869    ];
870
871    /// Anchors `ALL` to the enum: the `match` has no wildcard, so a new
872    /// variant breaks the build here rather than silently escaping every guard
873    /// that walks `ALL`.
874    #[cfg(test)]
875    pub(crate) const fn is_exhaustive(self) -> bool {
876        match self {
877            Self::NneAbschlag
878            | Self::NneArbeit
879            | Self::NneArbeitHt
880            | Self::NneArbeitSt
881            | Self::NneArbeitNt
882            | Self::NneArbeitModul1
883            | Self::NneArbeitModul2
884            | Self::NneArbeitModul3
885            | Self::NneLeistung
886            | Self::NneGasGrundpreis
887            | Self::Konzessionsabgabe
888            | Self::Mehrmenge
889            | Self::Mindermenge
890            | Self::MsbGrundgebuehr
891            | Self::MsbSteuereinrichtung
892            | Self::Messdienstleistung
893            | Self::GasAwhSperrung
894            | Self::GasAwhEntsperrung
895            | Self::GasAwhSonstige
896            | Self::Blindmehrarbeit
897            | Self::Sect19StromNevUmlage
898            | Self::OffshoreNetzumlage
899            | Self::KwkgUmlage
900            | Self::DezentraleEinspeisung
901            | Self::Sect19IndividuellesEntgelt
902            | Self::GasKapazitaetsentgelt => true,
903        }
904    }
905}
906
907/// One line item in a grid settlement.
908///
909/// Carries raw numbers for the service layer to map into the required format
910/// (BO4E `Rechnungsposition`, EN16931 UBL, etc.).
911///
912/// Invariant: `net_eur == (quantity × unit_price_eur).round_kfm(5)`.
913/// The pricing formula behind a §14a Modul 3 spot-priced position.
914///
915/// Modelled as a value object rather than a serialised BO4E document. The engine
916/// states *what the formula was*; translating that into
917/// `LastvariablePreisposition` — or into any other representation — is the
918/// adapter's job. Carrying BO4E JSON here would put schema knowledge inside the
919/// calculation, untyped and unvalidated, which is the coupling the crate exists
920/// to avoid.
921#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
922pub struct SpotPriceFormula {
923    /// What the price refers to — for Modul 3 always the metered energy.
924    pub reference: PriceReference,
925    /// The unit the price is expressed per.
926    pub unit: QuantityUnit,
927    /// How the rate was derived.
928    pub method: TariffCalculationMethod,
929    /// The rate steps that applied, in order.
930    pub steps: Vec<PriceStep>,
931}
932
933/// What a price refers to.
934#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
935pub enum PriceReference {
936    /// The metered energy quantity.
937    Energiemenge,
938    /// Contracted or metered capacity.
939    Leistung,
940}
941
942/// How a rate was derived.
943#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
944pub enum TariffCalculationMethod {
945    /// A published fixed rate.
946    Festpreis,
947    /// Derived from a spot-market price, under the NB's own Preisblatt formula.
948    Spotpreis,
949}
950
951/// One step of a rate schedule.
952#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
953pub struct PriceStep {
954    /// Lower bound of the step, inclusive.
955    pub from: Decimal,
956    /// Upper bound, exclusive; `None` for the open top step.
957    pub to: Option<Decimal>,
958    /// The rate in EUR per [`SpotPriceFormula::unit`].
959    pub unit_price_eur: Decimal,
960}
961
962/// One line of a settlement.
963///
964/// Carries no position number and no BDEW Artikel-ID: both are properties of the
965/// *document* that presents the settlement, not of the calculation. An adapter
966/// numbers the positions it renders and resolves article identifiers from the
967/// price sheet.
968#[derive(Debug, Clone, serde::Serialize)]
969pub struct SettlementPosition {
970    /// Human-readable description.
971    pub text: String,
972    /// Semantic kind — what was charged, independent of how it is coded.
973    pub kind: BillingPositionKind,
974    /// Metered or contracted quantity.
975    pub quantity: Decimal,
976    /// Unit of measure.
977    pub unit: QuantityUnit,
978    /// Unit price in EUR.
979    pub unit_price_eur: Decimal,
980    /// Net amount in EUR, rounded to 5 decimal places.
981    ///
982    /// May be negative for credit positions (Mindermengen, Gutschriften).
983    pub net_eur: Decimal,
984    /// The formula behind the rate, where one applied.
985    pub spot_price_formula: Option<SpotPriceFormula>,
986    /// Why this amount is what it is.
987    pub trace: CalculationTrace,
988}
989
990impl BillingPositionKind {
991    /// The BDEW Artikelnummer that codes this position, as its codelist name.
992    ///
993    /// Which article number applies depends on both what was charged and what
994    /// kind of settlement it appears in — Gas NNE keeps the classic `WIRKARBEIT`
995    /// code, while Strom NNE moved to Artikel-IDs under BK6-20-160 and carries
996    /// no Artikelnummer at all.
997    ///
998    /// Returned as the codelist *name* rather than a BO4E enum so that this
999    /// crate stays free of BO4E types. A consumer parses it into whatever it
1000    /// renders — `rubo4e::current::BdewArtikelnummer` implements `FromStr` over
1001    /// exactly these names.
1002    ///
1003    /// `None` means the position carries an Artikel-ID instead, resolved from
1004    /// the price sheet by the renderer.
1005    ///
1006    /// Source: BDEW Codeliste der Artikelnummern und Artikel-IDs v5.6.
1007    #[must_use]
1008    pub fn artikelnummer(self, settlement_type: SettlementType) -> Option<&'static str> {
1009        use BillingPositionKind as K;
1010        use SettlementType as ST;
1011        match (self, settlement_type) {
1012            // An Abschlag prices nothing, so it carries no Artikelnummer: the
1013            // codelist names charges, and a payment on account is not one.
1014            (K::NneAbschlag, _) => None,
1015            // Gas NNE keeps the classic codes — BK6-20-160 changed Strom only.
1016            (
1017                K::NneArbeit
1018                | K::NneArbeitHt
1019                | K::NneArbeitSt
1020                | K::NneArbeitNt
1021                | K::NneArbeitModul1
1022                | K::NneArbeitModul2
1023                | K::NneArbeitModul3,
1024                ST::NneGas,
1025            ) => Some("WIRKARBEIT"),
1026            (K::NneLeistung, ST::NneGas) => Some("LEISTUNG"),
1027            (K::NneGasGrundpreis, _) => Some("GRUNDPREIS"),
1028            // Strom NNE: the Artikel-ID replaces the Artikelnummer.
1029            (
1030                K::NneArbeit
1031                | K::NneArbeitHt
1032                | K::NneArbeitSt
1033                | K::NneArbeitNt
1034                | K::NneArbeitModul1
1035                | K::NneArbeitModul2
1036                | K::NneArbeitModul3
1037                | K::NneLeistung,
1038                _,
1039            ) => None,
1040            (K::Konzessionsabgabe, _) => Some("KONZESSIONSABGABE"),
1041            (K::Mehrmenge, _) => Some("MEHRMENGE"),
1042            (K::Mindermenge, _) => Some("MINDERMENGE"),
1043            (K::MsbGrundgebuehr | K::MsbSteuereinrichtung, _) => {
1044                Some("ENTGELT_EINBAU_BETRIEB_WARTUNG_MESSTECHNIK")
1045            }
1046            (K::Messdienstleistung, _) => Some("ENTGELT_MESSUNG_ABLESUNG"),
1047            // AWH Gas positions carry a 2-01-7-xxx Artikel-ID from the input.
1048            (K::GasAwhSperrung | K::GasAwhEntsperrung | K::GasAwhSonstige, _) => None,
1049            (K::Blindmehrarbeit, _) => Some("BLINDMEHRARBEIT"),
1050            // Netzseitige Umlagen (EnFG). `OFFSHORE_HAFTUNGSUMLAGE` is the code's
1051            // legacy name — the levy was renamed Offshore-Netzumlage, the article
1052            // number was not.
1053            (K::Sect19StromNevUmlage, _) => Some("PARAGRAF_19_STROM_NEV_UMLAGE"),
1054            // Bilateral payment outside the INVOIC market processes — the
1055            // codelist has no article number for it.
1056            (K::DezentraleEinspeisung, _) => None,
1057            // A reduction over Strom NNE positions, which carry Artikel-IDs.
1058            (K::Sect19IndividuellesEntgelt, _) => None,
1059            // Capacity is the gas Leistung analogue and keeps the classic code.
1060            (K::GasKapazitaetsentgelt, ST::NneGas) => Some("LEISTUNG"),
1061            (K::GasKapazitaetsentgelt, _) => None,
1062            (K::OffshoreNetzumlage, _) => Some("OFFSHORE_HAFTUNGSUMLAGE"),
1063            (K::KwkgUmlage, _) => Some("ABGABE_KWKG"),
1064        }
1065    }
1066}
1067
1068// ── Arbeitspreis model ────────────────────────────────────────────────────────
1069
1070/// A §14a **Modul 2** reduction factor — the fraction of the published
1071/// Arbeitspreis actually paid.
1072///
1073/// Modul 2 is the *prozentuale* reduction; Modul 1 is the flat annual pauschale
1074/// and carries no factor at all (see [`ArbeitspreisModell::Modul1Pauschal`]).
1075///
1076/// A newtype because the range matters: `"0.40"` is a 60 % reduction, and a
1077/// value outside `(0, 1]` is not a reduction at all. The range is checked in
1078/// the constructor rather than in a validator, so a caller that reaches the
1079/// engine directly cannot multiply the tariff instead of reducing it. It
1080/// travels as a JSON **string** like every other `Decimal` — see the
1081/// architecture page's *Quantities and money on the wire*.
1082#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1083pub struct Reduktionsfaktor(Decimal);
1084
1085impl Reduktionsfaktor {
1086    /// The statutory factor: 40 % of the Arbeitspreis, i.e. a 60 % reduction.
1087    ///
1088    /// BNetzA BK8-22/010-A Tenor 2. b): „Der reduzierte Arbeitspreis entspricht
1089    /// **40%** des Arbeitspreises für die Entnahme ohne Leistungsmessung des
1090    /// Netzbetreibers in der Niederspannung." Tenor 2. c) makes Modul 2
1091    /// verpflichtend ab 01.01.2024, so this is the rate, not a default a
1092    /// Netzbetreiber may publish around — the reference price is the operator's,
1093    /// the percentage is not.
1094    ///
1095    /// Tenor 2. d) adds that no Grundpreis is levied on a Marktlokation billed
1096    /// under Modul 2.
1097    pub const REGELFALL: Self = Self(rust_decimal::dec!(0.40));
1098
1099    /// Build a factor.
1100    ///
1101    /// # Errors
1102    ///
1103    /// Returns [`crate::error::BillingError::InvalidInput`] outside `(0, 1]`.
1104    pub fn new(factor: Decimal) -> Result<Self, crate::error::BillingError> {
1105        if factor <= Decimal::ZERO || factor > Decimal::ONE {
1106            return Err(crate::error::BillingError::InvalidInput {
1107                reason: format!("§14a Modul 2 reduction factor must be in (0, 1], got {factor}"),
1108            });
1109        }
1110        Ok(Self(factor))
1111    }
1112
1113    /// The factor as a fraction.
1114    #[must_use]
1115    pub const fn get(self) -> Decimal {
1116        self.0
1117    }
1118}
1119
1120/// Deserialising goes through [`Reduktionsfaktor::new`], so a factor that
1121/// arrives over the wire is range-checked exactly like one built in process.
1122///
1123/// Deriving it would have reintroduced the unconstrained `Decimal` this newtype
1124/// exists to prevent: a request body carrying `5` would then multiply the
1125/// Arbeitspreis by five with no error anywhere.
1126impl<'de> serde::Deserialize<'de> for Reduktionsfaktor {
1127    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1128        let raw = <Decimal as serde::Deserialize>::deserialize(d)?;
1129        Self::new(raw).map_err(serde::de::Error::custom)
1130    }
1131}
1132
1133/// A §14a **Modul 1** Jahresanteil — the fraction of a year the settlement
1134/// period covers, by which the Netzbetreiber's annual pauschale is credited.
1135///
1136/// A newtype for the same reason as [`Reduktionsfaktor`]: the range carries the
1137/// meaning. Outside `(0, 1]` the figure is not a share of a year — `1` on a
1138/// monthly run credits the whole annual pauschale twelve times over the year,
1139/// and a negative value turns the credit into a charge against the customer.
1140///
1141/// The range is necessary and not sufficient: `1` is a valid share of a year and
1142/// the wrong one for a month. [`crate::settle_nne`] therefore also measures the
1143/// share against the settlement period and warns when the two disagree.
1144///
1145/// It travels as a JSON **string** like every other `Decimal` on this crate's
1146/// wire.
1147#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1148pub struct Jahresanteil(Decimal);
1149
1150impl Jahresanteil {
1151    /// A twelfth — one calendar month of a year.
1152    ///
1153    /// One twelfth to the full precision `Decimal` carries. A shorter constant
1154    /// under-credits: at six decimal places twelve monthly credits of a 120 EUR
1155    /// pauschale come to 119.99952 EUR, and the operator keeps the difference on
1156    /// every plant, every year.
1157    pub const MONAT: Self = Self(rust_decimal::dec!(0.0833333333333333333333333333));
1158
1159    /// A whole year.
1160    pub const JAHR: Self = Self(Decimal::ONE);
1161
1162    /// Build a share of a year.
1163    ///
1164    /// # Errors
1165    ///
1166    /// Returns [`crate::error::BillingError::InvalidInput`] outside `(0, 1]`.
1167    pub fn new(anteil: Decimal) -> Result<Self, crate::error::BillingError> {
1168        if anteil <= Decimal::ZERO || anteil > Decimal::ONE {
1169            return Err(crate::error::BillingError::InvalidInput {
1170                reason: format!("§14a Modul 1 Jahresanteil must be in (0, 1], got {anteil}"),
1171            });
1172        }
1173        Ok(Self(anteil))
1174    }
1175
1176    /// The share as a fraction of a year.
1177    #[must_use]
1178    pub const fn get(self) -> Decimal {
1179        self.0
1180    }
1181}
1182
1183/// Deserialising goes through [`Jahresanteil::new`], so a share that arrives
1184/// over the wire is range-checked exactly like one built in process.
1185impl<'de> serde::Deserialize<'de> for Jahresanteil {
1186    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1187        let raw = <Decimal as serde::Deserialize>::deserialize(d)?;
1188        Self::new(raw).map_err(serde::de::Error::custom)
1189    }
1190}
1191
1192/// A metered quantity priced at a rate.
1193#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1194pub struct MengePreis {
1195    /// Metered energy in kWh.
1196    pub menge_kwh: Decimal,
1197    /// Rate in ct/kWh.
1198    pub preis_ct_per_kwh: Decimal,
1199}
1200
1201/// How the Arbeitspreis is structured, and whether §14a applies.
1202///
1203/// One enum rather than three independent field groups. The five variants —
1204/// `Einheitlich`, the three §14a modules and `SpotpreisNetzentgelt` — are
1205/// mutually exclusive **by construction**, which removes a whole class of defect:
1206///
1207/// - The four HT/NT fields were 2⁴ states of which two were valid. Setting three
1208///   of them fell through to flat billing with no error — the invoice looked
1209///   right and was billed on the wrong basis.
1210/// - Modul 1 and Modul 3 could both be set. The engine applied the flat
1211///   reduction *and* the per-interval rates, double-billing the same energy.
1212/// - Modul 1 and Modul 2 could both be set; the engine silently preferred
1213///   Modul 2 rather than rejecting the conflict.
1214///
1215/// Those were runtime warnings in a validator the engine never called. They are
1216/// now unrepresentable.
1217#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1218pub enum ArbeitspreisModell {
1219    /// A single rate for all metered energy.
1220    Einheitlich(MengePreis),
1221
1222    /// **§14a Modul 1** — pauschale Reduzierung des Netzentgelts.
1223    ///
1224    /// A **flat annual amount** the Netzbetreiber publishes, credited pro rata
1225    /// for the settlement period. It does not scale with consumption — that is
1226    /// what makes it *pauschal*, and what distinguishes it from
1227    /// [`Self::Modul2ProzentualeReduzierung`], which reduces the Arbeitspreis by
1228    /// a percentage. The two were structurally identical here once; a factor on
1229    /// the Arbeitspreis is Modul 2's mechanism wearing Modul 1's name.
1230    ///
1231    /// Needs no additional metering, which is why it is the default where the
1232    /// connection holder makes no choice.
1233    Modul1Pauschal {
1234        /// The energy delivered in the period, at its published rate. Billed in
1235        /// full — Modul 1 does not touch the Arbeitspreis.
1236        basis: MengePreis,
1237        /// The Netzbetreiber's published annual pauschale, in EUR per year.
1238        ///
1239        /// A published amount, so it is non-negative; [`crate::settle_nne`]
1240        /// refuses a negative one, which would turn the credit into a charge.
1241        pauschale_eur_pro_jahr: Decimal,
1242        /// The fraction of a year this settlement period covers, so the annual
1243        /// pauschale is credited pro rata.
1244        jahresanteil: Jahresanteil,
1245    },
1246
1247    /// **§14a Modul 2** — the Arbeitspreis reduced by a percentage.
1248    ///
1249    /// The reduction attaches to the controllable device's own metered energy,
1250    /// so `basis` carries that device's consumption rather than the whole
1251    /// connection's.
1252    Modul2ProzentualeReduzierung {
1253        /// The device's metered energy and its published rate, before reduction.
1254        basis: MengePreis,
1255        /// The fraction of that rate actually paid.
1256        reduktion: Reduktionsfaktor,
1257    },
1258
1259    /// **§14a Modul 3** — zeitvariable Netzentgelte in three Tarifstufen.
1260    ///
1261    /// All three bands are required: BK8-22/010-A Tenor 3. b) obliges a
1262    /// zeitvariables Netzentgelt „mit drei Tarifstufen gemäß der Anlage",
1263    /// and permitting a subset would leave the partial state this type exists to
1264    /// prevent. A band with no energy carries
1265    /// `menge_kwh = 0` rather than being omitted.
1266    Modul3ZeitVariabel {
1267        /// Hochtarif band.
1268        ht: MengePreis,
1269        /// Standardtarif band.
1270        st: MengePreis,
1271        /// Niedertarif band.
1272        nt: MengePreis,
1273    },
1274
1275    /// A spot-derived NNE rate per dispatch interval.
1276    ///
1277    /// **Not a §14a module.** BK8-22/010-A defines exactly three, none of which is
1278    /// spot-linked; this models a Netzentgelt whose rate follows the spot price
1279    /// under the NB's own `PreisblattNetznutzung` formula. The rates arrive
1280    /// already derived — this crate never queries a spot market.
1281    SpotpreisNetzentgelt {
1282        /// The dispatch intervals, each with its own rate.
1283        intervalle: Vec<SpotpreisInterval>,
1284    },
1285}
1286
1287impl ArbeitspreisModell {
1288    /// Total metered energy across the model, in kWh.
1289    ///
1290    /// This is the base the Konzessionsabgabe and the network levies are charged
1291    /// on, so it is derived here once rather than recomputed per levy.
1292    #[must_use]
1293    pub fn menge_kwh(&self) -> Decimal {
1294        match self {
1295            Self::Einheitlich(mp) | Self::Modul1Pauschal { basis: mp, .. } => mp.menge_kwh,
1296            Self::Modul2ProzentualeReduzierung { basis, .. } => basis.menge_kwh,
1297            Self::Modul3ZeitVariabel { ht, st, nt } => ht.menge_kwh + st.menge_kwh + nt.menge_kwh,
1298            Self::SpotpreisNetzentgelt { intervalle } => {
1299                intervalle.iter().map(|i| i.menge_kwh).sum()
1300            }
1301        }
1302    }
1303
1304    /// The §14a module in play, if any.
1305    #[must_use]
1306    pub const fn sect14a_modul(&self) -> Option<Sect14aModule> {
1307        match self {
1308            Self::Einheitlich(_) => None,
1309            Self::Modul1Pauschal { .. } => Some(Sect14aModule::Modul1),
1310            Self::Modul2ProzentualeReduzierung { .. } => Some(Sect14aModule::Modul2),
1311            Self::Modul3ZeitVariabel { .. } => Some(Sect14aModule::Modul3),
1312            // A spot-linked Netzentgelt is the NB's own price model, not one of
1313            // the three modules BK8-22/010-A defines.
1314            Self::SpotpreisNetzentgelt { .. } => None,
1315        }
1316    }
1317}
1318
1319// ── Blindarbeit ───────────────────────────────────────────────────────────────
1320
1321/// Reactive energy and the terms on which its excess is charged.
1322///
1323/// A Netzbetreiber supplies a *free share* of reactive energy alongside the
1324/// active energy delivered, and charges only what exceeds it (Blindmehrarbeit).
1325/// The customary boundary is a power factor of cos φ 0,9 — reactive energy up to
1326/// **tan φ ≈ 0,4843** of the active energy — though many Preisblätter round that
1327/// to a flat 50 %, and some set different shares for inductive and capacitive
1328/// draw.
1329///
1330/// The share is therefore an **input**, not a constant: it is a term of the
1331/// Netzbetreiber's price sheet, and hard-coding one would bill some networks
1332/// wrongly. [`Blindarbeit::COS_PHI_0_9`] is the documented default.
1333#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1334pub struct Blindarbeit {
1335    /// Reactive energy drawn in the period, in kvarh.
1336    pub blindarbeit_kvarh: Decimal,
1337    /// Free share of the active energy, as a fraction.
1338    ///
1339    /// `0.4843` for cos φ 0,9; `0.5` where the Preisblatt rounds it.
1340    pub freigrenze_anteil: Decimal,
1341    /// Price per excess kvarh, in ct/kvarh, from the Preisblatt.
1342    pub preis_ct_per_kvarh: Decimal,
1343}
1344
1345impl Blindarbeit {
1346    /// tan φ at cos φ 0,9 — the customary free share, to 4 dp.
1347    pub const COS_PHI_0_9: Decimal = rust_decimal::dec!(0.4843);
1348
1349    /// The chargeable excess in kvarh for the given active energy.
1350    ///
1351    /// Zero when the draw stays inside the free share; never negative, because
1352    /// an unused allowance is not a credit.
1353    #[must_use]
1354    pub fn mehrarbeit_kvarh(&self, wirkarbeit_kwh: Decimal) -> Decimal {
1355        let frei = wirkarbeit_kwh * self.freigrenze_anteil;
1356        (self.blindarbeit_kvarh - frei).max(Decimal::ZERO)
1357    }
1358}
1359
1360// ── Paired inputs ─────────────────────────────────────────────────────────────
1361
1362/// An RLM demand charge — peak demand and its rate.
1363///
1364/// A pair, because billing one without the other is meaningless — which two
1365/// independent `Option`s cannot express.
1366#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1367pub struct Leistungspreis {
1368    /// The Höchstleistung in kW the price is charged against.
1369    ///
1370    /// Which peak that is follows [`system`](Self::system): the
1371    /// Jahreshöchstleistung „im Abrechnungsjahr" under §17 Abs. 2 Satz 2
1372    /// StromNEV, the month's own Höchstleistung under a Monatsleistungspreis.
1373    pub spitzenleistung_kw: Decimal,
1374    /// The Leistungspreis in EUR per kW, per the unit
1375    /// [`system`](Self::system) names — EUR/kW·a for a Jahresleistungspreis,
1376    /// EUR/kW·Monat for a Monatsleistungspreis.
1377    pub preis_eur_per_kw: Decimal,
1378    /// Which Leistungspreissystem the price sheet states.
1379    ///
1380    /// Defaults to [`LeistungspreisSystem::Jahr`], the only one StromNEV itself
1381    /// defines.
1382    #[serde(default)]
1383    pub system: LeistungspreisSystem,
1384}
1385
1386/// Which Leistungspreissystem a price sheet bills under.
1387///
1388/// §17 Abs. 2 Satz 1 StromNEV builds the Netzentgelt „aus einem
1389/// Jahresleistungspreis in Euro pro Kilowatt und einem Arbeitspreis in Cent pro
1390/// Kilowattstunde", and Satz 2 states the entgelt: „Das Jahresleistungsentgelt
1391/// ist das Produkt aus dem jeweiligen Jahresleistungspreis und der
1392/// Jahreshöchstleistung in Kilowatt der jeweiligen Entnahme im Abrechnungsjahr."
1393/// It is a product of two figures and nothing else — the ordinance sets no
1394/// day-count convention for it, so there is no pro-rating to apply.
1395///
1396/// Abs. 8 nevertheless lets a Netzbetreiber offer Tagesleistungspreise for
1397/// Landstrom „neben einem Jahres- und **Monatsleistungspreissystem**", which
1398/// presupposes that a Monatsleistungspreissystem exists. StromNEV does not
1399/// define it; it is published in the Netzbetreiber's own Preisblatt as a
1400/// EUR/kW·Monat price against the month's Höchstleistung, paired with a higher
1401/// Arbeitspreis. A settlement bills whichever of the two the price sheet states,
1402/// and this enum is where it says which.
1403#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1404#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1405pub enum LeistungspreisSystem {
1406    /// §17 Abs. 2 Satz 2 StromNEV — Jahresleistungspreis × Jahreshöchstleistung
1407    /// im Abrechnungsjahr.
1408    ///
1409    /// The whole Abrechnungsjahr's Leistungsentgelt, so a settlement period
1410    /// shorter than the year bills a year of demand.
1411    /// [`crate::settle_nne`] warns when the two disagree rather than inventing a
1412    /// share the price sheet does not publish.
1413    #[default]
1414    Jahr,
1415    /// A Preisblatt Monatsleistungspreis — EUR/kW·Monat against the period's own
1416    /// Höchstleistung, for the months the period covers.
1417    ///
1418    /// This is the system §17 Abs. 8 StromNEV names alongside the annual one.
1419    Monat {
1420        /// Months billed. A monthly settlement is `1`.
1421        monate: Decimal,
1422    },
1423}
1424
1425/// A Gas NNE Grundpreis — monthly rate and the months billed.
1426#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1427pub struct Grundpreis {
1428    /// Rate in EUR per month.
1429    pub eur_per_month: Decimal,
1430    /// Months in the billing period.
1431    pub months: Decimal,
1432}
1433
1434/// The § 2 Abs. 7 KAV facts that classify a Niederspannungslieferung.
1435///
1436/// „Unbeschadet des § 1 Abs. 3 und 4 gelten Stromlieferungen aus dem
1437/// Niederspannungsnetz (bis 1 Kilovolt) konzessionsabgabenrechtlich als
1438/// Lieferungen an Tarifkunden, es sei denn, die gemessene Leistung des Kunden
1439/// überschreitet in mindestens zwei Monaten des Abrechnungsjahres 30 Kilowatt
1440/// **und** der Jahresverbrauch beträgt mehr als 30.000 Kilowattstunden."
1441///
1442/// Both limbs must be met for the point to be a Sondervertragslieferung, and
1443/// that decides between a 1,32-ct and a 0,11-ct ceiling — so the classification
1444/// is derived from the facts rather than taken from the caller's own label.
1445///
1446/// The reference is the **einzelne Betriebsstätte oder Abnahmestelle** (Satz 2).
1447/// Netzbetreiber and Gemeinde may agree lower figures (Satz 4); where they have,
1448/// state them here.
1449#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1450pub struct NiederspannungsEinstufung {
1451    /// Months of the Abrechnungsjahr in which the **gemessene** Leistung
1452    /// exceeded the Leistungswert — the statutory 30 kW, or the lower figure
1453    /// [`Self::leistungsgrenze_kw`] names. Counted by the caller, because only
1454    /// it holds the per-month series.
1455    pub monate_ueber_leistungsgrenze: u32,
1456    /// Jahresverbrauch at this Betriebsstätte or Abnahmestelle, in kWh.
1457    ///
1458    /// Satz 3 excludes Lieferungen nach §§ 7 und 9 BTOElt and Lieferungen im
1459    /// Rahmen von Sonderabkommen für lastschwache Zeiten from this figure; those
1460    /// are priced under Abs. 2 Nr. 1a and Abs. 3 in their own right.
1461    pub jahresverbrauch_kwh: Decimal,
1462    /// The agreed Leistungswert in kW, where Netzbetreiber and Gemeinde set one
1463    /// below the statutory 30 (Satz 4). Recorded on the settlement so the
1464    /// month count above can be read against the figure it was counted at;
1465    /// `None` means [`Self::LEISTUNGSGRENZE_KW`].
1466    pub leistungsgrenze_kw: Option<Decimal>,
1467    /// The agreed Jahresverbrauchsmenge in kWh, where one below the statutory
1468    /// 30 000 was set (Satz 4). `None` uses the statute's figure.
1469    pub verbrauchsgrenze_kwh: Option<Decimal>,
1470}
1471
1472impl NiederspannungsEinstufung {
1473    /// „überschreitet in mindestens zwei Monaten des Abrechnungsjahres 30
1474    /// Kilowatt" — the statutory Leistungswert.
1475    pub const LEISTUNGSGRENZE_KW: Decimal = rust_decimal::dec!(30);
1476    /// „und der Jahresverbrauch beträgt mehr als 30.000 Kilowattstunden".
1477    pub const VERBRAUCHSGRENZE_KWH: Decimal = rust_decimal::dec!(30_000);
1478    /// „in mindestens zwei Monaten des Abrechnungsjahres".
1479    pub const MINDESTMONATE: u32 = 2;
1480
1481    /// Whether § 2 Abs. 7 makes this a Sondervertragslieferung.
1482    ///
1483    /// Both limbs, because the statute joins them with „und": a point drawing
1484    /// 40 kW in one month of the year, or 200 kW every month on 20 000 kWh, is
1485    /// still a Tariflieferung.
1486    #[must_use]
1487    pub fn ist_sondervertragslieferung(&self) -> bool {
1488        let verbrauchsgrenze = self
1489            .verbrauchsgrenze_kwh
1490            .unwrap_or(Self::VERBRAUCHSGRENZE_KWH);
1491        self.monate_ueber_leistungsgrenze >= Self::MINDESTMONATE
1492            && self.jahresverbrauch_kwh > verbrauchsgrenze
1493    }
1494
1495    /// The group § 2 Abs. 7 puts this metering point in.
1496    #[must_use]
1497    pub fn klasse(&self, gemeinde: GemeindeGroesse) -> KaKundengruppe {
1498        if self.ist_sondervertragslieferung() {
1499            KaKundengruppe::Sondervertragskunde
1500        } else {
1501            KaKundengruppe::Tarifkunde {
1502                gemeinde,
1503                nur_kochen_warmwasser: false,
1504            }
1505        }
1506    }
1507}
1508
1509/// The § 2 Abs. 4 / Abs. 5 Nr. 2 KAV Grenzpreisvergleich, as facts.
1510///
1511/// Both Absätze forbid a Konzessionsabgabe outright — „dürfen
1512/// Konzessionsabgaben … nicht vereinbart oder gezahlt werden" — for a
1513/// Sondervertragskunde whose Durchschnittspreis im Kalenderjahr lies below the
1514/// Grenzpreis. Neither figure is derivable here: the Strom Grenzpreis is the
1515/// Durchschnittserlös the amtliche Statistik published for the *vorletzte*
1516/// Kalenderjahr, and the customer's own Durchschnittspreis is measured
1517/// „unter Einschluß des Netznutzungsentgelts" over the whole supply. Both are
1518/// supplied.
1519#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1520pub struct Grenzpreisvergleich {
1521    /// The customer's Durchschnittspreis im Kalenderjahr, in ct/kWh, ohne USt.
1522    pub durchschnittspreis_ct_per_kwh: Decimal,
1523    /// The applicable Grenzpreis, in ct/kWh, ohne USt.
1524    pub grenzpreis_ct_per_kwh: Decimal,
1525}
1526
1527impl Grenzpreisvergleich {
1528    /// Whether the Verordnung forbids a Konzessionsabgabe on this supply.
1529    ///
1530    /// „unter dem … Durchschnittserlös … liegt" — strictly below, so a price
1531    /// exactly at the Grenzpreis still admits one.
1532    #[must_use]
1533    pub fn verbietet_konzessionsabgabe(&self) -> bool {
1534        self.durchschnittspreis_ct_per_kwh < self.grenzpreis_ct_per_kwh
1535    }
1536}
1537
1538/// § 2 Abs. 5 Nr. 1 KAV — the Gas Grenzmenge per Jahr und Abnahmefall.
1539///
1540/// Above it no Konzessionsabgabe may be agreed or paid for a
1541/// Sondervertragskunde. Netzbetreiber and Gemeinde may agree a lower
1542/// Grenzmenge (Satz 3).
1543pub const KAV_GAS_GRENZMENGE_KWH: Decimal = rust_decimal::dec!(5_000_000);
1544
1545/// A Konzessionsabgabe — the rate together with the customer group it applies to.
1546///
1547/// Paired so the KAV § 2 Höchstbetrag check can always run: independent
1548/// `Option`s let the ceiling check be skipped exactly when an over-charge is
1549/// most likely to go unnoticed.
1550#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1551pub struct Konzessionsabgabe {
1552    /// Published rate in ct/kWh.
1553    pub satz_ct_per_kwh: Decimal,
1554    /// The KAV §2 customer group, which fixes the ceiling.
1555    pub klasse: KaKundengruppe,
1556    /// § 2 Abs. 7 KAV — the facts that classify a Niederspannungslieferung.
1557    ///
1558    /// `None` where the supply is not aus dem Niederspannungsnetz, or where the
1559    /// per-month Leistung is not held; the stated [`Self::klasse`] then stands
1560    /// unchecked.
1561    #[serde(default)]
1562    pub niederspannung: Option<NiederspannungsEinstufung>,
1563    /// § 2 Abs. 4 (Strom) / Abs. 5 Nr. 2 (Gas) KAV — the Grenzpreisvergleich.
1564    ///
1565    /// `None` leaves it unchecked, which is what a Tarifkunde needs: both
1566    /// Absätze speak only of Sondervertragskunden.
1567    #[serde(default)]
1568    pub grenzpreis: Option<Grenzpreisvergleich>,
1569}
1570
1571// ── SettlementPeriod ──────────────────────────────────────────────────────────
1572
1573/// The delivery period a settlement covers.
1574///
1575/// A validated pair rather than two loose dates: constructing the type is the
1576/// ordering check, so no calculation carries its own copy of it.
1577///
1578/// Both bounds are inclusive: a monthly period runs from the 1st to the last day
1579/// of the month, matching how Netzentgelte are published and billed.
1580#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1581pub struct SettlementPeriod {
1582    from: time::Date,
1583    to: time::Date,
1584}
1585
1586impl SettlementPeriod {
1587    /// Build a period.
1588    ///
1589    /// # Errors
1590    ///
1591    /// Returns [`crate::error::BillingError::InvalidInput`] when `from` is after `to`. A
1592    /// zero-length period (`from == to`) is a valid single day.
1593    pub fn new(from: time::Date, to: time::Date) -> Result<Self, crate::error::BillingError> {
1594        if from > to {
1595            return Err(crate::error::BillingError::InvalidInput {
1596                reason: format!("period start {from} is after its end {to}"),
1597            });
1598        }
1599        Ok(Self { from, to })
1600    }
1601
1602    /// Start of the period, inclusive.
1603    #[must_use]
1604    pub const fn from(&self) -> time::Date {
1605        self.from
1606    }
1607
1608    /// End of the period, inclusive.
1609    #[must_use]
1610    pub const fn to(&self) -> time::Date {
1611        self.to
1612    }
1613
1614    /// Number of days covered, both bounds inclusive.
1615    #[must_use]
1616    pub fn days(&self) -> i64 {
1617        (self.to - self.from).whole_days() + 1
1618    }
1619
1620    /// The share of a year the period covers.
1621    ///
1622    /// Each calendar year the period touches contributes its own days over its
1623    /// own length, so a period running 15 December 2023 to 14 January 2024 is
1624    /// 17/365 + 14/366 and not 31 days over either year alone. Taking the
1625    /// divisor from the starting year mis-scales every period that straddles a
1626    /// leap-year boundary, and the error is silent because the figure still
1627    /// looks like a plausible fraction.
1628    ///
1629    /// A full calendar year returns exactly `1`.
1630    #[must_use]
1631    pub fn jahresanteil(&self) -> Decimal {
1632        let mut anteil = Decimal::ZERO;
1633        for jahr in self.from.year()..=self.to.year() {
1634            let Ok(jahresbeginn) = time::Date::from_ordinal_date(jahr, 1) else {
1635                continue;
1636            };
1637            let jahrestage = time::util::days_in_year(jahr);
1638            let Ok(jahresende) = time::Date::from_ordinal_date(jahr, jahrestage) else {
1639                continue;
1640            };
1641            let von = self.from.max(jahresbeginn);
1642            let bis = self.to.min(jahresende);
1643            if von > bis {
1644                continue;
1645            }
1646            let tage = (bis - von).whole_days() + 1;
1647            anteil += Decimal::from(tage) / Decimal::from(jahrestage);
1648        }
1649        anteil
1650    }
1651}
1652
1653// ── SettlementResult ──────────────────────────────────────────────────────────
1654
1655/// What a settlement calculation produced.
1656///
1657/// This is the canonical output of every calculation in this crate. It answers
1658/// *what is owed and why*, and deliberately not *what the invoice looks like*:
1659/// invoice numbers, issue and due dates, Prüfidentifikatoren and position
1660/// numbering live on [`InvoiceDocument`], which an adapter builds around this.
1661///
1662/// The separation is what makes a settlement recomputable. The same period can
1663/// be settled twice — for a correction, a dispute, or an audit — and the two
1664/// results compared, without inventing a document each time.
1665///
1666/// ## Explainability
1667///
1668/// Every position carries a [`CalculationTrace`]; [`Self::all_legal_refs`]
1669/// collects the paragraphs the settlement rests on. `warnings` records what the
1670/// engine could not do, which is as much part of the result as the amounts.
1671#[derive(Debug, Clone, serde::Serialize)]
1672pub struct SettlementResult {
1673    /// What was settled.
1674    pub settlement_type: SettlementType,
1675    /// Where this settlement sits in the correction lifecycle.
1676    pub status: SettlementStatus,
1677    /// Why this settlement was recalculated.
1678    ///
1679    /// `None` for an `Initial` settlement and required for every other status —
1680    /// see [`SettlementResult::lineage_is_consistent`].
1681    pub korrektur_grund: Option<KorrekturGrund>,
1682    /// The delivery period.
1683    pub period: SettlementPeriod,
1684    /// The rules the calculation applied.
1685    pub regime: crate::regulatory::RegulatoryRegime,
1686    /// Commodity.
1687    pub sparte: Sparte,
1688    /// The metering location settled.
1689    pub malo_id: String,
1690    /// Sender MP-ID — the party issuing the invoice.
1691    ///
1692    /// The Netzbetreiber for NNE/MMM, and the **Messstellenbetreiber** for a
1693    /// MSB-Rechnung (PID 31009). Named for the role it plays, not for one of
1694    /// the roles that can fill it.
1695    pub sender_mp_id: String,
1696    /// Recipient MP-ID — the party being billed (LF, NB, MSB, MGV or ESA).
1697    pub recipient_mp_id: String,
1698    /// The positions, in calculation order.
1699    pub positions: Vec<SettlementPosition>,
1700    /// Net total in EUR, rounded to 2 decimal places.
1701    pub total_eur: Decimal,
1702    /// The Umsatzsteuer on that net total.
1703    ///
1704    /// §14 Abs. 4 Nr. 8 UStG requires the rate and the amount on every invoice,
1705    /// or a note saying why neither is stated. A settlement that carries only a
1706    /// net figure cannot be rendered as a lawful Rechnung, and the recipient
1707    /// gets no Vorsteuerabzug from one.
1708    pub steuer: crate::umsatzsteuer::Steuerausweis,
1709    /// What the engine could not do, or did with a caveat.
1710    pub warnings: Vec<SettlementWarning>,
1711}
1712
1713impl SettlementResult {
1714    /// Whether the lifecycle status and the recorded reason agree.
1715    ///
1716    /// An `Initial` settlement corrects nothing and must carry no reason; every
1717    /// other status is a recalculation and must say why. A `Correction` with no
1718    /// reason is the state this check exists to catch — it looks like a complete
1719    /// settlement and answers none of the questions an audit asks of one.
1720    #[must_use]
1721    pub const fn lineage_is_consistent(&self) -> bool {
1722        match self.status {
1723            SettlementStatus::Initial => self.korrektur_grund.is_none(),
1724            _ => self.korrektur_grund.is_some(),
1725        }
1726    }
1727
1728    /// Whether this settlement records a defect in an earlier one.
1729    ///
1730    /// Distinguishes an engineering signal from a lawful recalculation — see
1731    /// [`KorrekturGrund::indicates_defect`].
1732    #[must_use]
1733    pub fn corrects_a_defect(&self) -> bool {
1734        self.korrektur_grund
1735            .is_some_and(KorrekturGrund::indicates_defect)
1736    }
1737}
1738
1739// ── Abschlagsverrechnung ──────────────────────────────────────────────────────
1740
1741/// An Abschlagsrechnung a later invoice deducts.
1742///
1743/// The INVOIC AHB puts these in the Summenteil, not among the positions:
1744/// `SG50 MOA+113` carries the **gross** amount already paid, `SG51 RFF+AFL` the
1745/// invoice number it was billed under and `SG51 DTM+3` that invoice's date. They
1746/// therefore reduce what is *owed*, never the net or the tax — §14 Abs. 5 UStG
1747/// taxes the Anzahlung when it is received, so the Abschlussrechnung does not
1748/// tax it a second time.
1749///
1750/// Two rules from the AHB travel with this type:
1751///
1752/// - **\[526\]** — the amount stated must equal the referenced Abschlagsrechnung's
1753///   own Rechnungsbetrag. A deduction that does not match what was billed is a
1754///   deduction the counterparty will reject.
1755/// - **\[519\]** — a *stornierte* Abschlagsrechnung is not listed. It was
1756///   reversed, so nothing was paid on it, and deducting it would credit money
1757///   that never moved.
1758#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1759pub struct Abschlagsverrechnung {
1760    /// The Abschlagsrechnung's invoice number (`SG51 RFF+AFL`).
1761    pub rechnungsnummer: String,
1762    /// That invoice's date (`SG51 DTM+3`).
1763    pub rechnungsdatum: time::Date,
1764    /// The **gross** amount already billed on it (`SG50 MOA+113`, inkl. USt.).
1765    pub betrag_brutto_eur: Decimal,
1766}
1767
1768// ── InvoiceDocument ───────────────────────────────────────────────────────────
1769
1770/// A settlement presented as an invoice.
1771///
1772/// Everything here is a property of the document rather than of the calculation:
1773/// an invoice number, the dates it was issued and falls due, the
1774/// Prüfidentifikator that routes it, and the reference to whatever it corrects.
1775/// None of it affects what is owed.
1776///
1777/// Built by an adapter around a [`SettlementResult`]; the engine never produces
1778/// one, which is why the engine can be run without inventing an invoice number.
1779#[derive(Debug, Clone, serde::Serialize)]
1780pub struct InvoiceDocument {
1781    /// What the document presents.
1782    pub settlement: SettlementResult,
1783    /// BDEW Prüfidentifikator.
1784    pub pid: u32,
1785    /// Unique invoice reference.
1786    pub rechnungsnummer: String,
1787    /// The `rechnungsnummer` this corrects, if any.
1788    pub correction_of: Option<String>,
1789    /// Issue date.
1790    pub invoice_date: time::Date,
1791    /// Payment due date (Zahlungsziel, §271 BGB).
1792    pub due_date: time::Date,
1793    /// The billing cadence — `IMD+7081` on the wire.
1794    ///
1795    /// A document fact, not a calculation one: an NNE settlement is the same
1796    /// arithmetic whether it is billed monthly, per Turnus or as the
1797    /// Abschlussrechnung that closes a year. `None` leaves the field unset
1798    /// rather than guessing a rhythm nothing supports.
1799    pub cadence: Option<Rechnungscharakter>,
1800    /// Abschlagsrechnungen this document settles, deducted from what is owed.
1801    ///
1802    /// Empty on an Abschlagsrechnung itself and on every document that has no
1803    /// payments on account to reconcile.
1804    pub abschlaege: Vec<Abschlagsverrechnung>,
1805}
1806
1807/// The billing cadence of a Netznutzungsrechnung — `IMD+7081`.
1808///
1809/// Named for what the AHB calls it. The set is closed: these are the codes the
1810/// INVOIC AHB 1.0b permits for a Netznutzungsrechnung, and inventing a rhythm
1811/// outside them would put a claim on the wire the standard does not carry.
1812#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1813pub enum Rechnungscharakter {
1814    /// `ABS` — a payment on account (PID 31001).
1815    Abschlagsrechnung,
1816    /// `ABR` — the invoice that closes a period and settles its Abschläge.
1817    Abschlussrechnung,
1818    /// `JVR` — the periodic invoice of a billing cycle.
1819    Turnusrechnung,
1820    /// `MVR` — a monthly invoice.
1821    Monatsrechnung,
1822    /// `ZVR` — an invoice between two Turnus invoices.
1823    Zwischenrechnung,
1824}
1825
1826impl InvoiceDocument {
1827    /// Positions paired with their 1-based document numbers.
1828    ///
1829    /// Numbering is assigned here, at rendering time, rather than carried through
1830    /// the calculation as mutable state.
1831    pub fn numbered_positions(&self) -> impl Iterator<Item = (u32, &SettlementPosition)> {
1832        self.settlement
1833            .positions
1834            .iter()
1835            .enumerate()
1836            .map(|(i, p)| (u32::try_from(i + 1).unwrap_or(u32::MAX), p))
1837    }
1838}
1839
1840impl SettlementResult {
1841    /// Number of billing positions.
1842    #[must_use]
1843    pub fn positions_count(&self) -> usize {
1844        self.positions.len()
1845    }
1846
1847    /// `true` when the settlement has no warnings at `Warning` or `Error` severity.
1848    #[must_use]
1849    pub fn is_clean(&self) -> bool {
1850        !self
1851            .warnings
1852            .iter()
1853            .any(|w| w.severity >= WarningSeverity::Warning)
1854    }
1855
1856    /// All legal references cited across all positions (deduplicated by citation string).
1857    #[must_use]
1858    pub fn all_legal_refs(&self) -> Vec<String> {
1859        let mut seen = std::collections::HashSet::new();
1860        self.positions
1861            .iter()
1862            .flat_map(|p| p.trace.legal_refs.iter().map(|r| r.citation()))
1863            .filter(|c| seen.insert(c.clone()))
1864            .collect()
1865    }
1866
1867    /// Net total as computed from positions (re-summed for verification).
1868    ///
1869    /// Should equal `total_eur`. A mismatch indicates a calculation bug.
1870    #[must_use]
1871    pub fn recomputed_total(&self) -> Decimal {
1872        self.positions
1873            .iter()
1874            .map(|p| p.net_eur)
1875            .sum::<Decimal>()
1876            .round_kfm(2)
1877    }
1878}
1879
1880// ── Input types ───────────────────────────────────────────────────────────────
1881
1882/// Input for NNE (Netznutzungsentgelt) invoice calculation.
1883///
1884/// Covers:
1885/// - **PID 31002** (NN-Rechnung) — NNE Strom and Gas (NB → LF, monthly network
1886///   usage billing). The Sparte is carried in the message content, not the PID.
1887///
1888/// For **RLM** (Leistungsmessung) meters:
1889/// - Set `spitzenleistung_kw` to the peak demand in kW.
1890/// - Set `leistungspreis_eur_per_kw` to the published tariff.
1891///
1892/// For **SLP** meters:
1893/// - Leave both fields as `None` (Arbeitspreisanteil only).
1894///
1895/// For **§14a Modul 3 zeitvariable NNE** (BNetzA BK8-22/010-A Tenor 3.):
1896/// - Set `arbeitsmenge_ht_kwh` + `arbeitspreis_ht_ct_per_kwh` for Hochlast periods.
1897/// - Set `arbeitsmenge_nt_kwh` + `arbeitspreis_nt_ct_per_kwh` for Niedertarif periods.
1898/// - Leave `arbeitsmenge_kwh` / `arbeitspreis_ct_per_kwh` as the base fallback.
1899///
1900/// For Gas:
1901/// - The `arbeitsmenge_kwh` should already be converted from m³ using
1902///   `brennwert × zustandszahl` before being supplied here.
1903///   (edmd's `MeterBillingPeriod.arbeitsmenge_kwh` carries this converted value.)
1904#[derive(Debug, Clone)]
1905pub struct NneInput {
1906    /// 11-digit Marktlokations-ID.
1907    pub malo_id: String,
1908    /// Invoice sender — Netzbetreiber or Gasnetzbetreiber MP-ID.
1909    pub nb_mp_id: String,
1910    /// Invoice recipient — Lieferant MP-ID.
1911    pub lf_mp_id: String,
1912    /// The delivery period being settled.
1913    pub period: SettlementPeriod,
1914
1915    /// Letztverbrauchergruppe for the network levies (EnFG §§21 ff.).
1916    ///
1917    /// Determines which rate of the §19 StromNEV-, Offshore- and KWKG-Umlage
1918    /// applies at this Entnahmestelle.
1919    pub letztverbrauchergruppe: crate::umlagen::Letztverbrauchergruppe,
1920
1921    /// kWh already consumed at this Entnahmestelle earlier in the same calendar
1922    /// year, for the EnFG 1-GWh boundary.
1923    ///
1924    /// The B′/C′ rates are published for quantities *über* 1 000 000 kWh a year,
1925    /// so a settlement covering one period of that year cannot tell which side
1926    /// of the boundary its quantity falls on without knowing what came before.
1927    /// `None` is read as zero — the start of the year — which puts the first
1928    /// Gigawattstunde on the full rate. That is the direction that over-bills
1929    /// rather than under-bills, and the settlement says so in a warning.
1930    ///
1931    /// Ignored for groups A′ and Befreit, which have no boundary.
1932    pub enfg_jahresvorverbrauch_kwh: Option<Decimal>,
1933
1934    /// §19 StromNEV-Umlage in ct/kWh, overriding the tabled rate.
1935    ///
1936    /// `None` uses the statutory rate for the delivery year and group. Set it
1937    /// where an EnFG decision grants a rate the published schedule does not
1938    /// express.
1939    pub sect19_umlage_ct_per_kwh: Option<Decimal>,
1940    /// Offshore-Netzumlage in ct/kWh, overriding the tabled rate.
1941    pub offshore_umlage_ct_per_kwh: Option<Decimal>,
1942    /// KWKG-Umlage in ct/kWh, overriding the tabled rate.
1943    pub kwkg_umlage_ct_per_kwh: Option<Decimal>,
1944
1945    /// Reactive energy and its Preisblatt terms.
1946    ///
1947    /// `None` = the network does not charge Blindmehrarbeit at this location, or
1948    /// the reactive energy was not metered.
1949    pub blindarbeit: Option<Blindarbeit>,
1950
1951    /// Optional tariff sheet identifier for audit tracing.
1952    ///
1953    /// When set, each position's `trace.tariff_source` references this sheet.
1954    pub tariff_sheet_id: Option<String>,
1955    /// Commodity — drives legal references (StromNEV vs GasNEV) and `SettlementType`.
1956    ///
1957    /// - `Sparte::Strom` (default) → `StromNEV §21` Arbeit, `StromNEV §17` Leistung,
1958    ///   `SettlementType::NneStrom`
1959    /// - `Sparte::Gas` → `GasNEV §14`, `SettlementType::NneGas`
1960    pub sparte: Sparte,
1961
1962    // ── §14a Modul 3 Spotpreis-NNE per-interval dispatch data ────────────────
1963    /// Per-dispatch-interval positions for a spot-linked Netzentgelt.
1964    ///
1965    /// Each entry represents one 15-min interval during which a spot-price-linked
1966    /// NNE rate applies. The caller fetches the EPEX Spot day-ahead price for each
1967    /// interval and applies the formula from `PreisblattNetznutzung.lastvariablePreispositionen`
1968    /// to derive `nne_rate_ct_per_kwh`. `grid-billing` receives pre-calculated rates —
1969    /// it never queries EPEX directly.
1970    ///
1971    /// **Empty (default)** when no spot-linked Netzentgelt applies to this MaLo.
1972    ///
1973    /// Selecting this model excludes every other `ArbeitspreisModell` by
1974    /// construction — the enum holds one at a time.
1975    ///
1976    /// Each interval generates one `InvoicePosition` with
1977    /// `kind = NneArbeitModul3` and `lastvariable_preisposition_json` populated.
1978    #[doc = "Spot-linked Netzentgelt per-interval input data."]
1979    ///
1980    /// One value rather than twelve loose fields: the four shapes are mutually
1981    /// exclusive by construction.
1982    pub arbeitspreis: ArbeitspreisModell,
1983
1984    /// RLM demand charge — peak demand and its rate, or neither.
1985    pub leistungspreis: Option<Leistungspreis>,
1986
1987    /// Gas NNE Grundpreis. `None` for Strom, which has no separate Grundpreis.
1988    pub grundpreis: Option<Grundpreis>,
1989
1990    /// Konzessionsabgabe — rate and customer group together, so the KAV §2
1991    /// ceiling can always be checked.
1992    pub konzessionsabgabe: Option<Konzessionsabgabe>,
1993
1994    /// The Netzebene this metering point takes supply from.
1995    ///
1996    /// Netzentgelte are published per level, so the level is what makes a rate
1997    /// checkable against a price sheet. Recorded on the settlement and in the
1998    /// trace; it does not itself select a rate — this crate is given the rates.
1999    pub netzebene: Option<crate::netzebene::Netzebene>,
2000
2001    /// Annual peak demand in kW, where the metering point has one.
2002    ///
2003    /// Used with the annual energy to record the Benutzungsstundenzahl in the
2004    /// trace. This is the *annual* peak, which is not the same as the peak in
2005    /// the billing period — a monthly settlement carries the annual figure so
2006    /// the utilisation can be checked against the price sheet that priced it.
2007    pub jahreshoechstleistung_kw: Option<Decimal>,
2008
2009    /// Annual energy in kWh, where known.
2010    ///
2011    /// Pairs with `jahreshoechstleistung_kw` for the Benutzungsstundenzahl, and
2012    /// decides whether §17 Abs. 6 permits an Arbeitspreis-only tariff.
2013    pub jahresarbeit_kwh: Option<Decimal>,
2014
2015    /// An agreed §19 Abs. 2 StromNEV individual charge, where one exists.
2016    ///
2017    /// Applied as a reduction over the Arbeits- and Leistungspreis positions,
2018    /// with the statutory Mindestentgelt floor checked against the utilisation
2019    /// data above. The Konzessionsabgabe and the network levies are unaffected —
2020    /// the Netzbetreiber's lost revenue is compensated through the
2021    /// §19 StromNEV-Umlage, billed separately.
2022    pub sect19: Option<crate::sect19::Sect19Vereinbarung>,
2023
2024    /// A booked gas capacity, billed alongside the commodity charge.
2025    ///
2026    /// Gas only; §15 GasNEV. The annual rate is pro-rated over the settlement
2027    /// period by calendar days.
2028    pub gas_kapazitaet: Option<crate::gas::GasKapazitaet>,
2029}
2030
2031// ── SpotpreisInterval ─────────────────────────────────────────────────────
2032
2033/// One dispatch interval for a spot-linked Netzentgelt (not a §14a module).
2034///
2035/// Each interval represents a 15-min period during which the DSO exercised load
2036/// control and the NNE rate is derived from the day-ahead spot price via the
2037/// formula published in `PreisblattNetznutzung.lastvariablePreispositionen`.
2038///
2039/// ## Calculation
2040///
2041/// `Einsatzkosten = menge_kwh × nne_rate_ct_per_kwh / 100`
2042///
2043/// The NB computes one `InvoicePosition` per interval, allowing the LF (and their
2044/// customers) to see the exact tariff breakdown for each dispatch event.
2045///
2046/// ## Caller responsibility
2047///
2048/// The caller (service layer) must:
2049/// 1. Fetch the EPEX Spot day-ahead price for each 15-min interval from `productd`
2050///    or the `PreisblattNetznutzung` formula.
2051/// 2. Apply the formula from `lastvariablePreispositionen` to derive `nne_rate_ct_per_kwh`.
2052/// 3. Fetch `menge_kwh` from `edmd Lastgang` for the interval.
2053///
2054/// `grid-billing` receives pre-calculated rates — it does NOT query EPEX or `edmd`.
2055///
2056/// ## Regulatory basis
2057///
2058/// **Not a §14a module.** BK8-22/010-A defines exactly three — Modul 1 (pauschale
2059/// Reduzierung), Modul 2 (prozentuale Arbeitspreisreduzierung) and Modul 3
2060/// (zeitvariable Netzentgelte in three Tarifstufen) — and none of them is
2061/// spot-linked. See [`ArbeitspreisModell::SpotpreisNetzentgelt`], which states
2062/// the same thing.
2063///
2064/// This models a Netzentgelt whose rate follows the spot price under the
2065/// Netzbetreiber's own `PreisblattNetznutzung` formula: the NNE varies per
2066/// 15-minute interval. The rates arrive already derived — this crate never
2067/// queries a spot market.
2068#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2069pub struct SpotpreisInterval {
2070    /// UTC start of this controlled dispatch interval (ISO-8601).
2071    ///
2072    /// Typically the start of a 15-min settlement slot.
2073    #[serde(with = "time::serde::rfc3339")]
2074    pub period_from: time::OffsetDateTime,
2075    /// UTC end of this controlled dispatch interval (ISO-8601).
2076    ///
2077    /// Typically `period_from + 15 min`.
2078    #[serde(with = "time::serde::rfc3339")]
2079    pub period_to: time::OffsetDateTime,
2080    /// Energy consumption (or reduction) during this interval in kWh.
2081    ///
2082    /// Sourced from `edmd Lastgang` for the MaLo during the interval window.
2083    pub menge_kwh: Decimal,
2084    /// Effective NNE rate in **ct/kWh** for this interval.
2085    ///
2086    /// Derived from the `LastvariablePreisposition` formula applied to the
2087    /// applicable EPEX Spot day-ahead price. Pre-calculated by the caller.
2088    pub nne_rate_ct_per_kwh: Decimal,
2089    /// EPEX Spot day-ahead price in ct/kWh used to derive `nne_rate_ct_per_kwh`.
2090    ///
2091    /// Stored in the `CalculationTrace.explanation` for audit transparency.
2092    /// `None` when the rate was determined by a fixed formula without market reference.
2093    pub epex_spot_ct_per_kwh: Option<Decimal>,
2094}
2095
2096// ── MmmInput ──────────────────────────────────────────────────────────────────
2097
2098/// Input for Mehr-/Mindermengen (MMM) settlement invoice calculation.
2099///
2100/// Covers:
2101/// - **PID 31005** — MMM-Rechnung used for Mehr-/Mindermengen settlement between
2102///   NB and LF (Strom and Gas).
2103///
2104/// Mehr-/Mindermengen settle the difference between the LF's forecast profile
2105/// (SLP standard load profile) and the actual measured consumption.
2106///
2107/// - **Mehrmengen** (positive deviation): actual > profil → LF owes NB
2108/// - **Mindermengen** (negative deviation): actual < profil → NB owes LF
2109///
2110/// The settlement amount is the algebraic sum of both positions.  It can be
2111/// negative (i.e. a credit note from NB to LF) when Mindermengen dominate.
2112#[derive(Debug, Clone)]
2113pub struct MmmInput {
2114    /// 11-digit Marktlokations-ID.
2115    pub malo_id: String,
2116    /// Invoice sender — Netzbetreiber MP-ID.
2117    pub nb_mp_id: String,
2118    /// Invoice recipient — Lieferant MP-ID.
2119    pub lf_mp_id: String,
2120    /// The delivery period being settled.
2121    pub period: SettlementPeriod,
2122    /// Commodity — determines which Festlegung the legal references cite.
2123    ///
2124    /// - `Sparte::Strom` → `GPKE (BK6-24-174) Teil 1 Kap. 8.4`, `GPKE BK6-22-024`
2125    /// - `Sparte::Gas` → `GaBi Gas 2.1 (BK7-24-01-008)`, `GeLi Gas 3.0 (BK7-24-01-009)`
2126    pub sparte: Sparte,
2127    /// Actual measured consumption in kWh (from MSCONS / `MeterBillingPeriod`).
2128    pub actual_kwh: Decimal,
2129    /// Standard load profile (SLP) forecast consumption in kWh.
2130    pub profil_kwh: Decimal,
2131    /// Mehrmengen price in **ct/kWh** (from `PreisblattNetznutzung` MMM position).
2132    pub mehr_preis_ct_per_kwh: Decimal,
2133    /// Mindermengen price in **ct/kWh** (from `PreisblattNetznutzung` MMM position).
2134    pub minder_preis_ct_per_kwh: Decimal,
2135    /// Who holds §3g Wiederverkäufer status, evidenced by a *USt 1 TH*.
2136    ///
2137    /// A Mehr-/Mindermenge is a **Lieferung** of electricity or gas, not a
2138    /// network service, so §13b Abs. 2 Nr. 5 Buchst. b UStG can shift the tax to
2139    /// the recipient. The condition differs by Sparte — electricity needs both
2140    /// parties, gas needs the recipient — which is why this is a status rather
2141    /// than a `reverse_charge: bool` the caller has to reason out.
2142    pub wiederverkaeufer: crate::umsatzsteuer::Wiederverkaeuferstatus,
2143    /// The receiving party issues this invoice itself (Gutschriftverfahren).
2144    ///
2145    /// PID 31006 (Strom) / 31008 (Gas) is the Mehrmenge leg written by the
2146    /// party that would otherwise receive it, which the AHB marks as
2147    /// *Selbstausgestellt* rather than *Handelsrechnung*. That distinction is
2148    /// on the wire (`IMD+7081` and the Rechnungsart), so it has to come from
2149    /// the settlement rather than be stamped on the document afterwards:
2150    /// labelling an ordinary [`SettlementType::MmmStrom`] with PID 31006
2151    /// produces a message that states Handelsrechnung under a
2152    /// Selbstausstellung Prüfidentifikator.
2153    pub selbstausgestellt: bool,
2154}
2155
2156// ── AbschlagInput ─────────────────────────────────────────────────────────────
2157
2158/// Input for an Abschlagsrechnung Netznutzung (PID 31001).
2159///
2160/// A payment on account. There is no metered quantity and no Arbeitspreis: the
2161/// Netzbetreiber asks for an amount against a period it has not settled yet, and
2162/// the Abschlussrechnung that follows deducts it by invoice number.
2163///
2164/// How the amount is arrived at — a share of last year's Turnusrechnung, a
2165/// forecast from the Jahresarbeit, a figure agreed in the
2166/// Lieferantenrahmenvertrag — is the operator's judgement, not arithmetic this
2167/// crate can check. [`Self::grundlage`] records which of them it was, so an
2168/// auditor can see the basis rather than infer it from a bare number.
2169#[derive(Debug, Clone)]
2170pub struct AbschlagInput {
2171    /// 11-digit Marktlokations-ID.
2172    pub malo_id: String,
2173    /// Invoice sender — the Netzbetreiber.
2174    pub nb_mp_id: String,
2175    /// Invoice recipient — the Lieferant.
2176    pub lf_mp_id: String,
2177    /// The period the payment is on account of.
2178    pub period: SettlementPeriod,
2179    /// Commodity.
2180    pub sparte: Sparte,
2181    /// The **net** amount requested, in EUR.
2182    ///
2183    /// Net rather than gross: the tax is stated separately on the invoice, as
2184    /// §14 Abs. 4 Nr. 8 UStG requires of an Anzahlungsrechnung like any other.
2185    pub betrag_netto_eur: Decimal,
2186    /// How the amount was arrived at.
2187    pub grundlage: AbschlagGrundlage,
2188}
2189
2190/// How an Abschlag's amount was arrived at.
2191///
2192/// Recorded rather than computed. The engine cannot check a forecast, but an
2193/// audit can ask which basis was used, and an invoice that answers "a share of
2194/// the prior Turnusrechnung" is defensible where a bare figure is not.
2195#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2196pub enum AbschlagGrundlage {
2197    /// A share of the previous settled period's invoice.
2198    Vorjahresverbrauch,
2199    /// A forecast of the period being paid for.
2200    Prognose,
2201    /// A figure fixed in the Lieferantenrahmenvertrag.
2202    Vereinbarung,
2203}
2204
2205impl AbschlagGrundlage {
2206    /// Short label for the position text and the trace.
2207    #[must_use]
2208    pub const fn label(self) -> &'static str {
2209        match self {
2210            Self::Vorjahresverbrauch => "auf Basis des Vorjahresverbrauchs",
2211            Self::Prognose => "auf Basis einer Verbrauchsprognose",
2212            Self::Vereinbarung => "gemäß Vereinbarung im Lieferantenrahmenvertrag",
2213        }
2214    }
2215}
2216
2217// ── MsbInput ──────────────────────────────────────────────────────────────────
2218
2219/// Input for MSB (Messstellenbetreiber) invoice calculation.
2220///
2221/// Covers:
2222/// - **PID 31009** — MSB-Rechnung (MSB → NB / LF / ESA, monthly metering
2223///   service settlement; Strom only)
2224///
2225/// The NB bills the MSB for the metering service period.  Positions:
2226/// 1. Grundgebühr Messstellenbetrieb — flat monthly base fee × billing months.
2227/// 2. Messdienstleistung — optional per-period measurement service fee.
2228/// 3. Steuerungseinrichtung am Netzanschlusspunkt — optional, §30 Abs. 2 MsbG.
2229#[derive(Debug, Clone)]
2230pub struct MsbInput {
2231    /// 11-digit Marktlokations-ID.
2232    pub malo_id: String,
2233    /// Invoice **sender** — the Messstellenbetreiber.
2234    ///
2235    /// PID 31009 is issued *by* the MSB in all seven of its Anwendungsfälle; it
2236    /// is never sent to one. See [`MsbRechnungsempfaenger`].
2237    pub msb_mp_id: String,
2238    /// Invoice **recipient** — who is billed, and in which market role.
2239    pub empfaenger: MsbRechnungsempfaenger,
2240    /// The delivery period being settled.
2241    pub period: SettlementPeriod,
2242    /// The Sparte of the metering point.
2243    ///
2244    /// §30 MsbG prices metering, not energy, so this changes no arithmetic — but
2245    /// it is what the invoice states, and a service that stores one Sparte on
2246    /// the draft while the settlement carries another cannot answer which is
2247    /// right.
2248    pub sparte: Sparte,
2249    /// Grundgebühr Messstellenbetrieb in **EUR/month** (from `PreisblattMessung`).
2250    pub grundgebuehr_eur_per_month: Decimal,
2251    /// Number of full calendar months in the billing period.
2252    pub billing_months: u32,
2253    /// Optional Messdienstleistung flat fee in **EUR** for the full period.
2254    ///
2255    /// `None` when the MSB provides only the meter, not a separate measurement service.
2256    pub messdienstleistung_eur: Option<Decimal>,
2257
2258    /// Einbau und Betrieb einer Steuerungseinrichtung am Netzanschlusspunkt in
2259    /// **EUR/month** — §30 Abs. 2 MsbG, where the gMSB equipped the metering
2260    /// point under §29 Abs. 1 Nr. 2.
2261    ///
2262    /// It is charged „zusätzlich zu den nach den Absätzen 1 und 5 zulässigen
2263    /// Preisobergrenzen" and carries a ceiling of its own, so it is stated apart
2264    /// from the Grundgebühr: folded into that figure it would consume Abs. 1
2265    /// headroom it does not belong to, and its own 50-EUR cap could not be
2266    /// checked at all.
2267    pub steuereinrichtung_eur_per_month: Option<Decimal>,
2268
2269    /// Which §30 MsbG case this metering point falls under.
2270    ///
2271    /// Fixes the Preisobergrenze the charge is checked against. `None` skips the
2272    /// check, which should be rare: a metering charge above the POG is an amount
2273    /// the customer is entitled to have refunded.
2274    pub messstellen_kategorie: Option<crate::msbg::MessstellenKategorie>,
2275
2276    /// Whose share of the metering charge this settlement bills.
2277    ///
2278    /// §30 MsbG splits the ceiling between the Netzbetreiber and the
2279    /// Letztverbraucher, so the applicable cap depends on who is being billed.
2280    pub entgeltschuldner: Option<crate::msbg::Entgeltschuldner>,
2281}
2282
2283/// Recipient of a MSB-Rechnung (PID 31009).
2284///
2285/// The *Anwendungsübersicht der Prüfidentifikatoren* 4.0 (01.04.2026) lists
2286/// seven Anwendungsfälle for 31009, and the sender is the **MSB** in every one:
2287///
2288/// | Prozessbeschreibung | von | an |
2289/// |---|---|---|
2290/// | GPKE Teil 3 | MSB | NB |
2291/// | GPKE Teil 3 | MSB | LF |
2292/// | WiM Strom Teil 1 | MSB (am Objekt Marktlokation) | LF |
2293/// | WiM Strom Teil 1 | MSB (am Objekt Marktlokation) | NB |
2294/// | WiM Strom Teil 2 | MSB | ESA |
2295/// | AWH Prozesse zur Änderung der Technik an Lokationen | MSB | NB |
2296/// | AWH Prozesse zur Änderung der Technik an Lokationen | MSB | LF |
2297///
2298/// So the recipient varies across three market roles while the sender does not,
2299/// which is why it is modelled as a role plus an MP-ID rather than a bare
2300/// `nb_mp_id`. 31009 is Strom-only — the overview marks Sparte Gas `--`.
2301///
2302/// Distinct from [`crate::msbg::Entgeltschuldner`], which selects the §30 MsbG
2303/// Preisobergrenze (whose *share* of the ceiling applies) rather than who
2304/// receives the invoice. The LF commonly receives an invoice for the
2305/// Letztverbraucher share under the Rechnungsabwicklung über den LF, so the two
2306/// axes do not coincide.
2307#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2308pub struct MsbRechnungsempfaenger {
2309    /// Which market role receives the invoice.
2310    pub rolle: MsbEmpfaengerRolle,
2311    /// The recipient's 13-digit MP-ID.
2312    pub mp_id: String,
2313}
2314
2315/// Market role a MSB-Rechnung (PID 31009) may be addressed to.
2316#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2317pub enum MsbEmpfaengerRolle {
2318    /// Netzbetreiber — GPKE Teil 3, WiM Strom Teil 1, AWH Technikänderung.
2319    Netzbetreiber,
2320    /// Lieferant — GPKE Teil 3, WiM Strom Teil 1, AWH Technikänderung
2321    /// (Rechnungsabwicklung des MSB über den LF).
2322    Lieferant,
2323    /// Energieserviceanbieter — WiM Strom Teil 2.
2324    Energieserviceanbieter,
2325}
2326
2327impl MsbEmpfaengerRolle {
2328    /// BDEW role code as it appears in the NAD segment.
2329    #[must_use]
2330    pub const fn code(self) -> &'static str {
2331        match self {
2332            Self::Netzbetreiber => "NB",
2333            Self::Lieferant => "LF",
2334            Self::Energieserviceanbieter => "ESA",
2335        }
2336    }
2337}
2338
2339// ── GasAwhInput ───────────────────────────────────────────────────────────────
2340
2341/// Input for GeLi Gas AWH Sperrprozesse settlement (PID 31011).
2342///
2343/// **PID 31011 — Rechnung sonstige Leistung (NB → LF)**
2344///
2345/// Bills the Lieferant (LFG/LFA) for abrechnungswürdige Handlungen (AWH)
2346/// performed by the GNB/VNB during the Sperrung/Entsperrung process.
2347/// Governed by BK7-24-01-009 §5.4 (GeLi Gas 3.0).
2348///
2349/// ## What counts as AWH
2350///
2351/// AWH are chargeable actions not included in the network tariff, triggered by
2352/// the LF through the Sperrung process. Typical AWH:
2353/// - `Sperrung` (disconnection)
2354/// - `Entsperrung` (reconnection)
2355/// - `Teilsperrung` (partial disconnection)
2356/// - `Unterbrechung Verfahren` (process interruption)
2357///
2358/// Each action type has a fixed price published in the `PreisblattNetznutzung`.
2359#[derive(Debug, Clone)]
2360pub struct GasAwhInput {
2361    /// 11-digit Marktlokations-ID.
2362    pub malo_id: String,
2363    /// Invoice sender — Gasnetzbetreiber (GNB/VNB) MP-ID.
2364    pub nb_mp_id: String,
2365    /// Invoice recipient — Lieferant Gas (LFG or LFA) MP-ID.
2366    pub lf_mp_id: String,
2367    /// The delivery period being settled.
2368    pub period: SettlementPeriod,
2369    /// Optional tariff sheet identifier for audit tracing.
2370    pub tariff_sheet_id: Option<String>,
2371    /// AWH line items: each chargeable action with count and unit price.
2372    ///
2373    /// At least one position is required.
2374    pub awh_positionen: Vec<AwhPositionInput>,
2375}
2376
2377/// One AWH action line item for [`GasAwhInput`].
2378///
2379/// ## Examples
2380///
2381/// ```rust
2382/// # use grid_billing::AwhPositionInput;
2383/// # use rust_decimal::dec;
2384/// let sperrung = AwhPositionInput {
2385///     beschreibung: "Sperrung Gaszähler".to_owned(),
2386///     anzahl: 1,
2387///     preis_eur: dec!(45.00),
2388///     artikel_id: Some("2-01-7-001".to_owned()),
2389/// };
2390/// ```
2391#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2392pub struct AwhPositionInput {
2393    /// Human-readable action description, e.g. `"Sperrung Gaszähler"`.
2394    pub beschreibung: String,
2395    /// Number of executions of this action.
2396    pub anzahl: u32,
2397    /// Price per execution in **EUR** (from `PreisblattNetznutzung`).
2398    pub preis_eur: Decimal,
2399    /// BDEW Artikel-ID from section 3.2 of the Codeliste Artikelnummern v5.6.
2400    ///
2401    /// Standard values for Gas AWH Sperrprozesse (BK7-24-01-009 §5.4):
2402    /// - `"2-01-7-001"` — Unterbrechung der Anschlussnutzung (reguläre AZ)
2403    /// - `"2-01-7-002"` — Wiederherstellung der Anschlussnutzung (reguläre AZ)
2404    /// - `"2-01-7-003"` — Erfolglose Unterbrechung
2405    /// - `"2-01-7-004"` — Stornierung Unterbrechungsauftrag (bis Vortag)
2406    /// - `"2-01-7-005"` — Stornierung Unterbrechungsauftrag (am Sperrtag)
2407    /// - `"2-01-7-006"` — Wiederherstellung außerhalb regulärer AZ
2408    ///
2409    /// `None` for custom / non-standard AWH positions.
2410    pub artikel_id: Option<String>,
2411}
2412
2413// ── ValidationResult ─────────────────────────────────────────────────────────
2414
2415/// Result of pre-calculation input validation.
2416///
2417/// For NNE there is no separate validator: the invariants that mattered are
2418/// either unrepresentable — an inverted [`SettlementPeriod`], a half-set
2419/// [`Leistungspreis`], two §14a modules at once — or enforced inside
2420/// [`crate::settle_nne`] itself. A validator the engine did not call was how a
2421/// caller who skipped it got billed on the wrong basis with no error.
2422///
2423/// [`validate_mmm_input`], [`validate_msb_input`] and [`validate_gas_awh_input`]
2424/// remain for inputs whose engines accept looser shapes.
2425#[derive(Debug, Clone)]
2426pub struct ValidationResult {
2427    /// Whether the input passed all validation checks.
2428    pub is_valid: bool,
2429    /// All warnings and errors found. May contain [`WarningSeverity::Info`] items
2430    /// even when `is_valid = true`.
2431    pub warnings: Vec<SettlementWarning>,
2432}
2433
2434impl ValidationResult {
2435    /// Returns a clean (valid, no warnings) result.
2436    #[must_use]
2437    pub fn ok() -> Self {
2438        Self {
2439            is_valid: true,
2440            warnings: Vec::new(),
2441        }
2442    }
2443
2444    /// Appends a warning. `WarningSeverity::Error` marks the result invalid.
2445    pub fn push(&mut self, w: SettlementWarning) {
2446        if w.severity == WarningSeverity::Error {
2447            self.is_valid = false;
2448        }
2449        self.warnings.push(w);
2450    }
2451}
2452
2453/// Validate a [`MmmInput`] before calling [`crate::settle_mmm`].
2454#[must_use]
2455pub fn validate_mmm_input(input: &MmmInput) -> ValidationResult {
2456    let mut r = ValidationResult::ok();
2457    if input.period.from() >= input.period.to() {
2458        r.push(SettlementWarning {
2459            severity: WarningSeverity::Error,
2460            code: "INVALID_PERIOD",
2461            message: "period_from must be strictly before period_to".to_owned(),
2462        });
2463    }
2464    if input.mehr_preis_ct_per_kwh < Decimal::ZERO {
2465        r.push(SettlementWarning {
2466            severity: WarningSeverity::Warning,
2467            code: "NEGATIVE_MEHR_PREIS",
2468            message: format!(
2469                "mehr_preis_ct_per_kwh is negative: {}",
2470                input.mehr_preis_ct_per_kwh
2471            ),
2472        });
2473    }
2474    if input.minder_preis_ct_per_kwh < Decimal::ZERO {
2475        r.push(SettlementWarning {
2476            severity: WarningSeverity::Warning,
2477            code: "NEGATIVE_MINDER_PREIS",
2478            message: format!(
2479                "minder_preis_ct_per_kwh is negative: {}",
2480                input.minder_preis_ct_per_kwh
2481            ),
2482        });
2483    }
2484    r
2485}
2486
2487/// Validate a [`MsbInput`] before calling [`crate::settle_msb`].
2488#[must_use]
2489pub fn validate_msb_input(input: &MsbInput) -> ValidationResult {
2490    let mut r = ValidationResult::ok();
2491    if input.period.from() >= input.period.to() {
2492        r.push(SettlementWarning {
2493            severity: WarningSeverity::Error,
2494            code: "INVALID_PERIOD",
2495            message: "period_from must be strictly before period_to".to_owned(),
2496        });
2497    }
2498    if input.grundgebuehr_eur_per_month < Decimal::ZERO {
2499        r.push(SettlementWarning {
2500            severity: WarningSeverity::Error,
2501            code: "NEGATIVE_GRUNDGEBUEHR",
2502            message: format!(
2503                "grundgebuehr_eur_per_month is negative: {}",
2504                input.grundgebuehr_eur_per_month
2505            ),
2506        });
2507    }
2508    if input.billing_months == 0 {
2509        r.push(SettlementWarning {
2510            severity: WarningSeverity::Error,
2511            code: "ZERO_BILLING_MONTHS",
2512            message: "billing_months must be at least 1".to_owned(),
2513        });
2514    }
2515    r
2516}
2517
2518/// Validate a [`GasAwhInput`] before calling [`crate::settle_gas_awh`].
2519///
2520/// Checks that:
2521/// - `period_from < period_to`
2522/// - `awh_positionen` is non-empty
2523/// - All positions have `anzahl ≥ 1` and `preis_eur ≥ 0`
2524#[must_use]
2525pub fn validate_gas_awh_input(input: &GasAwhInput) -> ValidationResult {
2526    let mut r = ValidationResult::ok();
2527    if input.period.from() >= input.period.to() {
2528        r.push(SettlementWarning {
2529            severity: WarningSeverity::Error,
2530            code: "INVALID_PERIOD",
2531            message: "period_from must be strictly before period_to".to_owned(),
2532        });
2533    }
2534    if input.awh_positionen.is_empty() {
2535        r.push(SettlementWarning {
2536            severity: WarningSeverity::Error,
2537            code: "EMPTY_AWH_POSITIONEN",
2538            message: "awh_positionen must contain at least one position".to_owned(),
2539        });
2540    }
2541    for (i, awh) in input.awh_positionen.iter().enumerate() {
2542        if awh.anzahl == 0 {
2543            r.push(SettlementWarning {
2544                severity: WarningSeverity::Error,
2545                code: "ZERO_AWH_ANZAHL",
2546                message: format!("awh_positionen[{i}].anzahl must be ≥ 1"),
2547            });
2548        }
2549        if awh.preis_eur < Decimal::ZERO {
2550            r.push(SettlementWarning {
2551                severity: WarningSeverity::Error,
2552                code: "NEGATIVE_AWH_PREIS",
2553                message: format!(
2554                    "awh_positionen[{i}].preis_eur must be non-negative, got {}",
2555                    awh.preis_eur
2556                ),
2557            });
2558        }
2559    }
2560    r
2561}
2562
2563#[cfg(test)]
2564mod input_model_tests {
2565
2566    /// A factor arriving over the wire is range-checked, not merely parsed.
2567    ///
2568    /// The whole point of the newtype is that an out-of-range value cannot
2569    /// exist; a derived `Deserialize` would have let one in through a request
2570    /// body and multiplied the Arbeitspreis by it.
2571    #[test]
2572    fn a_wire_reduktionsfaktor_is_range_checked() {
2573        // A `Decimal` is a JSON string on the wire, so the factor is too — a
2574        // float cannot carry 0.85 exactly and this one multiplies a tariff.
2575        let ok: Reduktionsfaktor = serde_json::from_str(r#""0.85""#).expect("in range");
2576        assert_eq!(ok.get(), dec!(0.85));
2577
2578        for bad in [r#""0""#, r#""-0.5""#, r#""1.01""#, r#""5""#] {
2579            assert!(
2580                serde_json::from_str::<Reduktionsfaktor>(bad).is_err(),
2581                "{bad} must be refused"
2582            );
2583        }
2584        // The boundary is inclusive at 1 — no reduction is still a valid factor.
2585        assert!(serde_json::from_str::<Reduktionsfaktor>(r#""1""#).is_ok());
2586        // A bare number is refused before the range is even considered.
2587        assert!(serde_json::from_str::<Reduktionsfaktor>("0.85").is_err());
2588    }
2589
2590    /// The Arbeitspreis model round-trips, so a settlement input can be stored
2591    /// and recomputed rather than a rendered document being edited in place.
2592    #[test]
2593    fn the_arbeitspreis_model_round_trips() {
2594        let model = ArbeitspreisModell::Modul3ZeitVariabel {
2595            ht: MengePreis {
2596                menge_kwh: dec!(600),
2597                preis_ct_per_kwh: dec!(4.2),
2598            },
2599            st: MengePreis {
2600                menge_kwh: dec!(100),
2601                preis_ct_per_kwh: dec!(3.0),
2602            },
2603            nt: MengePreis {
2604                menge_kwh: dec!(400),
2605                preis_ct_per_kwh: dec!(1.5),
2606            },
2607        };
2608        let json = serde_json::to_string(&model).expect("serialize");
2609        let back: ArbeitspreisModell = serde_json::from_str(&json).expect("deserialize");
2610        assert_eq!(back, model);
2611    }
2612
2613    use super::*;
2614    use rust_decimal::dec;
2615
2616    /// A reduction factor outside `(0, 1]` cannot be built.
2617    ///
2618    /// The type is the check. A bare `Decimal` range-checked in a validator
2619    /// leaves `settle_nne` free to multiply the published tariff by 5 whenever
2620    /// the engine does not call it.
2621    #[test]
2622    fn a_reduction_factor_must_actually_reduce() {
2623        assert!(Reduktionsfaktor::new(dec!(0.85)).is_ok());
2624        assert!(
2625            Reduktionsfaktor::new(dec!(1)).is_ok(),
2626            "no reduction is still valid"
2627        );
2628        assert!(
2629            Reduktionsfaktor::new(dec!(0)).is_err(),
2630            "zero is not a reduction"
2631        );
2632        assert!(Reduktionsfaktor::new(dec!(-0.5)).is_err());
2633        assert!(
2634            Reduktionsfaktor::new(dec!(5)).is_err(),
2635            "5x is not a reduction"
2636        );
2637        assert_eq!(Reduktionsfaktor::REGELFALL.get(), dec!(0.40));
2638    }
2639
2640    /// The charged energy is the same figure whichever model priced it.
2641    ///
2642    /// The Konzessionsabgabe and the three network levies are all charged on it,
2643    /// so they read one figure rather than each deriving its own.
2644    #[test]
2645    fn every_model_reports_the_energy_it_priced() {
2646        let flat = ArbeitspreisModell::Einheitlich(MengePreis {
2647            menge_kwh: dec!(1000),
2648            preis_ct_per_kwh: dec!(3.5),
2649        });
2650        assert_eq!(flat.menge_kwh(), dec!(1000));
2651
2652        let tou = ArbeitspreisModell::Modul3ZeitVariabel {
2653            ht: MengePreis {
2654                menge_kwh: dec!(600),
2655                preis_ct_per_kwh: dec!(4.0),
2656            },
2657            st: MengePreis {
2658                menge_kwh: dec!(0),
2659                preis_ct_per_kwh: dec!(0),
2660            },
2661            nt: MengePreis {
2662                menge_kwh: dec!(400),
2663                preis_ct_per_kwh: dec!(1.5),
2664            },
2665        };
2666        assert_eq!(tou.menge_kwh(), dec!(1000), "HT + NT, not one of them");
2667
2668        let modul1 = ArbeitspreisModell::Modul1Pauschal {
2669            basis: MengePreis {
2670                menge_kwh: dec!(1000),
2671                preis_ct_per_kwh: dec!(3.5),
2672            },
2673            pauschale_eur_pro_jahr: dec!(120),
2674            jahresanteil: Jahresanteil::MONAT,
2675        };
2676        assert_eq!(
2677            modul1.menge_kwh(),
2678            dec!(1000),
2679            "the reduction changes the rate, not the energy"
2680        );
2681    }
2682
2683    /// Each model names its §14a module, and only one can be in play.
2684    #[test]
2685    fn a_model_carries_at_most_one_sect14a_module() {
2686        use Sect14aModule as M;
2687        let cases = [
2688            (
2689                ArbeitspreisModell::Einheitlich(MengePreis {
2690                    menge_kwh: dec!(1),
2691                    preis_ct_per_kwh: dec!(1),
2692                }),
2693                None,
2694            ),
2695            (
2696                ArbeitspreisModell::Modul1Pauschal {
2697                    basis: MengePreis {
2698                        menge_kwh: dec!(1),
2699                        preis_ct_per_kwh: dec!(1),
2700                    },
2701                    pauschale_eur_pro_jahr: dec!(120),
2702                    jahresanteil: Jahresanteil::MONAT,
2703                },
2704                Some(M::Modul1),
2705            ),
2706            (
2707                ArbeitspreisModell::Modul3ZeitVariabel {
2708                    ht: MengePreis {
2709                        menge_kwh: dec!(1),
2710                        preis_ct_per_kwh: dec!(1),
2711                    },
2712                    st: MengePreis {
2713                        menge_kwh: dec!(0),
2714                        preis_ct_per_kwh: dec!(0),
2715                    },
2716                    nt: MengePreis {
2717                        menge_kwh: dec!(1),
2718                        preis_ct_per_kwh: dec!(1),
2719                    },
2720                },
2721                Some(M::Modul3),
2722            ),
2723            (
2724                // Not a §14a module at all — BK8-22/010-A defines exactly three,
2725                // none of them spot-linked.
2726                ArbeitspreisModell::SpotpreisNetzentgelt { intervalle: vec![] },
2727                None,
2728            ),
2729        ];
2730        for (model, expected) in cases {
2731            assert_eq!(model.sect14a_modul(), expected);
2732        }
2733    }
2734
2735    /// BK8-22/010-A numbers the three modules in a specific way, and this project
2736    /// had them shuffled: the time-variable model was labelled Modul 2 and a
2737    /// spot-linked Netzentgelt was labelled Modul 3.
2738    ///
2739    /// Getting this wrong prints the wrong statutory module on a real invoice
2740    /// and makes the LF-side and NB-side engines disagree about the same
2741    /// connection, so it is pinned here rather than left to a doc comment.
2742    #[test]
2743    fn the_modules_are_numbered_as_bk6_22_300_defines_them() {
2744        use Sect14aModule as M;
2745        assert!(M::Modul1.label().contains("pauschale Reduzierung"));
2746        assert!(
2747            M::Modul2
2748                .label()
2749                .contains("prozentuale Arbeitspreisreduzierung")
2750        );
2751        assert!(M::Modul3.label().contains("zeitvariable Netzentgelte"));
2752    }
2753
2754    /// `Modul 1 + Modul 3` is the only pair BK8-22/010-A offers.
2755    ///
2756    /// Modul 1 and Modul 2 are the two forms of the *base* module and the
2757    /// Anschlussnutzer picks one; Modul 3 adds to the pauschale Modul 1 and not
2758    /// to Modul 2, which re-prices the same Arbeitspreis.
2759    #[test]
2760    fn modul_1_and_modul_3_are_the_only_combination() {
2761        use Sect14aModule as M;
2762        assert!(M::Modul1.combinable_with(M::Modul3));
2763        assert!(M::Modul3.combinable_with(M::Modul1));
2764
2765        assert!(!M::Modul2.combinable_with(M::Modul3));
2766        assert!(!M::Modul3.combinable_with(M::Modul2));
2767        assert!(
2768            !M::Modul1.combinable_with(M::Modul2),
2769            "Modul 2 is an alternative to Modul 1, not an addition to it"
2770        );
2771        assert!(!M::Modul2.combinable_with(M::Modul1));
2772
2773        for m in [M::Modul1, M::Modul2, M::Modul3] {
2774            assert!(!m.combinable_with(m), "{m:?} with itself is not a pair");
2775        }
2776    }
2777
2778    /// A period is ordered by construction; a single day is valid.
2779    #[test]
2780    fn a_period_cannot_be_inverted() {
2781        use time::macros::date;
2782        assert!(SettlementPeriod::new(date!(2026 - 01 - 31), date!(2026 - 01 - 01)).is_err());
2783        let one_day = SettlementPeriod::new(date!(2026 - 01 - 01), date!(2026 - 01 - 01))
2784            .expect("a single day is a period");
2785        assert_eq!(one_day.days(), 1);
2786        let january =
2787            SettlementPeriod::new(date!(2026 - 01 - 01), date!(2026 - 01 - 31)).expect("valid");
2788        assert_eq!(january.days(), 31, "both bounds are inclusive");
2789    }
2790}
2791
2792#[cfg(test)]
2793mod korrektur_grund_tests {
2794    use super::*;
2795
2796    /// A correction that cannot say why it happened is not an audit trail.
2797    ///
2798    /// The invoice numbers answer *what* was replaced; only the reason
2799    /// distinguishes a lawful retroactive recalculation from a defect in the
2800    /// original settlement, and those have different consequences.
2801    #[test]
2802    fn a_reason_is_required_for_every_recalculation() {
2803        let mut r = sample_result(SettlementStatus::Initial, None);
2804        assert!(
2805            r.lineage_is_consistent(),
2806            "an initial settlement corrects nothing"
2807        );
2808
2809        r.status = SettlementStatus::Correction;
2810        assert!(
2811            !r.lineage_is_consistent(),
2812            "a correction with no reason must be detectable"
2813        );
2814
2815        r.korrektur_grund = Some(KorrekturGrund::Tarifkorrektur);
2816        assert!(r.lineage_is_consistent());
2817    }
2818
2819    /// An initial settlement carrying a correction reason is equally inconsistent.
2820    #[test]
2821    fn an_initial_settlement_carries_no_reason() {
2822        let r = sample_result(
2823            SettlementStatus::Initial,
2824            Some(KorrekturGrund::Rechenfehler),
2825        );
2826        assert!(!r.lineage_is_consistent());
2827    }
2828
2829    /// Separating defects from lawful recalculations is the point of the field:
2830    /// a rising Rechenfehler count is an engineering signal, a rising
2831    /// RegulatorischeAenderung count is not.
2832    #[test]
2833    fn only_some_reasons_indicate_a_defect() {
2834        assert!(KorrekturGrund::Rechenfehler.indicates_defect());
2835        assert!(KorrekturGrund::Stammdatenkorrektur.indicates_defect());
2836        assert!(!KorrekturGrund::RegulatorischeAenderung.indicates_defect());
2837        assert!(!KorrekturGrund::Messwertkorrektur.indicates_defect());
2838        assert!(!KorrekturGrund::Clearing.indicates_defect());
2839    }
2840
2841    /// The codes are stable — they reach reporting and structured records.
2842    #[test]
2843    fn the_codes_are_stable() {
2844        assert_eq!(
2845            KorrekturGrund::Messwertkorrektur.code(),
2846            "MESSWERTKORREKTUR"
2847        );
2848        assert_eq!(
2849            KorrekturGrund::RegulatorischeAenderung.code(),
2850            "REGULATORISCHE_AENDERUNG"
2851        );
2852    }
2853
2854    fn sample_result(
2855        status: SettlementStatus,
2856        korrektur_grund: Option<KorrekturGrund>,
2857    ) -> SettlementResult {
2858        SettlementResult {
2859            settlement_type: SettlementType::NneStrom,
2860            status,
2861            korrektur_grund,
2862            period: SettlementPeriod::new(
2863                time::macros::date!(2026 - 01 - 01),
2864                time::macros::date!(2026 - 01 - 31),
2865            )
2866            .expect("valid period"),
2867            regime: crate::regulatory::RegulatoryRegime::for_period(
2868                time::macros::date!(2026 - 01 - 01),
2869                time::macros::date!(2026 - 01 - 31),
2870            ),
2871            sparte: Sparte::Strom,
2872            malo_id: "51238696012".to_owned(),
2873            sender_mp_id: "9900000000001".to_owned(),
2874            recipient_mp_id: "9900000000002".to_owned(),
2875            positions: Vec::new(),
2876            total_eur: rust_decimal::Decimal::ZERO,
2877            steuer: crate::umsatzsteuer::Steuerausweis {
2878                kategorie: crate::umsatzsteuer::TaxCategory::Standard,
2879                satz_prozent: crate::umsatzsteuer::REGELSTEUERSATZ,
2880                bemessungsgrundlage_eur: rust_decimal::Decimal::ZERO,
2881                steuer_eur: rust_decimal::Decimal::ZERO,
2882                hinweis: None,
2883                rechtsgrundlage: "§12 Abs. 1 UStG",
2884            },
2885            warnings: Vec::new(),
2886        }
2887    }
2888}
2889
2890#[cfg(test)]
2891mod position_kind_tests {
2892    use super::BillingPositionKind as K;
2893
2894    /// `K::ALL` really is every variant.
2895    ///
2896    /// The `const fn` it calls matches without a wildcard, so a new kind that is
2897    /// not listed in `ALL` breaks this file's build — which is what keeps every
2898    /// guard walking `ALL` honest.
2899    #[test]
2900    fn the_kind_catalogue_is_complete() {
2901        assert!(K::ALL.iter().all(|k| k.is_exhaustive()));
2902        for (i, a) in K::ALL.iter().enumerate() {
2903            assert!(!K::ALL[i + 1..].contains(a), "ALL lists {a:?} twice");
2904        }
2905    }
2906}
2907
2908#[cfg(test)]
2909mod blindarbeit_tests {
2910    use super::*;
2911    use rust_decimal::dec;
2912
2913    /// cos φ 0,9 is the customary boundary: reactive energy up to tan φ ≈ 0,4843
2914    /// of the active energy travels with it and is not charged.
2915    #[test]
2916    fn draw_inside_the_free_share_costs_nothing() {
2917        let b = Blindarbeit {
2918            blindarbeit_kvarh: dec!(400),
2919            freigrenze_anteil: Blindarbeit::COS_PHI_0_9,
2920            preis_ct_per_kvarh: dec!(2.0),
2921        };
2922        // 1 000 kWh × 0,4843 = 484,3 kvarh free; 400 stays inside it.
2923        assert_eq!(b.mehrarbeit_kvarh(dec!(1000)), Decimal::ZERO);
2924    }
2925
2926    /// Only the excess is chargeable.
2927    #[test]
2928    fn only_the_excess_is_charged() {
2929        let b = Blindarbeit {
2930            blindarbeit_kvarh: dec!(600),
2931            freigrenze_anteil: Blindarbeit::COS_PHI_0_9,
2932            preis_ct_per_kvarh: dec!(2.0),
2933        };
2934        assert_eq!(b.mehrarbeit_kvarh(dec!(1000)), dec!(115.7));
2935    }
2936
2937    /// An unused allowance is not a credit — the excess floors at zero.
2938    #[test]
2939    fn an_unused_allowance_is_never_negative() {
2940        let b = Blindarbeit {
2941            blindarbeit_kvarh: dec!(10),
2942            freigrenze_anteil: Blindarbeit::COS_PHI_0_9,
2943            preis_ct_per_kvarh: dec!(2.0),
2944        };
2945        assert_eq!(b.mehrarbeit_kvarh(dec!(5000)), Decimal::ZERO);
2946    }
2947
2948    /// The share is a term of the Preisblatt, not a constant: many networks
2949    /// round cos φ 0,9 to a flat 50 %, and billing them at 0,4843 overcharges.
2950    #[test]
2951    fn the_free_share_follows_the_preisblatt() {
2952        let rounded = Blindarbeit {
2953            blindarbeit_kvarh: dec!(600),
2954            freigrenze_anteil: dec!(0.5),
2955            preis_ct_per_kvarh: dec!(2.0),
2956        };
2957        assert_eq!(rounded.mehrarbeit_kvarh(dec!(1000)), dec!(100.0));
2958
2959        let exact = Blindarbeit {
2960            freigrenze_anteil: Blindarbeit::COS_PHI_0_9,
2961            ..rounded
2962        };
2963        assert!(
2964            exact.mehrarbeit_kvarh(dec!(1000)) > rounded.mehrarbeit_kvarh(dec!(1000)),
2965            "the tighter cos φ 0,9 share charges more than a rounded 50 %"
2966        );
2967    }
2968
2969    /// Blindmehrarbeit rests on the Netzbetreiber's Preisblatt under StromNEV
2970    /// §17 — not §18 (dezentrale Erzeugung) and not §19 (Sonderformen).
2971    #[test]
2972    fn the_position_kind_maps_to_the_bdew_artikelnummer() {
2973        assert_eq!(
2974            BillingPositionKind::Blindmehrarbeit.artikelnummer(SettlementType::NneStrom),
2975            Some("BLINDMEHRARBEIT")
2976        );
2977    }
2978}