grid-billing
Deterministic, regulation-aware German grid settlement engine — NNE, KA, MMM, MSB, and GeLi Gas AWH Sperrprozesse (PIDs 31001, 31002, 31005, 31006, 31009, 31011).
Regulatory ceilings and structure
KAV §2 — Konzessionsabgabe
The Höchstbeträge are checked on every settlement, and each position cites the paragraph its group is actually capped under: §2 Abs. 2 for Tarifkunden and Schwachlast, Abs. 3 for Sondervertragskunden, Abs. 7 where the customer is freigestellt.
The rates themselves are undated because the statute has not changed them since the Euro conversion — the annual reductions people remember were the §3 transitional phase-down, which completed long ago.
MsbG §30 — Preisobergrenzen für den Messstellenbetrieb
| §30 Abs. 1 band | Netzbetreiber | Letztverbraucher | Total |
|---|---|---|---|
| > 6 000 – ≤ 10 000 kWh | 80 € | 40 € | 120 € |
| > 10 000 – ≤ 20 000 kWh · steuerbare VE · > 7 – ≤ 15 kW | 80 € | 50 € | 130 € |
| > 20 000 – ≤ 50 000 kWh · > 15 – ≤ 25 kW | 80 € | 110 € | 190 € |
| > 50 000 – ≤ 100 000 kWh · > 25 – ≤ 100 kW | 80 € | 140 € | 220 € |
| > 100 000 kWh · > 100 kW | 80 € | angemessenes Entgelt | — |
§30 Abs. 3 (optionaler Einbau) is 30 € each, 60 € total. §30 Abs. 2 adds up to 50 € a year per party for a Steuereinrichtung.
The charge is annualised before comparison — billing a year in monthly
instalments does not raise the cap. A charge above the ceiling raises
MSB_ABOVE_MSBG_POG.
§17 StromNEV — Netzebene and Benutzungsstundenzahl
Netzebene covers the seven levels, distinguishing network levels from
transformation levels. It is recorded, not applied: Netzentgelte are
published per level, so the level is what makes a rate checkable against a price
sheet, but this crate is given rates rather than resolving them.
The same holds for the Benutzungsstundenzahl (annual energy ÷ annual peak). It
does not appear in §17 as a threshold — it is the convention by which a price
sheet publishes two rate pairs — so it goes into the trace rather than selecting
anything. Zero peak yields None, not zero.
What is enforced is §17 Abs. 6: an Arbeitspreis-only tariff is permitted only
in Niederspannung up to 100 000 kWh a year. Billing without a Leistungspreis
outside that raises ARBEITSPREIS_ONLY_OUTSIDE_SECT17_ABS6.
§19 Abs. 2 StromNEV — individuelle Netzentgelte
Both forms are settled, with the statutory floors — which are in the ordinance text itself, not only in the BK4-22-089 methodology:
| Form | Qualification | Mindestentgelt |
|---|---|---|
| Atypische Netznutzung (Satz 1) | peak in the low-load windows (BNetzA-approved) | 20 % |
| Intensive Netznutzung (Satz 2) | ≥ 7 000 h and ≥ 10 GWh | 20 % |
| ≥ 7 500 h | 15 % | |
| ≥ 8 000 h | 10 % |
Sect19Vereinbarung carries the agreed fraction; the engine applies it as a
reduction over the Arbeits- and Leistungspreis positions only — the
Konzessionsabgabe and the levies are untouched, because the Netzbetreiber's lost
revenue is recovered through the §19-Umlage billed separately. An agreement
below the floor raises SECT19_BELOW_MINDESTENTGELT; a Satz 2 agreement whose
utilisation data does not qualify raises SECT19_BANDLAST_CRITERIA_NOT_MET.
§18 StromNEV — Entgelte für dezentrale Erzeugung, under Abschmelzung
settle_dezentrale_einspeisung pays the plant operator the avoided upstream
costs, at the factor Festlegung GBK-25-02-1#1 (17.02.2026) leaves standing:
| Period | Factor |
|---|---|
| to 30.06.2026 | 1.00 |
| 01.07.2026 – 31.12.2027 | 0.50 |
| 2028 | 0.25 |
| from 2029 | 0.00 |
The Tenor cuts in three steps (50 % from 01.07.2026, 50 % from 01.01.2027, 75 % from 01.01.2028) — the annual averages fall by 25 points a year, which is the decision's own cross-check. A period crossing a step is refused, not averaged; an EEG-funded plant is refused outright (§18 Abs. 1 Satz 4 Nr. 1 — the payment would be unlawful).
Gas — Druckstufen and Kapazitätsprodukte (§15 GasNEV)
Druckstufe (Hoch-/Mittel-/Niederdruck) is the gas analogue of the Strom
Netzebene; GasKapazitaet bills a booked capacity at the price sheet's annual
rate, pro-rated by calendar days, distinguishing feste from unterbrechbarer
Kapazität — the latter cites §15 Abs. 5, and its discount stays where the
ordinance leaves it: on the price sheet, not in this crate.
Invalid inputs are unrepresentable
NneInput's cross-field rules live in the types, not in a validator a caller
could forget:
| Rule | Enforced by |
|---|---|
| Exactly one Arbeitspreis form (einheitlich, Modul 1, HT/NT Modul 2, or Modul 3 spot) | ArbeitspreisModell — one variant at a time; Modul3Spotpreis replaces the flat position, so the same energy is never billed twice |
| §14a modules are mutually exclusive | ArbeitspreisModell — Modul1Pauschal/Modul2ZeitVariabel/Modul3Spotpreis are variants of the same enum |
Reduction factors in (0, 1] |
Reduktionsfaktor enforces the range at construction |
| Leistungspreis needs both peak and rate | Leistungspreis — a pair |
| Grundpreis needs both rate and months | Grundpreis — a pair |
| KAV Höchstbetrag is always checked | Konzessionsabgabe pairs the rate with its KaKundengruppe |
| Period ordering | SettlementPeriod — constructing it is the check |
What the types cannot express — negative energy, empty or inverted Modul 3
intervals — settle_nne enforces itself and returns Err. There is no
separate NNE validator: settle_nne is pure and cheap, run it and read
warnings. validate_mmm_input / validate_msb_input /
validate_gas_awh_input exist for the settlement types whose engines accept
looser shapes.
Settlement, not invoice
The engine calculates what is owed and why. It does not know what the invoice looks like:
Input → Validation → Settlement Engine → SettlementResult → InvoiceDocument → BO4E → EDIFACT
SettlementResult carries the positions, totals, warnings, the applied
RegulatoryRegime and a CalculationTrace per position. InvoiceDocument
carries everything that is a property of the document — invoice number, issue
and due dates, the Prüfidentifikator that routes it, the reference to what it
supersedes — and is built by an adapter around a settlement.
The separation is what makes a settlement recomputable: the same period can be settled twice, for a correction or a dispute or an audit, and the two results compared, without inventing an invoice number each time.
Position numbering follows the same rule. InvoiceDocument::numbered_positions()
assigns 1-based numbers at rendering time; the engine carries no counter.
No BO4E inside the engine
SpotPriceFormula states the pricing formula behind a §14a Modul 3 rate as a
typed value — reference, unit, method, steps — never a serde_json::Value carrying
a hand-built BO4E COM. That keeps BO4E schema knowledge out of the engine: an
adapter that needs the COM builds it from the value object, and the crate has no
serde_json dependency at all.
SettlementPeriod
A validated pair, not two loose period_from / period_to dates each calculation
would have to re-check for ordering. Constructing SettlementPeriod is the check,
so an inverted period is unrepresentable rather than rejected at every call site.
Regulatory regime
German network-charge law is several timelines, each turning over on its own date:
| Axis | Turns over | Successor |
|---|---|---|
| Netzzugang | 31.12.2025 | §20 Abs. 3 EnWG via BNetzA Festlegungen (GPKE BK6-24-174, GaBi Gas 2.1) |
| Entgeltbildung | 31.12.2028 | BNetzA framework Festlegung AgNeS, replacing StromNEV and ARegV |
| Umlagen | annually | ÜNB publication each October |
RegulatoryRegime resolves those dates once, at the edge;
every calculation then matches on an enum. Scattering if period_to <= date
through the engine is how a rule change becomes a bug — each site has to be found
and each has to agree. Adding the AgNeS turnover is a new variant the compiler
forces every deciding site to handle.
The regime can also be supplied explicitly, so a historical settlement is
reproduced under the rules that applied then rather than under today's calendar.
A period crossing a turnover raises REGIME_TURNOVER_IN_PERIOD: different rules
govern its start and its end, so it should be split rather than half-billed.
Explainability
Every position carries a CalculationTrace — the inputs used, the paragraphs
applied, the tariff source, the reduction factor, the rounding. SettlementResult
additionally exposes all_legal_refs(), deduplicated across positions.
These types are Serialize, and the service adapters emit them as BO4E
ZusatzAttribute (mako:calculation_trace per position,
mako:legal_references and mako:settlement_warnings per settlement). BO4E has
no field for a calculation trace and inventing one would break the schema; a
ZusatzAttribut is the sanctioned place for what a standard does not model.
This matters because the settlement value itself is dropped once the Rechnung is stored — the attribute is the only surviving record of why an amount is what it is, and it is what a §20 EnWG audit or an LF dispute is answered from.
Netzseitige Umlagen
Three levies ride on the network charge rather than the commodity, and a Strom NNE invoice carries all three:
| Levy | Basis | 2026 (nicht privilegiert) |
|---|---|---|
| Aufschlag für besondere Netznutzung (§19 StromNEV-Umlage) | §19 Abs. 2 StromNEV | A′ 1.559 · B′ 0.050 · C′ 0.025 ct/kWh |
| Offshore-Netzumlage | §17f EnWG | 0.941 ct/kWh |
| KWKG-Umlage | §26 KWKG | 0.446 ct/kWh |
Rates are set annually by the ÜNB and published by 25 October for the following
year. They are held as a year-indexed series in umlagen so a
correction reopening an earlier period bills it at the rate that applied then —
a single configured scalar cannot express two years at once. NneInput carries
a per-levy override for the cases an EnFG decision does not fit the published
schedule.
Letztverbrauchergruppen (EnFG §§21 ff.)
The Energiefinanzierungsgesetz replaced the older per-levy privilege rules with
one scheme. Letztverbrauchergruppe selects the band: A′ is the full levy
and covers the first 1 GWh at an Entnahmestelle; B′ and C′ apply above
that, C′ for energy-intensive undertakings; Befreit (§21 EnFG) is zero
rather than reduced, and emits no line at all.
Only the §19 StromNEV-Umlage is published as an explicit A′/B′/C′ schedule. The other two publish the non-privileged rate, with privileges granted per Entnahmestelle — supply those through the override.
A year the series does not cover yields no rate rather than a neighbouring
year's, and the levy is omitted with an UMLAGE_RATE_MISSING warning. Billing
2027 at the 2026 rate would be wrong by an amount nobody notices until the ÜNB
reconciliation.
Regulatory baseline (2026)
StromNZV and GasNZV ceased to apply with the end of 31.12.2025 — Art. 15 Abs. 4 (Strom) and Abs. 6 (Gas) of the Gesetz v. 22.12.2023, BGBl. 2023 I Nr. 405. The successor competence is §20 Abs. 3 EnWG, exercised through BNetzA Festlegungen:
| Domain | Until 31.12.2025 | From 01.01.2026 |
|---|---|---|
| Mehr-/Mindermengen Strom | StromNZV §13 Abs. 3 | GPKE (BK6-24-174) Teil 1 Kap. 8.4 |
| Mehr-/Mindermengen Gas | GasNZV §25 | GaBi Gas 2.1 (BK7-24-01-008) |
| Standardlastprofile Strom | StromNZV §12 | GPKE (BK6-24-174), "Profilverfahren" |
| Standardlastprofile Gas | GasNZV §24 | GaBi Gas 2.1 (BK7-24-01-008) |
| Bilanzkreisabrechnung Strom | StromNZV §4 | MaBiS (Anlage 3 zu BK6-24-174) |
| Konzessionsabgabe | KAV §2 (unchanged) | KAV §2 |
settle_mmm picks its legal references from period_to, so a
settlement for a 2025 period still cites the ordinance that governed it and one
for 2026 does not. LegalReference::citation appends "(außer Kraft seit
01.01.2026)" to a repealed ordinance, keeping archived invoices self-explanatory.
Konzessionsabgabe is governed by the KAV plus §48 EnWG — not by StromNZV §17 or GasNZV §7, which concern balancing-group and network-access matters.
Mehr-/Mindermengen sign convention
Both quantities are named from the network operator's side, which inverts the intuitive reading. GPKE Kap. 8.4 Nr. 3:
Unterschreitet die Summe der in einem Zeitraum ermittelten elektrischen Arbeit die Summe der Arbeit, die den bilanzierten Profilen zu Grunde gelegt wurde (ungewollte Mehrmenge), so vergütet der Netzbetreiber dem Lieferanten oder dem Kunden diese Differenzmenge.
| Measurement vs profile | Quantity | Money |
|---|---|---|
| measured < profiled | ungewollte Mehrmenge | NB vergütet → credit |
| measured > profiled | ungewollte Mindermenge | NB stellt in Rechnung → charge |
GaBi Gas 2.1 states the same for gas: the Ausspeisenetzbetreiber nimmt Mehrmengen entgegen and liefert Mindermengen. Consuming below the profile leaves surplus energy the network absorbed — that surplus is the Mehrmenge, and it is reimbursed.
Konzessionsabgabe (KAV §2)
KaKundengruppe models the two orthogonal tests KAV actually applies:
Tarifkunde vs Sondervertragskunde is a contract-type test, and Tarifkunden
rates band on municipality inhabitants, not on annual consumption.
| Group | Strom | Gas |
|---|---|---|
| Tarifkunde, Gemeinde ≤ 25 000 Einw. | 1.32 | 0.51 (Kochen/Warmwasser) · 0.22 (übrige) |
| ≤ 100 000 | 1.59 | 0.61 · 0.27 |
| ≤ 500 000 | 1.99 | 0.77 · 0.33 |
| > 500 000 | 2.39 | 0.93 · 0.40 |
| Schwachlast (Strom only) | 0.61 | — |
| Sondervertragskunde | 0.11 | 0.03 |
These are Höchstbeträge, so settle_nne emits
KA_ABOVE_KAV_MAXIMUM when the agreed rate exceeds the ceiling for the group,
and KA_CHARGED_WHILE_EXEMPT when a rate is applied to a §2 Abs. 7 exemption.
What this crate does
grid-billing computes BDEW INVOIC billing positions with full explainability:
- NNE Strom (PID 31002, NN-Rechnung) — flat-rate Arbeit, Leistung (RLM), Konzessionsabgabe
- NNE Gas (PID 31002, NN-Rechnung) — GasNEV §14 legal basis, auto-set when
Sparte::Gas - §14a Modul 2 ToU — mandatory HT/NT Arbeit split for controllable loads (BNetzA BK6-22-300)
- MMM Strom (PID 31005) — Mehr-/Mindermengensaldo, GPKE (BK6-24-174) Teil 1 Kap. 8.4
- MMM Gas (PID 31005) — Gas imbalance, GaBi Gas 2.1 (BK7-24-01-008)
- MMM Mehrmenge selbst ausgestellt (PID 31006) — Mehr-/Mindermenge als Lieferung, self-issued (INVOIC AHB Selbstausstellung)
- MSB-Rechnung (PID 31009) — Grundgebühr Messstellenbetrieb + optional Messdienstleistung
- GeLi Gas AWH Sperrprozesse (PID 31011) — abrechnungswürdige Handlungen (BK7-24-01-009 §5.4)
- §13a EnWG Redispatch-Vergütung —
redispatch_verguetung()computes the angemessene Vergütung per activation (entgangene Einnahmen + zusätzliche − ersparte Aufwendungen;eeg_entgangene_einnahmen()for the Nr. 5 EEG basis) - Reversal (Stornorechnung) —
reverse()negates any prior settlement immutably
All calculations are pure functions — zero I/O, zero async, no side effects.
All monetary arithmetic uses rust_decimal::Decimal via billing::EuroAmount — no f64 anywhere.
Architecture
Settlement flow
NneInput / MmmInput / MsbInput / GasAwhInput
│
▼
validate_*_input() ← optional pre-check: ValidationResult
│
▼
settle_*() ← pure, deterministic, no I/O
│
▼
SettlementResult {
settlement_type, status, period, regime, sparte,
malo_id, nb_mp_id, counterparty_mp_id,
positions: Vec<SettlementPosition {
text, kind, ← what was charged
quantity, unit, unit_price_eur, net_eur,
spot_price_formula, ← the formula behind the rate, as a value
trace: CalculationTrace { ← "why is this amount here?"
explanation,
legal_refs: Vec<LegalReference>, ← StromNEV §17, KAV §2, §14a Modul 2…
tariff_source: Option<TariffSource>,
gross_eur, regulatory_reduction_factor, …
}
}>,
total_eur,
warnings: Vec<SettlementWarning>,
}
│
▼ (adapter — this is where document identity enters)
InvoiceDocument { settlement, pid, rechnungsnummer, invoice_date, due_date }
│
▼ (rubo4e lives in the service; grid-billing has no BO4E dep)
into_rechnung(&document) → rubo4e::current::Rechnung {
rechnungspositionen[].positionsnummer ← assigned here
rechnungspositionen[].artikelnummer ← via kind.artikelnummer()
}
│
▼
InvoicCheckEngine::check(pid, &nb_mp_id, &rechnung, …)
│
▼
invoice_drafts (PostgreSQL) → AS4 dispatch
BDEW Artikelnummern architecture
The service layer owns the BDEW Artikelnummer mapping. grid-billing stays free
of rubo4e:
flowchart LR
calc["grid_billing\nsettle_*()"]
pos["SettlementPosition\n.kind: BillingPositionKind\n.trace: CalculationTrace"]
svc["Service layer\nkind_to_artikelnummer()"]
bo4e["Rechnungsposition\n.artikelnummer ← Gas/MMM/KA\n.artikel_id ← NNE Strom/AWH Gas"]
calc --> pos --> svc --> bo4e
note1["BK6-20-160:\nNNE Strom replaced\nartikelnummer → artikel_id\nfrom PreisblattNetznutzung"]
note2["BDEW Codeliste v5.6:\nGas NNE/MMM/KA use\nclassic 9990001… codes\nAWH: 2-01-7-001/002"]
note1 -.->|Strom| bo4e
note2 -.->|Gas| bo4e
Responsibility split
grid-billing has zero dependency on rubo4e. BO4E conversion lives exclusively in the
service layer, keeping this crate publishable to crates.io without pulling in internal workspace crates.
| Responsibility | Where |
|---|---|
| Settlement math + legal refs | grid-billing |
BO4E Rechnung conversion |
netzbilanzd::into_rechnung() / invoicd::into_rechnung() |
| INVOIC plausibility checks 1–6 | invoic-checker |
| EDIFACT serialization + AS4 dispatch | makod |
Domain types
SettlementResult — canonical output
InvoiceDocument — the settlement presented as an invoice
Nothing on InvoiceDocument affects what is owed. numbered_positions() assigns
the 1-based document numbering at render time.
Helper methods on SettlementResult:
| Method | Returns | Description |
|---|---|---|
is_clean() |
bool |
true when no Warning/Error severity items in warnings |
recomputed_total() |
Decimal |
Re-sums positions — should equal total_eur (regression guard) |
all_legal_refs() |
Vec<String> |
Deduplicated citation strings across all positions |
positions_count() |
usize |
Number of settlement positions |
SettlementPosition with CalculationTrace
Every position carries a full audit record so any amount can be explained without
re-running the calculation. The kind field drives the BDEW Artikelnummer mapping
in the service layer. A position carries no position number and no
Artikel-ID: both are properties of the document that presents the settlement,
not of the calculation — an adapter numbers the positions it renders and resolves
Artikel-IDs (AWH Gas 2-01-7-xxx, NNE Strom from the PreisblattNetznutzung)
from the price sheet:
// No position number and no Artikel-ID: both are properties of the document that
// presents the settlement, not of the calculation.
LegalReference
.citation() returns a short German-language string (e.g. "StromNEV §17",
"KAV §2 Abs. 2", "ARegV §17"). Repealed ordinances carry their expiry:
StromNzv/GasNzv append "(außer Kraft seit 01.01.2026)".
Sect14aModule
Sect14aModule::Modul1.label() = "§14a EnWG Modul 1 (pauschale Reduzierung)";
.bnentza_reference() = "BK6-22-300" for all three modules.
TariffSource
Sparte — commodity dispatch
Sparte is required on NneInput and MmmInput. The calculation automatically
selects the correct legal references and SettlementType (from which
default_pid() yields the PID) — the caller sets no PID for standard Gas paths.
SettlementType
SettlementType::default_pid() returns the standard PID for the type; it is 0
for RedispatchKostenblatt and DezentraleEinspeisung, which are not EDIFACT
market processes. MmmGas and MmmStrom share PID 31002 but carry different
legal references.
BillingPositionKind — BDEW Artikelnummern bridge
BillingPositionKind is the rubo4e-free type carried by every SettlementPosition.kind.
The service layer maps it to rubo4e::current::BdewArtikelnummer in into_rechnung().
NNE Strom (PIDs 31001/31006): BK6-20-160 replaced classic
artikelnummercodes withartikel_idfrom the BNetzA Netznutzungspreisblatt. The service layer (netzbilanzd,invoicd) populatesRechnungsposition.artikel_idfrom the tariff sheet for those positions;BillingPositionKind::artikelnummer(settlement_type)returnsNonefor Strom NNE. Gas NNE, MMM, Konzessionsabgabe still use classic Artikelnummer codes.
Source: BDEW Codeliste Artikelnummern und Artikel-ID v5.6 (valid 01.09.2025).
KaKundengruppe / GemeindeGroesse — KAV §2 classifier
KAV applies two orthogonal tests: contract type (Tarifkunde vs Sondervertragskunde), and — for Tarifkunden — municipality size, not annual consumption.
KaKundengruppe::hoechstsatz_ct_per_kwh(sparte) returns the statutory KAV §2
Höchstbetrag (or None for Exempt, and for Schwachlast on Gas).
.kav_paragraph() returns the paragraph the group is actually capped under
("§2 Abs. 2", "§2 Abs. 3", or "§2 Abs. 7") and .label() the position
text. The group is carried on Konzessionsabgabe.klasse, so the ceiling check
always has what it needs: settle_nne emits KA_ABOVE_KAV_MAXIMUM when the
agreed rate exceeds the ceiling, and KA_CHARGED_WHILE_EXEMPT when a rate is
applied to a §2 Abs. 7 exemption.
Who uses this library
| Consumer | Role | Use case |
|---|---|---|
netzbilanzd |
NB | Generate INVOIC 31001/31002/31005/31009/31011 to LF/MSB/LFG |
invoicd |
LF | INVOIC AHB Selbstausstellung selbstausstellen PID 31006 — same formula, LF-initiated |
Quick start
[]
= { = "0.14" }
= "1"
= "0.3"
NNE flat-rate (SLP, Strom)
use ;
use ;
use Letztverbrauchergruppe;
use Decimal;
use date;
let settlement = settle_nne.expect;
// The settlement carries what was settled, not a PID — invoice number, dates and
// the Prüfidentifikator are properties of InvoiceDocument. SettlementType maps to
// the standard PID:
assert_eq!;
assert_eq!;
// counterparty_mp_id is auto-populated from lf_mp_id:
assert_eq!;
// A Strom NNE settlement also carries the three netzseitige Umlagen (§19 StromNEV,
// Offshore, KWKG) alongside the Arbeit and Konzessionsabgabe positions.
for pos in &settlement.positions
NNE Gas (GasNEV §14)
use ;
use ;
// Only Sparte changes — GasNEV §14 legal refs and SettlementType::NneGas are automatic:
let settlement = settle_nne.unwrap;
assert_eq!;
assert_eq!;
§14a Modul 2 ToU (HT/NT split, mandatory since 2024-01-01)
use ;
use ;
let settlement = settle_nne.unwrap;
// Positions: HT + NT Arbeit, the three netzseitige Umlagen, and Konzessionsabgabe.
assert!;
§14a Modul 1 (flat percentage reduction, mandatory offer since 2024-01-01)
use ;
use ;
use dec;
// BK6-22-300 Anlage 2: default reduction factor = 0.85 (customer pays 85% of full rate).
// Reduktionsfaktor enforces the (0, 1] range at construction; REGELFALL is the 0.85 default.
let settlement = settle_nne.unwrap;
// 1500 × 0.035 × 0.85 = 44.625 → 44.62 EUR (MidpointNearestEven)
assert!;
assert!;
Gas NNE with Grundpreis (GasNEV monthly standing charge)
use ;
use ;
let settlement = settle_nne.unwrap;
// Gas carries no netzseitige Umlagen: Grundpreis (15.00) + Arbeit (54.00) = 69.00 EUR
assert_eq!;
assert!;
GeLi Gas AWH Sperrprozesse (PID 31011)
use ;
use SettlementPeriod;
use date;
let settlement = settle_gas_awh.unwrap;
// Invoice number and dates live on InvoiceDocument, not on the settlement:
assert_eq!;
assert_eq!;
assert_eq!;
// Both positions cite BK7-24-01-009 §5.4
assert!;
Correction lifecycle (reversal + replacement pair)
use ;
let original = settle_nne.unwrap;
let corrected = settle_nne.unwrap;
// correct() negates the original and stamps the replacement's status. Invoice
// numbers and the correction chain are recorded on the InvoiceDocuments built
// around these results, not on the settlements themselves.
let = correct;
assert_eq!;
assert_eq!;
assert_eq!;
use ;
let original = settle_nne.unwrap;
// reverse() mirrors every position with the sign flipped; it takes only the
// original — the storno invoice number and dates belong to the InvoiceDocument.
let storno = reverse;
assert_eq!;
assert_eq!;
Pre-calculation validation
use ;
let input = MmmInput ;
let v = validate_mmm_input;
if !v.is_valid
let settlement = settle_mmm.unwrap;
(settle_nne validates inline — malformed NNE input returns Err directly;
validate_mmm_input / validate_msb_input / validate_gas_awh_input exist
for the settlement types where a pre-flight warning list is useful.)
Service-layer conversion to BO4E Rechnung
// In netzbilanzd/src/billing.rs — grid-billing itself has no rubo4e dep:
use ;
use ;
Generated invoice types
| PID | Description | Direction | Sparte |
|---|---|---|---|
| 31001 | Abschlagsrechnung Netznutzung | NB → LF | both |
| 31002 | NN-Rechnung Strom (Netznutzung) | NB → LF | Strom |
| 31002 | NN-Rechnung Gas (Netznutzung) | GNB → LFG | Gas (auto via Sparte::Gas) |
| 31005 | MMM-Rechnung (Mehr-/Mindermengensaldo) | NB → LF | both |
| 31006 | MMM Mehrmenge, selbst ausgestellt | LF | both |
| 31009 | MSB-Rechnung | NB → MSB | both |
| 31011 | AWH Sperrprozesse Gas | GNB → LFG | Gas |
Billing position reference
NNE
| # | Position text | Unit | kind |
Condition | Legal basis | Artikelnummer |
|---|---|---|---|---|---|---|
| 1 | Netznutzung Arbeit |
kWh | NneArbeit |
arbeitspreis: ArbeitspreisModell::Einheitlich |
StromNEV §21 (Strom) · GasNEV §14 (Gas) | Wirkarbeit (Gas); artikel_id (Strom) |
| 1 | Netznutzung Arbeit §14a Modul 1 (85% Reduzierung) |
kWh | NneArbeitModul1 |
arbeitspreis: ArbeitspreisModell::Modul1Pauschal |
§14a EnWG Modul 1 · BK6-22-300 | same as NneArbeit |
| 1+2 | Netznutzung Arbeit HT (§14a Modul 2) + NT |
kWh | NneArbeitHt / NneArbeitNt |
arbeitspreis: ArbeitspreisModell::Modul2ZeitVariabel |
§14a EnWG Modul 2 · BK6-22-300 | same as NneArbeit |
| opt | Netzentgelt Grundpreis Gas |
Monat | NneGasGrundpreis |
grundpreis set |
GasNEV §14 | Grundpreis |
| next | Netznutzung Leistung |
kW | NneLeistung |
leistungspreis set (RLM) |
StromNEV §17 | Leistung (Gas); artikel_id (Strom) |
| last | Konzessionsabgabe[tier] |
kWh | Konzessionsabgabe |
konzessionsabgabe set |
KAV §2 Abs. 2 | Konzessionsabgabe |
MMM
| # | Position text | kind |
Artikelnummer | Condition |
|---|---|---|---|---|
| 1 | Mehrmengen |
Mehrmenge |
Mehrmenge |
actual > profil |
| 2 | Mindermengen (Gutschrift) |
Mindermenge |
Mindermenge |
profil > actual |
MSB
| # | Position text | kind |
Artikelnummer | Condition |
|---|---|---|---|---|
| 1 | Grundgebühr Messstellenbetrieb |
MsbGrundgebuehr |
EntgeltEinbauBetriebWartungMesstechnik |
Always |
| 2 | Messdienstleistung |
Messdienstleistung |
EntgeltMessungAblesung |
messdienstleistung_eur set |
AWH Gas Sperrprozesse (PID 31011)
| # | Position text | artikel_id |
Condition |
|---|---|---|---|
| any | Sperrung Gaszähler |
2-01-7-001 |
Unterbrechung reguläre AZ |
| any | Entsperrung Gaszähler |
2-01-7-002 |
Wiederherstellung reguläre AZ |
| any | Erfolglose Unterbrechung |
2-01-7-003 |
Sperrung failed |
| any | Stornierung Sperrauftrag (Vortag) |
2-01-7-004 |
Cancelled day before |
| any | Stornierung Sperrauftrag (Sperrtag) |
2-01-7-005 |
Cancelled same day |
| any | Entsperrung außerhalb AZ |
2-01-7-006 |
Out of hours |
Source: BDEW Codeliste Artikelnummern und Artikel-ID v5.6, Section 3.2 (valid 01.09.2025).
Design invariants
| Invariant | Detail |
|---|---|
| No floating-point money | rust_decimal::Decimal throughout; billing::EuroAmount for overflow guard. No f64. |
| No rubo4e dependency | Returns SettlementResult; service layer owns into_rechnung(). |
counterparty_mp_id auto-populated |
lf_mp_id (NNE/MMM) or msb_mp_id (PID 31009) copied automatically. |
Sparte drives settlement type |
Sparte::Gas → SettlementType::NneGas, GasNEV §14, PID 31005. No manual override needed. |
| Every position cites regulation | trace.legal_refs is non-empty for every position. Enables BNetzA audit without re-calculation. |
| Artikelnummer on every position | BillingPositionKind::artikelnummer() in this crate. Never empty. |
MmmGas ≠ MmmStrom |
Separate SettlementType variants ensure correct legal refs (GaBi Gas 2.1 (BK7-24-01-008) vs GPKE (BK6-24-174) Teil 1 Kap. 8.4) per position. |
| Immutable correction chain | reverse() mirrors positions, sets status = Reversal, links via correction_of. Original never mutated. |
correct() pair |
Returns (reversal, replacement) — both get status set atomically; caller dispatches both. |
| Pure functions | All settlement functions are sync with no side effects. |
recomputed_total guard |
debug_assert_eq!(result.total_eur, result.recomputed_total()) inside every settlement function — catches rounding bugs in debug builds. |
See also
invoic-checker— validates the generatedRechnungin the service layernetzbilanzd— NB billing service that callsgrid-billinginvoicd— LF service usinggrid-billingfor selbstausstellen- Operator guide → netzbilanzd