grid-billing 0.20.0

Role-neutral German grid invoice calculation — NNE, Konzessionsabgabe, Mehr-/Mindermengen and Messstellenbetrieb. Zero I/O, no float money.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
//! Preisobergrenzen für den Messstellenbetrieb — §30 MsbG.
//!
//! What a Messstellenbetreiber may charge for an intelligentes Messsystem is
//! capped, and the cap is split: part falls to the Netzbetreiber, the remainder
//! to the Letztverbraucher. The bands are set by annual consumption **or** by
//! installed generating capacity, whichever puts the metering point in the
//! higher band.
//!
//! ## Every ceiling here is brutto
//!
//! §30 states each figure as a gross amount — „nicht mehr als 80 Euro **brutto**
//! jährlich" (Abs. 1), „jeweils nicht mehr als 50 Euro **brutto** jährlich"
//! (Abs. 2), „**brutto** jährlich nicht mehr als 60 Euro" (Abs. 3). A charge
//! measured against them has to carry its Umsatzsteuer first: comparing a net
//! charge against a gross ceiling grants the whole tax rate as headroom, so a
//! settlement 18.9 % above the statutory maximum passes at 19 %.
//!
//! ## The schedule is dated, and Abs. 4 decides the figure it is read with
//!
//! Abs. 1 and Abs. 3 state their ceilings „für die Zeit ab dem 1. Januar 2025",
//! so a settlement for an earlier period is not measured against them:
//! [`preisobergrenze_eur_per_jahr`] takes the period's end and answers
//! [`Preisobergrenze::VorSchedule`] below [`SCHEDULE_AB`]. Abs. 6 lets a BNetzA
//! Festlegung nach § 33 replace Abs. 1 bis 3 from a date it names; none has
//! issued, so there is nothing to model yet.
//!
//! Abs. 4 fixes **which** Jahresstromverbrauch picks the band: the average of
//! the last three erfasste Jahresverbrauchswerte, falling back to the
//! Netzbetreiber's Jahresverbrauchsprognose until three exist, re-checked once a
//! year. That figure is the caller's — one year's reading is not it.
//!
//! ## The band is derived, never asserted
//!
//! §30 Abs. 1 states five Nummern, each a disjunction of criteria over facts the
//! settlement already knows about the metering point. [`PflichtEinstufung`]
//! carries those facts and [`PflichtEinstufung::band`] walks the Nummern, so the
//! ceiling a charge is measured against follows from the metering point rather
//! than from whichever band the caller named. A settlement request cannot pick
//! its own ceiling.
//!
//! ## Why this is checked rather than assumed
//!
//! These are Höchstbeträge in the same sense as the KAV §2 ceilings, and the
//! crate already refuses to let a Konzessionsabgabe exceed its ceiling silently.
//! A metering charge above the POG is the same class of defect — an amount the
//! customer is entitled to have refunded — so the settlement checks the ceiling
//! and not merely that the fee is non-negative.

use rust_decimal::Decimal;
use rust_decimal::dec;

/// Which §30 MsbG case a metering point falls under.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MessstellenKategorie {
    /// **§30 Abs. 1** — Pflichteinbaufall, described by the facts that classify it.
    Pflichteinbau(PflichtEinstufung),
    /// **§30 Abs. 3** — optionaler Einbau, at the Anschlussnutzer's request.
    ///
    /// A single ceiling regardless of consumption.
    OptionalerEinbau,
}

/// The facts §30 Abs. 1 classifies a Pflichteinbau metering point by.
///
/// Every field is optional because a metering point need not exhibit every
/// criterion: a Letztverbraucher has a Jahresverbrauch and no installierte
/// Leistung, an Erzeugungsanlage the other way round, and a §14a
/// Vereinbarung is a fact about the Zählpunkt on its own.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct PflichtEinstufung {
    /// Jahresstromverbrauch in kWh at this Abnahmestelle.
    #[serde(default)]
    pub jahresverbrauch_kwh: Option<Decimal>,
    /// Installierte Leistung in kW of the Anlage at this Zählpunkt.
    #[serde(default)]
    pub installierte_leistung_kw: Option<Decimal>,
    /// A §14a EnWG Vereinbarung exists over a steuerbare Verbrauchseinrichtung
    /// at this Zählpunkt — a Nummer-4 criterion in its own right.
    #[serde(default)]
    pub steuerbare_verbrauchseinrichtung: bool,
}

impl PflichtEinstufung {
    /// The §30 Abs. 1 band these facts put the metering point in.
    ///
    /// The Nummern are walked from the top down, so a metering point that meets
    /// several takes the highest — which is what "**oder**" inside each Nummer
    /// and the descending order of the catalogue together mean.
    ///
    /// With no fact at all the result is [`PflichtBand::Bis10000`], the tightest
    /// ceiling §30 Abs. 1 sets. A Pflichteinbaufall exists at all only above
    /// 6 000 kWh (§29 Abs. 1), so that is the floor of the catalogue rather than
    /// a guess, and an unclassifiable point is measured against the strictest
    /// cap rather than escaping the check.
    #[must_use]
    pub fn band(&self) -> PflichtBand {
        let kwh = self.jahresverbrauch_kwh.unwrap_or(Decimal::ZERO);
        let kw = self.installierte_leistung_kw.unwrap_or(Decimal::ZERO);

        // Nr. 1 — > 100 000 kWh oder > 100 kW.
        if kwh > dec!(100_000) || kw > dec!(100) {
            PflichtBand::Ueber100000
        // Nr. 2 — > 50 000 bis einschließlich 100 000 kWh oder > 25 bis einschließlich 100 kW.
        } else if kwh > dec!(50_000) || kw > dec!(25) {
            PflichtBand::Bis100000
        // Nr. 3 — > 20 000 bis einschließlich 50 000 kWh oder > 15 bis einschließlich 25 kW.
        } else if kwh > dec!(20_000) || kw > dec!(15) {
            PflichtBand::Bis50000
        // Nr. 4 — > 10 000 bis einschließlich 20 000 kWh, eine steuerbare
        // Verbrauchseinrichtung mit §14a-Vereinbarung, oder > 7 bis
        // einschließlich 15 kW.
        } else if kwh > dec!(10_000) || kw > dec!(7) || self.steuerbare_verbrauchseinrichtung {
            PflichtBand::Bis20000
        // Nr. 5 — > 6 000 bis einschließlich 10 000 kWh.
        } else {
            PflichtBand::Bis10000
        }
    }
}

/// The §30 Abs. 1 bands.
///
/// `Ueber100000` has no fixed total: §30 Abs. 1 allows an "angemessenes
/// jährliches Entgelt", so only the Netzbetreiber's share is capped.
///
/// Reached through [`PflichtEinstufung::band`] rather than named directly, so a
/// band is always the consequence of the metering point's facts.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum PflichtBand {
    /// Nr. 5 — > 6 000 – ≤ 10 000 kWh.
    Bis10000,
    /// Nr. 4 — > 10 000 – ≤ 20 000 kWh, a steuerbare Verbrauchseinrichtung, or > 7 – ≤ 15 kW.
    Bis20000,
    /// Nr. 3 — > 20 000 – ≤ 50 000 kWh, or > 15 – ≤ 25 kW.
    Bis50000,
    /// Nr. 2 — > 50 000 – ≤ 100 000 kWh, or > 25 – ≤ 100 kW.
    Bis100000,
    /// Nr. 1 — > 100 000 kWh or > 100 kW.
    Ueber100000,
}

/// Who owes the charge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Entgeltschuldner {
    /// The Netzbetreiber's share.
    Netzbetreiber,
    /// The Letztverbraucher's share.
    Letztverbraucher,
}

/// The first day the §30 schedule below governs.
///
/// Abs. 1 and Abs. 3 both open „für die Zeit ab dem 1. Januar 2025", so the
/// figures are dated and a settlement for an earlier period is not measured
/// against them. The schedule that applied before is not in mako's regulatory
/// corpus; [`Preisobergrenze::VorSchedule`] says so rather than reaching for
/// the nearest number.
pub const SCHEDULE_AB: time::Date = time::macros::date!(2025 - 01 - 01);

/// What §30 MsbG has to say about one party's share.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Preisobergrenze {
    /// A fixed ceiling in EUR **brutto** per year.
    ///
    /// Gross, as §30 states it: a net charge is grossed up at the Umsatzsteuer
    /// rate of its delivery period before the comparison.
    Betrag(Decimal),
    /// §30 Abs. 1 Nr. 1 — „ein angemessenes jährliches Entgelt". There is no
    /// figure to compare against for the Anschlussnutzer's share above
    /// 100 000 kWh; the Netzbetreiber's share is still capped.
    Angemessen,
    /// The delivery period ends before [`SCHEDULE_AB`], so this schedule does
    /// not govern it and mako holds no earlier one.
    VorSchedule,
}

impl Preisobergrenze {
    /// The ceiling as a number, where there is one to compare against.
    #[must_use]
    pub const fn betrag(self) -> Option<Decimal> {
        match self {
            Self::Betrag(eur) => Some(eur),
            Self::Angemessen | Self::VorSchedule => None,
        }
    }
}

/// The §30 MsbG ceiling for one party over a delivery period ending `period_to`.
///
/// The date decides whether the schedule applies at all: a correction settled
/// today for a 2024 period is governed by the rules of 2024, not by these.
///
/// **§30 Abs. 6 is not modelled**, because nothing to model has issued: from the
/// date a BNetzA Festlegung nach §33 names, its Preisobergrenzen replace Abs. 1
/// bis 3. Wire the override when one publishes.
#[must_use]
pub fn preisobergrenze_eur_per_jahr(
    kategorie: MessstellenKategorie,
    schuldner: Entgeltschuldner,
    period_to: time::Date,
) -> Preisobergrenze {
    use Entgeltschuldner as E;
    use MessstellenKategorie as K;
    use PflichtBand as B;
    use Preisobergrenze as P;

    if period_to < SCHEDULE_AB {
        return P::VorSchedule;
    }

    match (kategorie, schuldner) {
        // §30 Abs. 1: the Netzbetreiber's share is 80 EUR in every band.
        (K::Pflichteinbau(_), E::Netzbetreiber) => P::Betrag(dec!(80)),
        (K::Pflichteinbau(einstufung), E::Letztverbraucher) => match einstufung.band() {
            B::Bis10000 => P::Betrag(dec!(40)),
            B::Bis20000 => P::Betrag(dec!(50)),
            B::Bis50000 => P::Betrag(dec!(110)),
            B::Bis100000 => P::Betrag(dec!(140)),
            B::Ueber100000 => P::Angemessen,
        },
        // §30 Abs. 3: 60 EUR in total, 30 EUR each.
        (K::OptionalerEinbau, _) => P::Betrag(dec!(30)),
    }
}

/// The combined §30 Abs. 1 ceiling across both parties, where one is fixed.
#[must_use]
pub fn gesamtobergrenze_eur_per_jahr(
    kategorie: MessstellenKategorie,
    period_to: time::Date,
) -> Option<Decimal> {
    let nb = preisobergrenze_eur_per_jahr(kategorie, Entgeltschuldner::Netzbetreiber, period_to)
        .betrag()?;
    let lv = preisobergrenze_eur_per_jahr(kategorie, Entgeltschuldner::Letztverbraucher, period_to)
        .betrag()?;
    Some(nb + lv)
}

/// **§30 Abs. 2** — the additional yearly ceiling per party, brutto, for
/// installing and operating a Steuereinrichtung at the Netzanschlusspunkt.
pub const STEUEREINRICHTUNG_OBERGRENZE_EUR_PER_JAHR: Decimal = dec!(50);

#[cfg(test)]
mod tests {
    use super::*;
    use Entgeltschuldner as E;
    use MessstellenKategorie as K;
    use PflichtBand as B;

    /// A delivery period inside the schedule's own window.
    const IM_SCHEDULE: time::Date = time::macros::date!(2026 - 12 - 31);

    /// A Pflichteinbau point known only by its Jahresverbrauch.
    fn verbrauch(kwh: Decimal) -> MessstellenKategorie {
        K::Pflichteinbau(PflichtEinstufung {
            jahresverbrauch_kwh: Some(kwh),
            ..PflichtEinstufung::default()
        })
    }

    /// A Pflichteinbau point known only by its installierte Leistung.
    fn leistung(kw: Decimal) -> PflichtEinstufung {
        PflichtEinstufung {
            installierte_leistung_kw: Some(kw),
            ..PflichtEinstufung::default()
        }
    }

    /// The §30 Abs. 1 schedule, as published — every figure brutto.
    #[test]
    fn the_pflichteinbau_schedule() {
        for (kwh, lv, total) in [
            (dec!(9_000), dec!(40), dec!(120)),
            (dec!(15_000), dec!(50), dec!(130)),
            (dec!(30_000), dec!(110), dec!(190)),
            (dec!(80_000), dec!(140), dec!(220)),
        ] {
            let k = verbrauch(kwh);
            assert_eq!(
                preisobergrenze_eur_per_jahr(k, E::Netzbetreiber, IM_SCHEDULE),
                Preisobergrenze::Betrag(dec!(80))
            );
            assert_eq!(
                preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
                Preisobergrenze::Betrag(lv)
            );
            assert_eq!(gesamtobergrenze_eur_per_jahr(k, IM_SCHEDULE), Some(total));
        }
    }

    /// Each Nummer's Jahresverbrauch bound, at the boundary. Every bound reads
    /// „über X bis einschließlich Y", so X belongs to the band below and Y to
    /// this one.
    #[test]
    fn the_consumption_bounds_are_exclusive_below_and_inclusive_above() {
        for (kwh, band) in [
            (dec!(6_001), B::Bis10000),
            (dec!(10_000), B::Bis10000),
            (dec!(10_001), B::Bis20000),
            (dec!(20_000), B::Bis20000),
            (dec!(20_001), B::Bis50000),
            (dec!(50_000), B::Bis50000),
            (dec!(50_001), B::Bis100000),
            (dec!(100_000), B::Bis100000),
            (dec!(100_001), B::Ueber100000),
        ] {
            let einstufung = PflichtEinstufung {
                jahresverbrauch_kwh: Some(kwh),
                ..PflichtEinstufung::default()
            };
            assert_eq!(einstufung.band(), band, "{kwh} kWh");
        }
    }

    /// The installierte-Leistung criteria run on their own scale — 7/15/25/100 kW.
    #[test]
    fn the_capacity_bounds_classify_a_generating_point() {
        for (kw, band) in [
            (dec!(7), B::Bis10000),
            (dec!(7.5), B::Bis20000),
            (dec!(15), B::Bis20000),
            (dec!(15.1), B::Bis50000),
            (dec!(25), B::Bis50000),
            (dec!(25.1), B::Bis100000),
            (dec!(100), B::Bis100000),
            (dec!(100.1), B::Ueber100000),
        ] {
            assert_eq!(leistung(kw).band(), band, "{kw} kW");
        }
    }

    /// Nr. 4 lists the §14a Zählpunkt beside the 10 000–20 000 kWh band, so a
    /// steuerbare Verbrauchseinrichtung reaches that band on consumption alone
    /// that would otherwise sit in Nr. 5.
    #[test]
    fn a_sect14a_zaehlpunkt_reaches_the_nummer_four_band() {
        let einstufung = PflichtEinstufung {
            jahresverbrauch_kwh: Some(dec!(7_000)),
            steuerbare_verbrauchseinrichtung: true,
            ..PflichtEinstufung::default()
        };
        assert_eq!(einstufung.band(), B::Bis20000);
        assert_eq!(
            preisobergrenze_eur_per_jahr(
                K::Pflichteinbau(einstufung),
                E::Letztverbraucher,
                IM_SCHEDULE
            ),
            Preisobergrenze::Betrag(dec!(50))
        );
    }

    /// Each Nummer is a disjunction: the criterion that classifies highest wins.
    #[test]
    fn the_highest_matching_nummer_wins() {
        let einstufung = PflichtEinstufung {
            jahresverbrauch_kwh: Some(dec!(7_000)),
            installierte_leistung_kw: Some(dec!(60)),
            steuerbare_verbrauchseinrichtung: true,
        };
        assert_eq!(
            einstufung.band(),
            B::Bis100000,
            "60 kW is Nr. 2 — the consumption and the §14a fact classify lower"
        );
    }

    /// With no fact at all the tightest ceiling applies, so an unclassifiable
    /// point is still measured rather than let through.
    #[test]
    fn an_unclassifiable_point_takes_the_tightest_ceiling() {
        let k = K::Pflichteinbau(PflichtEinstufung::default());
        assert_eq!(
            preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
            Preisobergrenze::Betrag(dec!(40))
        );
    }

    /// Above 100 000 kWh the Letztverbraucher's share is an angemessenes
    /// Entgelt, but the Netzbetreiber's share is capped like every other band.
    #[test]
    fn the_top_band_caps_only_the_grid_operators_share() {
        let k = verbrauch(dec!(250_000));
        assert_eq!(
            preisobergrenze_eur_per_jahr(k, E::Netzbetreiber, IM_SCHEDULE),
            Preisobergrenze::Betrag(dec!(80))
        );
        assert_eq!(
            preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, IM_SCHEDULE),
            Preisobergrenze::Angemessen
        );
        assert_eq!(
            gesamtobergrenze_eur_per_jahr(k, IM_SCHEDULE),
            None,
            "no total where one share is open"
        );
    }

    /// §30 Abs. 3 is one ceiling regardless of consumption.
    #[test]
    fn an_optional_installation_is_capped_at_thirty_each() {
        for schuldner in [E::Netzbetreiber, E::Letztverbraucher] {
            assert_eq!(
                preisobergrenze_eur_per_jahr(K::OptionalerEinbau, schuldner, IM_SCHEDULE),
                Preisobergrenze::Betrag(dec!(30))
            );
        }
        assert_eq!(
            gesamtobergrenze_eur_per_jahr(K::OptionalerEinbau, IM_SCHEDULE),
            Some(dec!(60))
        );
    }

    /// **Invariant: the schedule is dated.**
    ///
    /// Abs. 1 and Abs. 3 both read „für die Zeit ab dem 1. Januar 2025". A
    /// correction settled today for a 2024 period is governed by the rules of
    /// 2024, and mako holds none — so it answers `VorSchedule` rather than
    /// measuring the charge against a schedule that did not apply to it.
    #[test]
    fn a_period_before_the_schedule_is_not_measured_against_it() {
        let vorher = time::macros::date!(2024 - 12 - 31);
        let k = verbrauch(dec!(9_000));
        for schuldner in [E::Netzbetreiber, E::Letztverbraucher] {
            assert_eq!(
                preisobergrenze_eur_per_jahr(k, schuldner, vorher),
                Preisobergrenze::VorSchedule
            );
        }
        assert_eq!(gesamtobergrenze_eur_per_jahr(k, vorher), None);

        // The first day it governs.
        assert_eq!(
            preisobergrenze_eur_per_jahr(k, E::Letztverbraucher, SCHEDULE_AB),
            Preisobergrenze::Betrag(dec!(40))
        );
    }

    /// `betrag()` distinguishes "no ceiling in law" from "a ceiling of zero".
    #[test]
    fn only_a_fixed_ceiling_yields_a_number() {
        assert_eq!(Preisobergrenze::Betrag(dec!(80)).betrag(), Some(dec!(80)));
        assert_eq!(Preisobergrenze::Angemessen.betrag(), None);
        assert_eq!(Preisobergrenze::VorSchedule.betrag(), None);
    }

    /// The bands rise monotonically — a higher band never caps lower.
    #[test]
    fn the_bands_rise_monotonically() {
        let mut previous = Decimal::ZERO;
        for kwh in [dec!(9_000), dec!(15_000), dec!(30_000), dec!(80_000)] {
            let ceiling =
                preisobergrenze_eur_per_jahr(verbrauch(kwh), E::Letztverbraucher, IM_SCHEDULE)
                    .betrag()
                    .expect("a fixed ceiling");
            assert!(
                ceiling > previous,
                "{kwh} kWh must exceed the band below it"
            );
            previous = ceiling;
        }
    }
}