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
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
# grid-billing

> Deterministic, regulation-aware German grid settlement engine —
> NNE, KA, MMM, MSB, and GeLi Gas AWH Sperrprozesse
> (Prüfidentifikatoren 31001, 31002, 31005, 31006, 31009, 31011 — the five-digit
> BDEW codes that name each billing Anwendungsfall).

[![Crates.io](https://img.shields.io/crates/v/grid-billing?label=grid-billing&color=f59e0b&logo=rust)](https://crates.io/crates/grid-billing)

## 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, and **Abs. 4** (Strom) resp.
**Abs. 5** (Gas) where the Verordnung forbids a Konzessionsabgabe altogether.

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

Every figure §30 states is **brutto jährlich**.

| §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.

Two conversions before the comparison means anything. The monthly charge is
**annualised** — billing a year in instalments does not raise the cap — and it is
**grossed up** at the Umsatzsteuer rate of the delivery period, because the
ceiling is gross and the settlement's figures are net. Comparing net against
gross would grant the whole tax rate as headroom: at 19 % a charge could sit
18.9 % above the statutory maximum unreported. A gross charge above the ceiling
raises `MSB_ABOVE_MSBG_POG`, and the finding states both figures.

What is measured is the **Grundgebühr together with the Messdienstleistung**.
§30 caps the „Entgelt für den Messstellenbetrieb", and §3 Abs. 2 Nr. 1 MsbG puts
the Messung inside it — Messstellenbetrieb umfasst die „Gewährleistung einer
mess- und eichrechtskonformen Messung […] einschließlich der
Messwertaufbereitung"; §17 Abs. 7 Satz 1 StromNEV states it from the other side
(„ein Entgelt für den Messstellenbetrieb, zu dem auch die Messung gehört").
Testing the Grundgebühr alone would let a settlement clear any ceiling by moving
part of the charge into the second position. The Messdienstleistung is a flat fee
for the whole period, so it is spread over `billing_months` before annualising.

The **Steuerungseinrichtung am Netzanschlusspunkt** is the exception, and it is
`steuereinrichtung_eur_per_month`'s own position. §30 Abs. 2 charges it
„zusätzlich zu den nach den Absätzen 1 und 5 zulässigen Preisobergrenzen" and
caps it at 50 € brutto a year per party, so it is measured against that figure
and left out of the Abs. 1 comparison — folded into the Grundgebühr it would take
headroom the Absatz does not grant, and an excess raises
`MSB_STEUEREINRICHTUNG_ABOVE_MSBG_POG`.

**§ 2 Abs. 7 is a classification rule, and it is checked too.** A
Niederspannungslieferung counts as a Tariflieferung unless the gemessene Leistung
exceeds 30 kW in mindestens zwei Monaten des Abrechnungsjahres **und** the
Jahresverbrauch exceeds 30 000 kWh. `Konzessionsabgabe::niederspannung` carries
those two facts (with the Satz-4 lower figures where a Gemeinde agreed them), and
a stated `klasse` that contradicts them raises
`KA_GRUPPE_WIDERSPRICHT_KAV_ABS7` — the two ceilings are 1,32 ct and 0,11 ct
apart, so the label is worth checking.

**Two rules forbid a Konzessionsabgabe outright**, and a ceiling cannot catch
either: § 2 Abs. 4 (Strom) resp. Abs. 5 Nr. 2 (Gas) where a Sondervertragskunde's
Durchschnittspreis lies under the Grenzpreis (`Grenzpreisvergleich` →
`KA_UNTER_GRENZPREIS`), and § 2 Abs. 5 Nr. 1 above 5 Millionen kWh Gas je Jahr
und Abnahmefall (read off `jahresarbeit_kwh` → `KA_GAS_UEBER_GRENZMENGE`).

The band is **derived, not named**. Each §30 Abs. 1 Nummer is a disjunction over
facts about the metering point, so `MessstellenKategorie::Pflichteinbau` carries
`PflichtEinstufung { jahresverbrauch_kwh, installierte_leistung_kw,
steuerbare_verbrauchseinrichtung }` and `PflichtEinstufung::band` walks the Nummern
top down — a point meeting several takes the highest, and a settlement cannot pick
its own ceiling. With no fact at all the tightest applies: a Pflichteinbaufall
exists only above 6 000 kWh (§29 Abs. 1), so Nr. 5 is the catalogue's floor.

**The schedule is dated.** Abs. 1 and Abs. 3 state their figures „für die Zeit ab
dem 1. Januar 2025", so `preisobergrenze_eur_per_jahr` takes the period's end and
answers `Preisobergrenze::VorSchedule` below it — a correction settled today for a
2024 period is governed by rules mako does not carry, and reporting it against
these would call a lawful charge an excess (`MSB_POG_VOR_SCHEDULE`). The three
answers are distinct on purpose: `Betrag` is a ceiling, `Angemessen` is Abs. 1
Nr. 1's *angemessenes jährliches Entgelt* above 100 000 kWh, and `VorSchedule` is
"not this schedule's period". Abs. 6 lets a BNetzA Festlegung nach § 33 replace
Abs. 1 bis 3 from a date it names; none has issued.

**Which Jahresstromverbrauch picks the band is Abs. 4's answer, not the
caller's choice**: the average of the last three erfasste Jahresverbrauchswerte,
the Netzbetreiber's Jahresverbrauchsprognose until three exist, re-checked
annually. `PflichtEinstufung::jahresverbrauch_kwh` is that figure — one year's
reading is not.

### §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`.

`Leistungspreis::system` names which Leistungspreissystem the price sheet states,
and the two are billed differently.

`LeistungspreisSystem::Jahr` is §17 Abs. 2 Satz 2: „Das Jahresleistungsentgelt
ist das Produkt aus dem jeweiligen Jahresleistungspreis und der
Jahreshöchstleistung in Kilowatt der jeweiligen Entnahme im Abrechnungsjahr." Two
figures multiplied, and §17 states no day-count convention to scale them by — so
the rate is billed as published. A settlement period that is not the
Abrechnungsjahr raises `JAHRESLEISTUNGSPREIS_UNTERJAEHRIG` rather than being
silently pro-rated into a Zwölftelung no price sheet publishes.

`LeistungspreisSystem::Monat` is the Monatsleistungspreissystem §17 Abs. 8
presupposes when it offers Tagesleistungspreise for Landstrom „neben einem
Jahres- und Monatsleistungspreissystem". StromNEV does not define it; the
Netzbetreiber's Preisblatt publishes it as a €/kW·Monat rate against the period's
Höchstleistung. It is billed for the months stated, and a month count that does
not match the period raises `MONATSLEISTUNGSPREIS_MONATE_MISMATCH` — a twelvefold
over-charge multiplies out perfectly, so only the count can catch it.

### §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`; one
naming neither a Genehmigung (Satz 5) nor an Anzeige (Satz 7) raises
`SECT19_OHNE_GENEHMIGUNG`.

The two Satz 2 thresholds read differently: the Benutzungsstundenzahl must
*„mindestens 7 000 Stunden im Jahr erreichen"*, the Stromverbrauch must *„zehn
Gigawattstunden übersteigen"* — so exactly 10 GWh does not qualify. The agreed
fraction is a reduction, and outside `(0, 1]` it is refused as
`BillingError::InvalidInput`.

### §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 pauschal, Modul 2 prozentual, Modul 3 zeitvariabel, or spot-linked) | `ArbeitspreisModell` — one variant at a time; each replaces the flat position, so the same energy is never billed twice |
| `Modul 1 + Modul 3` is the only combination BK8-22/010-A offers | `ArbeitspreisModell` holds one variant at a time, so every pair is unrepresentable — which is right for Modul 1/Modul 2 (alternative base modules) and for Modul 2/Modul 3, and is a limitation for the Modul 1 + Modul 3 pair the Festlegung permits. See `Sect14aModule::combinable_with` |
| Reduction factors in `(0, 1]` | `Reduktionsfaktor` enforces the range at construction |
| Leistungspreis needs both peak and rate, and names its Leistungspreissystem | `Leistungspreis` — a pair plus `LeistungspreisSystem` |
| 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: the
`bo4e` bridge builds the COM from the value object, and the default build pulls
in no `serde_json` at all — it arrives only with that feature.

## 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`](src/regulatory.rs) 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
`ZusatzAttribut`e (`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 |

B′ and C′ are published „für Strommengen über 1 000 000 kWh" at one Entnahmestelle,
so they are a **tranche, not a rate**: the year's first Gigawattstunde carries A′
whatever the group. `NneInput::enfg_jahresvorverbrauch_kwh` places the period against
that annual boundary, and one straddling it bills two §19-Aufschlag positions.
Omitted, the period is billed as though it opened the year — the over-billing
direction — with `ENFG_VORVERBRAUCH_MISSING`.

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`](src/umlagen.rs) 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 modules** — Modul 1 (pauschale Reduzierung), Modul 2 (prozentuale Reduzierung des Arbeitspreises), Modul 3 (zeitvariable Netzentgelte HT/ST/NT, opt-in since 01.04.2025) — BNetzA BK8-22/010-A / BK8-22/010-A
- **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)
- **NNE Gas** (PID 31002) — GasNEV §14 Arbeits-/Grundpreis and §15 Kapazitätsentgelt
- **Abschlagsrechnung** (PID 31001) — a payment on account: one Positionszeile, no quantity, no
  Arbeitspreis (INVOIC AHB 1.0b Änd-ID 26817). The invoice that settles the period deducts it
  from what is **owed** via `InvoiceDocument::abschlaege`, never from the net or the tax, because
  §14 Abs. 5 UStG taxed the Anzahlung when it was received
- **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; the sign goes on the unit price, so each position still multiplies out

- **Umsatzsteuer** — every settlement states its tax (§14 Abs. 4 Nr. 8 UStG). Network services
  are 19 % and never reverse-charged (UStAE 13b.3a excludes them by name); a Mehr-/Mindermenge is
  a *Lieferung* and takes the §13b Abs. 2 Nr. 5 Buchst. b reverse charge on the asymmetric
  condition the statute sets — electricity needs both parties to hold §3g status, gas needs the
  recipient alone. A delivery period straddling a rate change is refused rather than billed at
  one of the two.

All calculations are **pure functions** — zero I/O, zero async, no side effects.
All monetary arithmetic uses `rust_decimal::Decimal` via `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, sender_mp_id, recipient_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 }
        ▼   (feature `bo4e` — off by default)
grid_billing::bo4e::into_rechnung(&document) → rubo4e::current::Rechnung {
                              rechnungspositionen[].positionsnummer ← assigned here
                              rechnungspositionen[].artikelnummer   ← via kind.artikelnummer()
                              rechnungstyp                          ← Netznutzungsrechnung (NNE + MMM only)
                              netznutzungrechnungsart               ← Handels-/Selbstausgestellt
                              netznutzungrechnungstyp               ← Mehrmindermengenrechnung (MMM only)
                            }
InvoicCheckEngine::check(pid, &sender_mp_id, &rechnung, …)
invoice_drafts (PostgreSQL) → AS4 dispatch
```

### Netznutzungsrechnung typing

`into_rechnung` marks the document so a consumer can recognise it without
inspecting positions:

| Field | Set for | Value |
|---|---|---|
| `rechnungstyp` | Abschlag, NNE Strom/Gas, MMM Strom/Gas, MMM selbst ausgestellt | `Netznutzungsrechnung` |
| `netznutzungrechnungsart` | the same six | `Selbstausgestellt` for PID 31006, else `Handelsrechnung` |
| `netznutzungrechnungstyp` | MMM · Abschlag · anything with a stated cadence | `Mehrmindermengenrechnung` · `Abschlagsrechnung` · the cadence |

The other four settlement types this engine produces — MSB-Rechnung (31009),
Gas-AWH Sperrung (31011), Redispatch Kostenblatt, dezentrale Einspeisung (§18
StromNEV) — are **left untyped on purpose**. They are not network-use invoices,
and typing them as one would assert something the AHB does not.

`netznutzungrechnungstyp` is the one field two different things decide. The
Mehr-/Mindermengen codes follow from the settlement, and so does
`Abschlagsrechnung`, because PID 31001 *is* that document. The rest —
Turnus-, Monats-, Abschluss-, Zwischenrechnung — describe the billing
**cadence**, which the same NNE computation does not carry: it is billed
monthly or annually depending on contract. So it rides on
`InvoiceDocument::cadence`, and with neither present the field stays unset
rather than guessing a rhythm nothing supports.

### BDEW Artikelnummern architecture

Which article number applies to which position in which settlement is domain
logic, so it lives here — `BillingPositionKind::artikelnummer(settlement_type)`
returns the BDEW **codelist name** as a `&'static str`, a plain string that costs
the core engine no BO4E type. The `bo4e` bridge parses it into the enum
(`bo4e::kind_to_artikelnummer`); a consumer rendering something else parses it
its own way.

```mermaid
flowchart LR
    calc["grid_billing<br/>settle_*()"]
    pos["SettlementPosition<br/>.kind: BillingPositionKind<br/>.trace: CalculationTrace"]
    bridge["feature bo4e<br/>kind_to_artikelnummer()"]
    bo4e["Rechnungsposition<br/>.artikelnummer  ← Gas/MMM/KA<br/>.artikel_id     ← NNE Strom/AWH Gas"]

    calc --> pos --> bridge --> bo4e

    note1["BK6-20-160:<br/>NNE Strom replaced<br/>artikelnummer → artikel_id<br/>from PreisblattNetznutzung"]
    note2["BDEW Codeliste v5.6:<br/>Gas NNE/MMM/KA use<br/>classic 9990001… codes<br/>AWH: 2-01-7-001/002"]

    note1 -.->|Strom| bo4e
    note2 -.->|Gas| bo4e
```

`artikelnummer` answers `None` where the position carries an **Artikel-ID**
instead — every Strom NNE position since BK6-20-160, the AWH Gas `2-01-7-xxx`
positions, and the Abschlag, which prices nothing. The renderer resolves those
from the price sheet or the input.

### Responsibility split

| Responsibility | Where |
|---|---|
| Settlement math + legal refs | `grid-billing` |
| BO4E `Rechnung` conversion | `grid_billing::bo4e`**feature `bo4e`**, off by default |
| INVOIC plausibility checks | `invoic-checker` |
| EDIFACT serialization + AS4 dispatch | `makod` |

The feature is off by default so the settlement engine stays publishable and
`rubo4e`-free for a consumer that renders something else; `netzbilanzd` and
`invoicd` both enable it.

| Feature | Pulls in | Enables |
|---|---|---|
| *(default)* | `billing`, `rust_decimal`, `serde`, `thiserror`, `time` | the settlement engine |
| `bo4e` | `rubo4e`, `serde_json` | `bo4e::into_rechnung`, `kind_to_artikelnummer`, `rechnungstyp_for`, `netznutzungrechnungsart_for`, `netznutzungrechnungstyp_for` |

## Domain types

### `SettlementResult` — canonical output

```rust
pub struct SettlementResult {
    pub settlement_type: SettlementType, // NneStrom | NneGas | MmmStrom | MsbRechnung | …
    pub status: SettlementStatus,        // Initial | Correction | Reversal | Final
    pub korrektur_grund: Option<KorrekturGrund>, // why — None only for Initial
    pub period: SettlementPeriod,        // validated pair, both bounds inclusive
    pub regime: RegulatoryRegime,        // the rules this calculation applied
    pub sparte: Sparte,
    pub malo_id: String,
    pub sender_mp_id: String,            // NB, or MSB for a MSB-Rechnung (31009)
    pub recipient_mp_id: String,         // LF, NB, MSB, MGV or ESA
    pub positions: Vec<SettlementPosition>,
    pub total_eur: Decimal,              // rounded to 2 dp
    pub steuer: Steuerausweis,           // §14 Abs. 4 Nr. 8 UStG — rate, amount, reverse-charge note
    pub warnings: Vec<SettlementWarning>,
}
```

### `InvoiceDocument` — the settlement presented as an invoice

```rust
pub struct InvoiceDocument {
    pub settlement: SettlementResult,
    pub pid: u32,                        // BDEW Prüfidentifikator — routes the document
    pub rechnungsnummer: String,
    pub correction_of: Option<String>,   // what this supersedes
    pub invoice_date: time::Date,
    pub due_date: time::Date,
    pub cadence: Option<Rechnungscharakter>,   // IMD+7081 — a document fact
    pub abschlaege: Vec<Abschlagsverrechnung>, // deducted from what is owed, not from the net
}
```

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:

```rust
pub struct SettlementPosition {
    pub text: String,                        // e.g. "Netznutzung Arbeit HT (§14a Modul 3)"
    pub kind: BillingPositionKind,           // what was charged
    pub quantity: Decimal,                   // rounded to 3 dp
    pub unit: QuantityUnit,                  // Kwh | Kw | Kvarh | Kvar | Monat | Jahr
    pub unit_price_eur: Decimal,             // rounded to 6 dp
    pub net_eur: Decimal,                    // quantity × unit_price_eur, rounded to 5 dp
    pub spot_price_formula: Option<SpotPriceFormula>,  // the formula behind the rate
    pub trace: CalculationTrace,
}

// No position number and no Artikel-ID: both are properties of the document that
// presents the settlement, not of the calculation.

pub struct CalculationTrace {
    /// Human-readable explanation, e.g.:
    ///   "1500.000 kWh × 0.035000 EUR/kWh = 52.50000 EUR"
    pub explanation: String,
    pub input_quantity: Decimal,
    pub input_unit_price_eur: Decimal,
    pub gross_eur: Decimal,                       // qty × price before rounding
    pub legal_refs: Vec<LegalReference>,          // at least one, always
    pub tariff_source: Option<TariffSource>,      // where the rate came from
    pub regulatory_reduction_factor: Option<Decimal>, // §14a Modul 2 factor (0–1)
    pub rounding_note: Option<&'static str>,
}
```

### `LegalReference`

```rust
pub enum LegalReference {
    StromNev { paragraph: &'static str },       // "§21" Arbeit, "§17" Leistung
    GasNev   { paragraph: &'static str },       // "§14"
    Kav      { paragraph: &'static str },       // "§2 Abs. 2"
    Kwkg     { paragraph: &'static str },       // "§26" KWKG-Umlage
    Ustg     { paragraph: &'static str },       // "§14 Abs. 5" Anzahlung, "§13b" reverse charge
    EnFG     { paragraph: &'static str },       // "§§21 ff." Letztverbrauchergruppe
    Sect14aEnwg { module: Sect14aModule },      // Modul1 | Modul2 | Modul3
    MsbG     { paragraph: &'static str },       // "§§6–7"
    BnetzaDecision { reference: &'static str }, // "BK8-22/010-A"
    BdewAhb  { reference: &'static str },       // "GPKE BK6-22-024"
    StromNzv { paragraph: &'static str },       // "§13 Abs. 3" — außer Kraft seit 01.01.2026
    GasNzv   { paragraph: &'static str },       // "§25" — außer Kraft seit 01.01.2026
    Enwg     { paragraph: &'static str },       // "§14a"
    ARegV    { paragraph: &'static str },       // "§17" incentive regulation
}
```

`.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`

```rust
pub enum Sect14aModule {
    Modul1, // pauschale Reduzierung des Netzentgelts — flat, needs no extra metering
    Modul2, // prozentuale Reduzierung des Arbeitspreises — needs the device's own metering
    Modul3, // zeitvariable Netzentgelte HT/ST/NT (from 01.04.2025) — Zählzeitdefinition
            // from UTILTS, requires an iMSys; also the Spotpreis-Netzentgelt variant
}
```

`Sect14aModule::Modul1.label()` = `"§14a EnWG Modul 1 (pauschale Reduzierung)"`;
`.bnentza_reference()` = `"BK8-22/010-A"` for all three modules.

### `TariffSource`

```rust
pub enum TariffSource {
    PublishedTariffSheet { sheet_id: String },
    HistoricalTariff     { valid_from: time::Date },
    RegulatoryTariff     { decision_ref: &'static str },
    ContractTariff       { contract_ref: String },
    ManualOverride       { reason: String },
}
```

### `Sparte` — commodity dispatch

```rust
#[derive(Default)]
pub enum Sparte {
    #[default]
    Strom,  // → StromNEV §21, SettlementType::NneStrom, PID 31002 (NN-Rechnung)
    Gas,    // → GasNEV §14,   SettlementType::NneGas,   PID 31002 (NN-Rechnung)
}
```

`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`

```rust
pub enum SettlementType {
    NneAbschlag,       // PID 31001 — Abschlagsrechnung Netznutzung (NB → LF)
    NneStrom,          // PID 31002 — NN-Rechnung Strom (NB → LF)
    NneGas,            // PID 31002 — NN-Rechnung Gas  (GNB → LFG)
    MmmStrom,          // PID 31005 — MMM Strom, GPKE (BK6-24-174) Teil 1 Kap. 8.4
    MmmGas,            // PID 31005 — MMM Gas,   GaBi Gas 2.1 (BK7-24-01-008) (separate to ensure correct legal refs)
    MmmSelbstausstellt,// PID 31006 — MMM Mehrmenge, selbst ausgestellte Rechnung (Lieferung)
    MsbRechnung,       // PID 31009 — MSB-Rechnung (MSB → NB / LF / ESA)
    GasAwhSperrung,    // PID 31011 — AWH Sperrprozesse Gas (GNB → LFG)
    RedispatchKostenblatt, // no standard PID — Redispatch 2.0 Einsatzkosten (NB → ÜNB)
    DezentraleEinspeisung, // no standard PID — §18 StromNEV, NB → Anlagenbetreiber (bilateral)
}
```

`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 31005, and `NneGas` and
`NneStrom` share 31002, but each pair carries different legal references.

### `BillingPositionKind` — BDEW Artikelnummern bridge

`BillingPositionKind` is the rubo4e-free type carried by every `SettlementPosition.kind`.
`bo4e::kind_to_artikelnummer` maps it to `rubo4e::current::BdewArtikelnummer`
under the `bo4e` feature; the core engine only ever holds the codelist name.

```rust
pub enum BillingPositionKind {
    NneAbschlag,         // the single line of an Abschlagsrechnung — no article number
    NneArbeit,           // Wirkarbeit       (9990001 00026 9)
    NneArbeitHt,         // Wirkarbeit       (9990001 00026 9) — §14a Modul 3 Hochtarif
    NneArbeitSt,         // Wirkarbeit       (9990001 00026 9) — §14a Modul 3 Standardtarif
    NneArbeitNt,         // Wirkarbeit       (9990001 00026 9) — §14a Modul 3 Niedertarif
    NneArbeitModul1,     // Wirkarbeit       (9990001 00026 9) — Modul 1 Arbeit + pauschale credit
    NneArbeitModul2,     // Wirkarbeit       (9990001 00026 9) — Modul 2 reduced Arbeitspreis
    NneArbeitModul3,     // Wirkarbeit — §14a Modul 3 spot, one position per dispatch interval
    NneLeistung,         // Leistung         (9990001 00005 3)
    NneGasGrundpreis,    // Grundpreis       (9990001 00008 7)
    Konzessionsabgabe,   // Konzessionsabgabe(9990001 00041 7)
    Mehrmenge,           // Mehrmenge        (9990001 00074 8)
    Mindermenge,         // Mindermenge      (9990001 00075 6)
    MsbGrundgebuehr,     // EntgeltEinbauBetriebWartungMesstechnik (9990001 00061 5)
    Messdienstleistung,  // EntgeltMessungAblesung (9990001 00062 3)
    GasAwhSperrung,      // Sperrkosten — Artikel-ID "2-01-7-001" (BK7-24-01-009 §5.4)
    GasAwhEntsperrung,    // Entsperrkosten — Artikel-ID "2-01-7-002"
    GasAwhSonstige,      // Artikel-ID from AwhPositionInput.artikel_id
    Blindmehrarbeit,     // Blindmehrarbeit  (9990001 00047 5)
    Sect19StromNevUmlage,// §19 StromNEV-Umlage — artikelnummer PARAGRAF_19_STROM_NEV_UMLAGE
    OffshoreNetzumlage,  // §17f EnWG — artikelnummer OFFSHORE_HAFTUNGSUMLAGE (legacy code name)
    KwkgUmlage,          // §26 KWKG — artikelnummer ABGABE_KWKG
    DezentraleEinspeisung,   // §18 StromNEV payment out (negative rate); no article number
    Sect19IndividuellesEntgelt, // §19 Abs. 2 StromNEV reduction over the Netzentgelt; no article number
    GasKapazitaetsentgelt,   // §15 GasNEV booked capacity — Leistung on Gas
}
```

> **NNE Strom (PID 31002):** BK6-20-160 replaced classic `artikelnummer` codes
> with `artikel_id` from the BNetzA Netznutzungspreisblatt. The service layer
> (`netzbilanzd`, `invoicd`) populates `Rechnungsposition.artikel_id` from the tariff
> sheet for those positions; `BillingPositionKind::artikelnummer(settlement_type)`
> returns `None` for 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.

```rust
pub enum KaKundengruppe {
    Tarifkunde {                     // KAV §2 Abs. 2 — rate bands on municipality size
        gemeinde: GemeindeGroesse,
        nur_kochen_warmwasser: bool, // Gas only: cooking/hot-water column vs übrige; ignored for Strom
    },
    Schwachlast,          // KAV §2 Abs. 2 — Strom only; gas has no such tier
    Sondervertragskunde,  // KAV §2 Abs. 3 — flat, independent of municipality size
    Exempt,               // KAV §2 Abs. 4 (Strom) / Abs. 5 (Gas) — keine KA zulässig
}

pub enum GemeindeGroesse {
    Bis25k,    // bis 25 000 Einwohner
    Bis100k,   // bis 100 000
    Bis500k,   // bis 500 000
    Ueber500k, // über 500 000
}
```

`KaKundengruppe::hoechstsatz_ct_per_kwh(sparte)` returns the statutory KAV §2
Höchstbetrag (or `None` for `Exempt`, and for `Schwachlast` on Gas).
`.kav_paragraph(sparte)` returns the paragraph the group is governed by
(`"§2 Abs. 2"`, `"§2 Abs. 3"`, or — where no Konzessionsabgabe may be charged at
all — `"§2 Abs. 4"` for Strom and `"§2 Abs. 5"` for Gas) and `.label(sparte)` 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 where the Verordnung forbids one.

The two prohibitions are Abs. 4 and Abs. 5, not Abs. 7. Abs. 4 forbids a
Konzessionsabgabe for Strom-Sondervertragskunden below the Grenzpreis, Abs. 5 for
Gas above 5 Millionen kWh je Jahr und Abnahmefall or below the Gas-Grenzpreis.
**Abs. 7 is a classification rule**: a Niederspannungslieferung counts as a
Tariflieferung unless the gemessene Leistung exceeds 30 kW in at least two months
of the Abrechnungsjahr *and* the Jahresverbrauch exceeds 30 000 kWh. Deciding
that is the caller's, which is why the group arrives with the rate.

## Who uses this library

| Consumer | Role | Use case |
|---|---|---|
| `netzbilanzd` | **NB** (and **MSB** for 31009) | Generate INVOIC 31001/31002/31005/31011 to LF/LFG, and 31009 from the MSB to NB/LF/ESA |
| `invoicd` | **LF** | INVOIC PID 31006, selbst ausgestellt — same formula, LF-initiated |

## Quick start

```bash
cargo add grid-billing
cargo add rust_decimal time
```

### NNE flat-rate (SLP, Strom)

```rust,no_run
use grid_billing::{NneInput, Sparte, SettlementType, settle_nne};
use grid_billing::types::{
    ArbeitspreisModell, MengePreis, Konzessionsabgabe, KaKundengruppe, SettlementPeriod,
};
use grid_billing::umlagen::Letztverbrauchergruppe;
use rust_decimal::Decimal;
use time::macros::date;

fn d(s: &str) -> Decimal { Decimal::from_str_exact(s).unwrap() }

let settlement = settle_nne(&NneInput {
    malo_id: "51238696012".into(),
    nb_mp_id: "9900357000004".into(),
    lf_mp_id: "9900012345678".into(),
    // The delivery period is a validated pair — inverted bounds are unrepresentable.
    period: SettlementPeriod::new(date!(2026-01-01), date!(2026-01-31)).unwrap(),
    // Letztverbrauchergruppe drives the network-levy rates (EnFG §§21 ff.).
    letztverbrauchergruppe: Letztverbrauchergruppe::A,
    // A′ has no EnFG 1-GWh boundary, so the year to date does not place it.
    enfg_jahresvorverbrauch_kwh: None,
    // Exactly one Arbeitspreis form — here a single flat rate.
    arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
        menge_kwh: d("1500"),
        preis_ct_per_kwh: d("3.5"),
    }),
    leistungspreis: None,   // SLP — no RLM demand charge
    grundpreis: None,       // Strom has no separate Grundpreis
    konzessionsabgabe: Some(Konzessionsabgabe {
        satz_ct_per_kwh: d("0.11"),
        klasse: KaKundengruppe::Sondervertragskunde,
    }),
    sparte: Sparte::Strom,
    tariff_sheet_id: Some("Preisblatt-NNE-2026-Q1".into()),
    netzebene: None,
    jahreshoechstleistung_kw: None,
    jahresarbeit_kwh: None,
    sect19: None,            // no §19 Abs. 2 individual charge
    blindarbeit: None,       // no reactive-energy charge
    gas_kapazitaet: None,
    sect19_umlage_ct_per_kwh: None,   // use the tabled rate for the delivery year/group
    offshore_umlage_ct_per_kwh: None,
    kwkg_umlage_ct_per_kwh: None,
}).expect("valid NNE input");

// 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!(settlement.settlement_type, SettlementType::NneStrom);
assert_eq!(settlement.settlement_type.default_pid(), 31002);
// recipient_mp_id is auto-populated from lf_mp_id:
assert_eq!(settlement.recipient_mp_id, "9900012345678");

// 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 {
    println!("{}: {}", pos.text, pos.trace.explanation);
    for lr in &pos.trace.legal_refs {
        println!("  → {}", lr.citation());
    }
}
```

### NNE Gas (GasNEV §14)

```rust,no_run
use grid_billing::{NneInput, Sparte, SettlementType, settle_nne};
use grid_billing::types::{ArbeitspreisModell, MengePreis};

// Only Sparte changes — GasNEV §14 legal refs and SettlementType::NneGas are automatic:
let settlement = settle_nne(&NneInput {
    sparte: Sparte::Gas,  // ← drives GasNEV §14 + NneGas (PID 31002)
    arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
        menge_kwh: d("3000"),        // already kWh_Hs from edmd gas conversion
        preis_ct_per_kwh: d("1.80"),
    }),
    konzessionsabgabe: None,  // KA typically not applicable for Gas
    grundpreis: None,
    leistungspreis: None,
    // … other identity + levy-override fields, all None …
}).unwrap();

assert_eq!(settlement.settlement_type, SettlementType::NneGas);
assert_eq!(settlement.settlement_type.default_pid(), 31002);
```

### §14a Modul 3 — zeitvariable Netzentgelte (HT/ST/NT, opt-in since 2025-04-01)

```rust,no_run
use grid_billing::{NneInput, Sparte, settle_nne};
use grid_billing::types::{
    ArbeitspreisModell, MengePreis, Konzessionsabgabe, KaKundengruppe, GemeindeGroesse,
};

let settlement = settle_nne(&NneInput {
    // Modul 3 requires all three bands; the enum makes the flat/ToU states exclusive.
    arbeitspreis: ArbeitspreisModell::Modul3ZeitVariabel {
        ht: MengePreis { menge_kwh: d("600"), preis_ct_per_kwh: d("4.20") },
        st: MengePreis { menge_kwh: d("250"), preis_ct_per_kwh: d("2.80") },
        nt: MengePreis { menge_kwh: d("400"), preis_ct_per_kwh: d("1.50") },
    },
    konzessionsabgabe: Some(Konzessionsabgabe {
        satz_ct_per_kwh: d("1.32"),
        // The group fixes the KAV §2 ceiling and annotates the position for audit.
        klasse: KaKundengruppe::Tarifkunde {
            gemeinde: GemeindeGroesse::Bis25k,
            nur_kochen_warmwasser: false,
        },
    }),
    sparte: Sparte::Strom,
    tariff_sheet_id: Some("Preisblatt-14a-2026".into()),
    leistungspreis: None,
    grundpreis: None,
    // … identity + levy-override fields …
}).unwrap();

// Positions: HT + ST + NT Arbeit, the three netzseitige Umlagen, and Konzessionsabgabe.
assert!(settlement.all_legal_refs().iter().any(|r| r.contains("§14a EnWG Modul 3")));
```

### §14a Modul 1 — pauschale Reduzierung (offered since 2024-01-01)

Modul 1 is a **flat annual amount**, credited pro rata for the settlement period.
It does not scale with consumption — that is what makes it *pauschal*, and what
separates it from Modul 2, which reduces the Arbeitspreis by a percentage. The
energy is billed at the full Arbeitspreis and the credit sits beside it as its
own position, so the invoice shows both.

```rust,no_run
use grid_billing::{Jahresanteil, NneInput, Sparte, settle_nne};
use grid_billing::types::{ArbeitspreisModell, MengePreis};
use rust_decimal::{Decimal, dec};

let settlement = settle_nne(&NneInput {
    // Modul 1 is a variant of ArbeitspreisModell, so it cannot coexist with a
    // flat rate or the Modul 3 bands — the conflict is unrepresentable.
    arbeitspreis: ArbeitspreisModell::Modul1Pauschal {
        basis: MengePreis { menge_kwh: d("1500"), preis_ct_per_kwh: d("3.5") },
        // The NB's published annual amount, and the share of a year this
        // period covers.
        pauschale_eur_pro_jahr: dec!(120.00),
        jahresanteil: Jahresanteil::MONAT,
    },
    sparte: Sparte::Strom,
    leistungspreis: None,
    grundpreis: None,
    konzessionsabgabe: None,
    // … other fields …
}).unwrap();

// Two positions: 1500 kWh × 0.035 = 52.50 EUR Arbeit, billed in full, and the
// credit beside it — 0.083333 of a year × −120.00 EUR/Jahr = −9.99996 EUR.
assert!(settlement.all_legal_refs().iter().any(|r| r.contains("Modul 1")));
assert_eq!(settlement.positions[1].unit_price_eur, dec!(-120.000000));
```

### Gas NNE with Grundpreis (GasNEV monthly standing charge)

```rust,no_run
use grid_billing::{NneInput, Sparte, settle_nne};
use grid_billing::types::{ArbeitspreisModell, MengePreis, Grundpreis};

let settlement = settle_nne(&NneInput {
    sparte: Sparte::Gas,
    arbeitspreis: ArbeitspreisModell::Einheitlich(MengePreis {
        menge_kwh: d("3000"),
        preis_ct_per_kwh: d("1.80"),
    }),
    // Grundpreis pairs the monthly rate with the months billed — one without the
    // other is meaningless, so they travel together.
    grundpreis: Some(Grundpreis {
        eur_per_month: d("15.00"),  // monthly base fee from PreisblattNetznutzung
        months: d("1"),
    }),
    leistungspreis: None,
    konzessionsabgabe: None,
    // … other fields …
}).unwrap();

// Gas carries no netzseitige Umlagen: Grundpreis (15.00) + Arbeit (54.00) = 69.00 EUR
assert_eq!(settlement.positions.len(), 2);
assert!(settlement.positions[0].text.contains("Grundpreis"));
```

### GeLi Gas AWH Sperrprozesse (PID 31011)

```rust,no_run
use grid_billing::{GasAwhInput, AwhPositionInput, SettlementType, settle_gas_awh};
use grid_billing::types::SettlementPeriod;
use time::macros::date;

let settlement = settle_gas_awh(&GasAwhInput {
    malo_id: "51238696012".into(),
    nb_mp_id: "9900357000004".into(),
    lf_mp_id: "9900012345678".into(),
    period: SettlementPeriod::new(date!(2026-01-01), date!(2026-01-31)).unwrap(),
    tariff_sheet_id: Some("Preisblatt-AWH-2026".into()),
    awh_positionen: vec![
        AwhPositionInput {
            beschreibung: "Sperrung Gaszähler".into(),
            anzahl: 1,
            preis_eur: d("45.00"),
            artikel_id: Some("2-01-7-001".to_owned()),  // BDEW Codeliste v5.6 §3.2
        },
        AwhPositionInput {
            beschreibung: "Entsperrung Gaszähler".into(),
            anzahl: 1,
            preis_eur: d("45.00"),
            artikel_id: Some("2-01-7-002".to_owned()),
        },
    ],
}).unwrap();

// Invoice number and dates live on InvoiceDocument, not on the settlement:
assert_eq!(settlement.settlement_type, SettlementType::GasAwhSperrung);
assert_eq!(settlement.settlement_type.default_pid(), 31011);
assert_eq!(settlement.total_eur, d("90.00"));
// Both positions cite BK7-24-01-009 §5.4
assert!(settlement.all_legal_refs().iter().any(|r| r.contains("BK7-24-01-009")));
```

### Correction lifecycle (reversal + replacement pair)

Two different facts are recorded in two different places, and the split is
deliberate:

- **What was replaced** — invoice numbers — lives on the `InvoiceDocument`.
  The same pair of settlements can be presented under different invoice numbers,
  so the chain is a property of the documents exchanged.
- **Why the recalculation happened**`KorrekturGrund` — lives on the
  `SettlementResult`. That is a fact about the settlement, and the invoice
  numbers never answer it: they cannot say whether the meter was wrong, the
  tariff was wrong, or the law changed underneath. Those have different
  consequences, so `reverse()` and `correct()` require the reason.

```rust,no_run
use grid_billing::{settle_nne, correct, KorrekturGrund, SettlementStatus};

let original = settle_nne(&nne_input).unwrap();
let corrected = settle_nne(&corrected_input).unwrap();

let (reversal, replacement) =
    correct(&original, corrected, KorrekturGrund::Tarifkorrektur);

assert_eq!(reversal.status, SettlementStatus::Reversal);
assert_eq!(reversal.total_eur, -original.total_eur);
assert_eq!(replacement.status, SettlementStatus::Correction);
assert_eq!(replacement.korrektur_grund, Some(KorrekturGrund::Tarifkorrektur));
assert!(replacement.lineage_is_consistent());
```

| `KorrekturGrund` | Meaning | Defect? |
|---|---|---|
| `Messwertkorrektur` | replaced or re-read metering (§ 60 Abs. 1 MsbG — the Aufbereitungs- und Übermittlungspflicht the corrected value discharges) | no |
| `Tarifkorrektur` | wrong tariff or price-sheet version applied | no |
| `Stammdatenkorrektur` | wrong Netzebene, KA-Klasse or Konzessionsgemeinde | **yes** |
| `RegulatorischeAenderung` | a regulatory change applies retroactively | no |
| `Rechenfehler` | arithmetic or logic error in the original | **yes** |
| `Clearing` | a clearing result between the parties (MMM, MaBiS) | no |
| `Sonstiges` | anything else — detail rides in the warnings | no |

`indicates_defect()` separates the two: a rising `Rechenfehler` count is an
engineering signal, a rising `RegulatorischeAenderung` count is not.
`lineage_is_consistent()` catches the state this exists to prevent — a
`Correction` with no reason, which looks like a complete settlement and answers
none of the questions an audit asks of one.

```rust,no_run
use grid_billing::{settle_nne, reverse, KorrekturGrund, SettlementStatus};

let original = settle_nne(&/* … NneInput … */).unwrap();

// reverse() mirrors every position with the sign flipped. It takes the reason
// too — the storno invoice number and dates belong to the InvoiceDocument, but
// *why* the settlement was withdrawn is a fact about the settlement.
let storno = reverse(&original, KorrekturGrund::Messwertkorrektur);

assert_eq!(storno.status, SettlementStatus::Reversal);
assert_eq!(storno.total_eur, -original.total_eur);
```

### Pre-calculation validation

```rust,no_run
use grid_billing::{MmmInput, validate_mmm_input};

let input = MmmInput { /* … */ };
let v = validate_mmm_input(&input);

if !v.is_valid {
    for w in &v.warnings {
        eprintln!("[{}] {}", w.code, w.message);
    }
    return;
}
let settlement = grid_billing::settle_mmm(&input).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.)

**Running them is not optional.** `ValidationResult::is_valid` goes false on any
`Error`-severity finding, and for a long time nothing read it: the engine
computed the finding, attached it to the result, and the service billed the
settlement anyway. An MSB input with `billing_months = 0` produced a Grundgebühr
of nothing; a Gas-AWH input with no positions produced an empty invoice — both
sent to a counterparty who then had to dispute them back. `netzbilanzd` and
`invoicd` now refuse an invalid input before settling, naming the finding.

### Conversion to a BO4E `Rechnung` (feature `bo4e`)

```rust,no_run
use grid_billing::{InvoiceDocument, bo4e};

let rechnung: rubo4e::current::Rechnung = bo4e::into_rechnung(&document);
```

`into_rechnung` takes the **document**, not the settlement: `rechnungsnummer`,
`rechnungsdatum` and `faelligkeitsdatum` are document facts, and the 1-based
`positionsnummer` is assigned here rather than carried through the calculation.
Each position's `artikelnummer` comes from `kind_to_artikelnummer`, which is
`None` where an Artikel-ID applies instead; `einzelpreis` is rounded to 6 dp and
`gesamtpreis` to 5, the same figures the settlement states.

The trace travels with the position it explains, as a `mako:calculation_trace`
ZusatzAttribut, alongside settlement-level `mako:legal_references` and
`mako:settlement_warnings`. Once the Rechnung is stored the `SettlementResult`
is dropped, so those attributes are the only surviving record of why an amount
is what it is — and what a §20 EnWG audit or an LF dispute is answered from.

## 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 | **MSB → NB / LF / ESA** | Strom |
| 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–2 | `Netznutzung Arbeit (§14a Modul 1)` + `§14a Modul 1 pauschale Reduzierung` | kWh · Jahr | `NneArbeitModul1` | `arbeitspreis: ArbeitspreisModell::Modul1Pauschal` | §14a EnWG Modul 1 · BK8-22/010-A | same as NneArbeit |
| 1–3 | `Netznutzung Arbeit HT/ST/NT (§14a Modul 3)` | kWh | `NneArbeitHt` / `NneArbeitSt` / `NneArbeitNt` | `arbeitspreis: ArbeitspreisModell::Modul3ZeitVariabel` | §14a EnWG Modul 3 · BK8-22/010-A | same as NneArbeit |
| opt | `Netzentgelt Grundpreis Gas` | Monat | `NneGasGrundpreis` | `grundpreis` set | GasNEV §14 | `Grundpreis` |
| next | `Netznutzung Leistung` | kW | `NneLeistung` | `leistungspreis` set (RLM) — the Jahresleistungspreis pro-rated by calendar days | StromNEV §17 Abs. 2 | `Leistung` (Gas); `artikel_id` (Strom) |
| next | `Blindmehrarbeit` | kvarh | `Blindmehrarbeit` | `blindarbeit` set **and** the draw exceeds the free share | StromNEV §17 (Preisblatt) | `Blindmehrarbeit` |
| last | `Konzessionsabgabe[tier]` | kWh | `Konzessionsabgabe` | `konzessionsabgabe` set | KAV §2 Abs. 2 | `Konzessionsabgabe` |

#### Blindmehrarbeit

A Netzbetreiber supplies a *free share* of reactive energy alongside the active
energy and charges only what exceeds it. The customary boundary is a power factor
of cos φ 0,9 — reactive energy up to **tan φ ≈ 0,4843** of the active energy —
but many Preisblätter round that to a flat 50 %, and some set separate shares for
inductive and capacitive draw.

The share is therefore an **input**, not a constant: it is a term of the price
sheet, and hard-coding one would bill some networks wrongly.
`Blindarbeit::COS_PHI_0_9` is the documented default.

```rust,no_run
use grid_billing::{Blindarbeit, NneInput};
use rust_decimal::dec;

let blindarbeit = Some(Blindarbeit {
    blindarbeit_kvarh: dec!(600),
    freigrenze_anteil: Blindarbeit::COS_PHI_0_9,
    preis_ct_per_kvarh: dec!(2.0),
});
// 1 000 kWh active → 484,3 kvarh free → 115,7 kvarh charged.
```

An unused allowance is never a credit — the excess floors at zero. The charge
rests on the Netzbetreiber's published Preisblatt, formed under **StromNEV §17**;
it is not §18 (Entgelt für dezentrale Erzeugung) and not §19 (Sonderformen der
Netznutzung).

### MMM

Both positions are always emitted; the one that does not apply carries a zero
quantity. The sign convention is GPKE's, from the network operator's side:
measured **below** profile is an ungewollte Mehrmenge the NB reimburses, and
measured above it is an ungewollte Mindermenge the NB invoices.

| # | Position text | `kind` | Artikelnummer | Condition |
|---|---|---|---|---|
| 1 | `Mehrmengen (Gutschrift)` | `Mehrmenge` | `Mehrmenge` | `actual < profil` — the Mehrmengenpreis is negated, so the position multiplies out |
| 2 | `Mindermengen` | `Mindermenge` | `Mindermenge` | `actual > profil` |

### 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; `EuroAmount` for overflow guard. No `f64`. |
| **rubo4e is opt-in** | The engine returns `SettlementResult` and depends on no BO4E type; `bo4e::into_rechnung()` ships behind the off-by-default `bo4e` feature. |
| **`recipient_mp_id` auto-populated** | `lf_mp_id` (NNE/MMM) or `empfaenger.mp_id` (PID 31009) copied automatically; `sender_mp_id` is the NB, or the **MSB** for 31009. |
| **`Sparte` drives settlement type** | `Sparte::Gas``SettlementType::NneGas`, `GasNEV §14`. NN-Rechnung is PID 31002 for both Sparten — the Sparte rides on `Rechnung.sparte`, not on the Prüfidentifikator. |
| **Every position cites regulation** | `trace.legal_refs` is non-empty for every position. Enables BNetzA audit without re-calculation. |
| **Artikelnummer decided in this crate** | `BillingPositionKind::artikelnummer(settlement_type)` returns the codelist name, or `None` where the position carries an Artikel-ID instead (Strom NNE, AWH Gas, Abschlag). The decision is never left to the renderer. |
| **`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. |
| **Abschläge reduce `zuZahlen` only** | `gesamtnetto` and `gesamtsteuer` stand; §14 Abs. 5 UStG taxes an Anzahlung on receipt, so the settling invoice does not tax it again. |
| **Cadence is a document fact** | `IMD+7081` rides on `InvoiceDocument::cadence`, not on `SettlementType`: the same NNE settlement is the same arithmetic at any billing rhythm. The two exceptions are decided by the settlement — MMM and the Abschlag, whose PID *is* its cadence. |
| **Pure functions** | All settlement functions are sync with no side effects. |
| **`recomputed_total` guard** | `debug_assert_eq!(result.total_eur, result.recomputed_total())` inside `settle_nne` and `settle_gas_awh` — catches rounding bugs in debug builds. `settle_mmm` is asserted in the test suite; `settle_abschlag`, `settle_msb` and `settle_dezentrale_einspeisung` are not yet covered either way. |

## Related crates

| Crate | Role |
|---|---|
| [`grid-billing`]https://docs.rs/grid-billing**this crate** | Grid-side settlement arithmetic — NNE, KA, MMM, MSB, Sperrprozesse Gas |
| [`mako-markt`]https://docs.rs/mako-markt | Marktstammdaten — Marktlokation, Messlokation, Marktpartner, Rollenzuordnung — the model the settlement inputs are keyed on |
| [`invoic-checker`]https://docs.rs/invoic-checker | Validates the resulting `Rechnung` in the service layer |
| [`energy-billing`]https://docs.rs/energy-billing | The retail (LF) counterpart — the customer's invoice |
| [`mako-invoic`]https://docs.rs/mako-invoic | The INVOIC settle/dispute workflow these invoices travel through |
| [`netzbilanzd`]https://hupe1980.github.io/mako/docs/services/netzbilanzd/ · [`invoicd`]https://hupe1980.github.io/mako/docs/services/invoicd/ | Production daemons — NB billing, and LF selbstausstellen |

Part of **mako**, an open-source Rust platform for German energy market
communication (Marktkommunikation). Full documentation: <https://hupe1980.github.io/mako/>