Skip to main content

grid_billing/
sect19.rs

1//! Individuelle Netzentgelte — §19 Abs. 2 StromNEV.
2//!
3//! Two forms, both agreed between Netzbetreiber and Letztverbraucher and both
4//! subject to BNetzA oversight (BK4-22-089, as amended):
5//!
6//! - **Atypische Netznutzung** (Satz 1): the customer's annual peak predictably
7//!   falls in the network's low-load windows. The individual charge must not be
8//!   less than **20 %** of the published charge.
9//! - **Intensive Netznutzung / Bandlast** (Satz 2): qualification requires at
10//!   least **7 000 Benutzungsstunden and 10 GWh** a year; the floor then falls
11//!   with utilisation — 20 % from 7 000 h, **15 %** from 7 500 h, **10 %** from
12//!   8 000 h.
13//!
14//! The floors are statutory — they are in the ordinance text itself, not only
15//! in the Beschlusskammer's methodology. What BK4-22-089 adds is *how* the
16//! reduced charge is derived (the physikalischer Pfad); this crate does not
17//! derive it — the agreed percentage arrives as an input, and the engine's job
18//! is to apply it to the right positions and to refuse to let it silently fall
19//! below the floor.
20//!
21//! ## What the reduction applies to
22//!
23//! The individual charge replaces the **Netzentgelt** — Arbeits- and
24//! Leistungspreis. It does not touch the Konzessionsabgabe or the network
25//! levies: the revenue the Netzbetreiber loses is compensated through the
26//! §19 StromNEV-Umlage, which this crate bills separately.
27
28use rust_decimal::Decimal;
29use rust_decimal::dec;
30
31/// The two §19 Abs. 2 forms.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33pub enum Sect19Art {
34    /// Satz 1 — annual peak predictably in the network's low-load windows.
35    AtypischeNetznutzung,
36    /// Satz 2 — Bandlast: ≥ 7 000 Benutzungsstunden and ≥ 10 GWh a year.
37    IntensiveNetznutzung,
38}
39
40/// Qualification threshold for Satz 2, in kWh.
41pub const BANDLAST_MINDESTARBEIT_KWH: Decimal = dec!(10_000_000);
42
43/// The Satz 2 floor for a given utilisation, as a fraction of the published
44/// charge.
45///
46/// Returns `None` below the qualification threshold — 7 000 h *and* 10 GWh.
47/// `None` means "no Satz 2 agreement is available at all", not "no floor".
48#[must_use]
49pub fn bandlast_mindestentgelt(
50    benutzungsstunden: Decimal,
51    jahresarbeit_kwh: Decimal,
52) -> Option<Decimal> {
53    if jahresarbeit_kwh < BANDLAST_MINDESTARBEIT_KWH || benutzungsstunden < dec!(7000) {
54        return None;
55    }
56    Some(if benutzungsstunden >= dec!(8000) {
57        dec!(0.10)
58    } else if benutzungsstunden >= dec!(7500) {
59        dec!(0.15)
60    } else {
61        dec!(0.20)
62    })
63}
64
65/// The Satz 1 floor — 20 % of the published charge, unconditionally.
66///
67/// Whether the peak really falls in the low-load windows is what the BNetzA
68/// approval establishes; by the time this crate is asked to settle, that
69/// question is decided.
70pub const ATYPISCH_MINDESTENTGELT: Decimal = dec!(0.20);
71
72/// An agreed §19 Abs. 2 individual charge, as a fraction of the published one.
73#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
74pub struct Sect19Vereinbarung {
75    /// Which form the agreement takes.
76    pub art: Sect19Art,
77    /// The agreed fraction of the published Netzentgelt — `0.20` pays 20 %.
78    pub vereinbarter_prozentsatz: Decimal,
79    /// The BNetzA approval or notification reference, for the trace.
80    pub genehmigung: Option<String>,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    /// The statutory staircase, at each boundary.
88    #[test]
89    fn the_statutory_staircase() {
90        let gwh10 = dec!(10_000_000);
91        assert_eq!(bandlast_mindestentgelt(dec!(7000), gwh10), Some(dec!(0.20)));
92        assert_eq!(bandlast_mindestentgelt(dec!(7499), gwh10), Some(dec!(0.20)));
93        assert_eq!(bandlast_mindestentgelt(dec!(7500), gwh10), Some(dec!(0.15)));
94        assert_eq!(bandlast_mindestentgelt(dec!(7999), gwh10), Some(dec!(0.15)));
95        assert_eq!(bandlast_mindestentgelt(dec!(8000), gwh10), Some(dec!(0.10)));
96        assert_eq!(bandlast_mindestentgelt(dec!(8760), gwh10), Some(dec!(0.10)));
97    }
98
99    /// Both qualification conditions are required, not either.
100    #[test]
101    fn qualification_needs_hours_and_energy() {
102        assert_eq!(bandlast_mindestentgelt(dec!(6999), dec!(10_000_000)), None);
103        assert_eq!(
104            bandlast_mindestentgelt(dec!(8000), dec!(9_999_999)),
105            None,
106            "9.999999 GWh is below the threshold however high the utilisation"
107        );
108    }
109}