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