energy-billing 0.20.0

Pure multi-product retail utility billing for German markets — Strom, Gas, Wärme, Wasser and the § 14a tariffs. 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
# energy-billing

**Pure multi-product retail energy billing library for German markets.**

`energy-billing` is the calculation core of [`billingd`](https://hupe1980.github.io/mako/docs/services/billingd/) — the
Energy Billing Engine daemon for the Lieferant (LF) role. The library is **zero I/O,
zero async, zero hardcoded regulatory rates**. It answers one question:

> Given a product definition, meter readings, and statutory rates —
> what does the customer's invoice look like?

---

## §42 Stromkennzeichnung — structured, and on the invoice

`BillingContext.energiequellen` carries the typed `EnergieQuellen` (fuel-mix
percentages, the CO₂ g/kWh figure §42 Abs. 2 Nr. 2 EnWG makes mandatory, HKN
certification) and `to_rechnung_json` emits it as the `stromkennzeichnung`
ZusatzAttribut with the structure intact. billingd copies it from the productd
product via `Product::energiequellen()`.

The invoice emits the structured type — there is no free-text Energiemix
field.

## §14a — all three modules

BK8-22/010-A defines exactly three, and their numbering matters — it is printed on
the invoice and shared with the NB-side `grid-billing` engine:

| Modul | What it is | Fields |
|---|---|---|
| **1** | *pauschale Reduzierung des Netzentgelts* — a flat reduction needing no extra metering, hence the default where no choice is made | `sect14a_modul1_pauschale_eur_per_year` |
| **2** | *prozentuale Reduzierung des Arbeitspreises* — attaches to the device's **separately metered** energy | `sect14a_modul2_nne_reduktion_ct_per_kwh` |
| **3** | *zeitvariable Netzentgelte* (from 01.04.2025) — **three** Tarifstufen HT/ST/NT, requires an iMSys | `sect14a_modul3_nne_*` + `Sect14aModul3Verbrauch` |

**`Modul 1 + Modul 3` is the only pair BK8-22/010-A offers.** Modul 1 and Modul 2 are
the two forms the base module takes and the Anschlussnutzer picks one; configuring
both raises the Error-severity `MODUL1_AND_MODUL2` and the run is refused. Modul 2 and
Modul 3 both re-price the Arbeitspreis, so holding both reduces the same network usage
twice; configuring both raises `MODUL2_AND_MODUL3`.

The Modul 3 bands *replace* the flat NNE Arbeitspreis; setting both raises
`MODUL3_AND_FLAT_NNE` for the same reason.

A **Steuerungsentschädigung** (`sect14a_steuerungsentschaedigung_*`) is compensation
for a dispatch that actually happened. It is deliberately not numbered: all three
BK8-22/010-A modules are rate reductions, none of them a payment for a Steuerungseingriff. The bands come from the
Netzbetreiber's time windows, which is why they are not derived from the
supplier's own HT/NT split.

## Warnings that actually fire

`invoice.warnings` carries machine-readable codes beyond the §41a guard:
`ESTIMATED_READING` (§ 40a Abs. 2 EnWG), `PREISGARANTIE_ENDET` (ends within 30
days of the period), `VERBRAUCH_ABWEICHUNG_50PCT` (deviation beyond half the
prior year's consumption). They are `Warning` severity: they inform dispatch,
they do not block it.

Every position built through the shared helpers (Arbeitspreis, Grundpreis, and
all levy positions — Stromsteuer, Energiesteuer, BEHG, KA, NNE) carries a
populated `PositionTrace`.

## Explainability reaches the stored invoice

Every `BillingPosition` carries a `PositionTrace` (formula, inputs, §-citations,
tariff source). `to_rechnung_json()` emits it per position as the
`mako:calculation_trace` ZusatzAttribut — BO4E has no field for a calculation
trace, and the attribute is the sanctioned place for what the schema does not
model. This is the only surviving record of *why* an amount is what it is once
the `Invoice` value is dropped after storage; billingd's
`explain_invoice_position` MCP tool reads it from there.

## Two boundary representations: BO4E and EN 16931

`to_rechnung()` (feature `bo4e`) produces the BO4E `Rechnung` for accounting.
`to_en16931(spec_id, seller, buyer)` (feature `en16931`) produces the
[`en16931::Invoice`](https://docs.rs/en16931) semantic model that
`en16931-formats` renders to XRechnung/CII and PEPPOL UBL (its `zugferd` PDF/A-3
feature exists but is not enabled here). The map runs
here — where every position still carries its own VAT category and rate — so each
BG-25 line keeps a correct BT-151/152, and the BG-23 breakdown plus BG-22 totals
are derived from the rounded line amounts so BR-CO-10/13 and BR-S-08 reconcile.
E-invoicing does not round-trip through BO4E.

**Statutory disclosures become BG-1 notes.** An `Info` position charges nothing —
the CO2KostAufG § 3 figures, the § 53a Entlastungshinweis, the § 40 Abs. 2 Nr. 6
Energieträgermix, the § 14 WPG share, the § 41a Abs. 6 comparison — so it is no
BG-25 line. The law requires each *auf der Rechnung* and the XRechnung **is** the
invoice a B2G/B2B recipient receives, so each becomes its own BT-22 note. The legal
basis is appended only when it reads as a citation, because the Gasqualität position
stores its value in `legal_basis` for `to_rechnung_json`.

**Both mappings emit net supply lines only.** `Tax` and `Abschlag` positions
live in this crate's flat position vector so one pass can compute everything,
but neither is an invoice *line* at either boundary: EN 16931 carries them as
BG-23 and BT-113, and BO4E as `steuerbetraege`/`gesamtsteuer` and
`vorauszahlungen`/`zuZahlen`. Emitting them as lines states each amount twice
and leaves the document's own totals irreconcilable —
`BillingPosition::is_rechnungsposition` is the single predicate both mappings
use, so they cannot drift apart again. `Info` positions do belong: they carry
`net_eur == 0`, so they change no sum, and § 40 EnWG wants the Zählerstand and
Brennwert lines on the document.

Every shape `to_rechnung()` can emit is asserted against mako's outbound BO4E
gate in `tests/golden_scenarios.rs` — out-of-schema enums *and* the rules BO4E
states about an invoice's totals. mako refuses a received document that breaks
those, so it must not emit one.

`to_en16931` is **fallible**, for one reason: EN 16931 category `O` (*not
subject to VAT*) is exclusive to its document under **BR-O-11 … BR-O-14**. A
hoheitliche Abwassergebühr is category `O`, so a combined
Trinkwasser-plus-Gebühr invoice — the shape over 90 % of German municipalities
bill in — has no valid rendering. The engine still produces the combined paper
document (with a `GEBUEHR_UND_ENTGELT_AUF_EINEM_BELEG` warning, because that
statement is lawful); it is the *e-invoice* that is refused, with the reason,
instead of handing the recipient a file their schematron rejects days later.

## Period-correct rates

The year tables (`stromsteuer_for_year`, `energiesteuer_gas_for_year`,
`behg_ct_per_kwh_for_year`) are joined by `mwst_rate_for_period`: 19 % since
2007 except the COVID window 01.07.2020–31.12.2020 at 16 %. A period straddling
the window yields `None` — no single rate is correct for it, so the caller
splits rather than misbilling half of it. billingd derives its default
`RegulatoryRates` from these tables per billing period; explicit configuration
still wins.

## Pro-rating conventions (stated, deliberately)

Everything is clipped to the **active contract window** — `vertragsbeginn` /
`vertragsende` intersected with the billing period — and then expressed in the
unit its price is quoted in:

| Price quoted in | Billed as | Helper |
|---|---|---|
| EUR/day, ct/day | active contract days | `ctx.prorate_days().0` |
| EUR/month | calendar-exact months | `ctx.billed_months()` |
| EUR/year, EUR/kW·a | calendar-exact years | `ctx.billed_years()` |

**Calendar-exact** means each calendar month contributes `billed days ÷ that
month's own length` and each year `billed days ÷ that year's length`. January
1–31 is exactly `1` month, a full year exactly `12`, a leap year exactly `1`
year, and 16–31 January exactly `16/31` — none of which an average-month
divisor produces.

## Typed errors

`BillingEngine::bill` returns `EngineError`, not a stringly error:

| Variant | Meaning | `code()` |
|---|---|---|
| `ValidationBlocked { warnings }` | `Error`-severity regulatory warnings blocked the run — carries **all** collected warnings | `VALIDATION_BLOCKED` |
| `PriceOutOfRange { field, value }` | A tariff price exceeds the monetary range (corrupt tariff) | `PRICE_OUT_OF_RANGE` |
| `InvalidPeriod { from, to }` | What `BillingPeriod::new` returns for `from > to` | `INVALID_PERIOD` |
| `AllocationMismatch { fractions, contexts }` | `allocate_proportionally` shape mismatch | `ALLOCATION_MISMATCH` |
| `AllocationWeightsInvalid { sum }` | a negative allocation weight, or weights summing to zero | `ALLOCATION_WEIGHTS_INVALID` |
| `NutzungsplanSharesInvalid { sum }` | §42b GGV Nutzungsplan shares that do not sum to 1 | `NUTZUNGSPLAN_SHARES_INVALID` |
| `Unrepresentable { field, value }` | a date or amount the EN 16931 semantic model cannot carry | `UNREPRESENTABLE` |
| `ReconciliationFailed { reason }` | `to_en16931` could not derive BG-22/BG-23 — refused rather than substituted | `RECONCILIATION_FAILED` |
| `Arithmetic(billing::BillingError)` | Passthrough from the arithmetic core | `ARITHMETIC` |

`code()` is stable and machine-readable; `blocking_warnings()` exposes the
warnings behind a blocked validation so services can answer with structured
error bodies instead of parsed prose.


### Refusals, not silent zeros

A pairing the engine cannot price refuses the run rather than issuing an invoice
that charges the levies and nothing for the energy:

| Finding | The pairing |
|---|---|
| `KEIN_ARBEITSPREIS` | a product with every price field `None` |
| `INDEXWERT_FEHLT` | an index-linked tariff whose index value has not arrived |
| `ZWEITARIF_UNVOLLSTAENDIG` | one HT/NT band priced and not the other |
| `ZWEITARIF_OHNE_HT_NT_AUFTEILUNG` | an HT/NT-only product against a meter reporting one total |
| `HT_NT_SUMME_WEICHT_AB` | an HT/NT split that does not reconcile with the stated total |
| `SECT41A_MISSING_EPEX_PRICES` | a §41a interval carrying consumption at no market price |
| `SECT41A_KEINE_INTERVALLE` | a §41a tariff with no interval series at all, against a meter reporting consumption |
| `SECT41A_INTERVALLSUMME_WEICHT_AB` | a §41a interval series that does not reconcile with the meter total (0,5 % tolerance, 1 kWh floor) |
| `KEIN_TRINKWASSERPREIS` | water delivered under a tariff pricing only the Abwasser side |
| `KEIN_LADEPREIS` | charging energy measured at the charge point under a tariff with no per-kWh price |

The two §41a series findings are the dynamic twin of the HT/NT pair above: on
that path the quarter-hour series *is* the billed quantity — Arbeitspreis,
Netzentgelt, Konzessionsabgabe and Stromsteuer all ride the sum of the priced
intervals — so an absent or short series bills every levy on whatever arrived.
The meter total is the independent witness.

Two neighbouring shapes are *billed* instead, because the data is complete and
only its reading is at issue: HT/NT registers with no stated total bill on their
sum (`MeterInput::billable_kwh`), and a two-register meter on an Eintarif product
bills on its total.

One finding does not refuse. `KEIN_EREIGNISPREIS` reports counted
Energiedienstleistung events that nothing prices, at `Warning` severity: an
event count is also a legitimate informational figure — how many Einsätze a
maintenance flat rate covered — so it alone does not settle which was meant.

The general statement is a pair of property tests in `proptest_invoice`:
whenever `bill()` succeeds and a quantity was delivered, an Arbeitspreis position
exists and is non-zero. `any_billable_consumption_produces_a_work_price` covers
the electricity pricing shapes (Eintarif, HT/NT, indexed, tiered, split and
unsplit meters); `any_delivered_commodity_produces_a_work_price` covers gas,
Fernwärme and charging energy. The second exists because the first was
electricity-only, which is precisely why the defect kept recurring outside it.

### Every line multiplies out

**PEPPOL-EN16931-R120** allows ±0.02 between a line's `price × quantity` and its
amount. The trap is a *rounded* price: rounding what the page prints is right,
but the same figure is BT-146, and the further it is rounded and the more units
it multiplies, the further the product drifts from the amount beside it.

§41a states a weighted average — rarely representable — so the machine field
carries it at full precision and only the description rounds
(`∅ 7,8430 ct/kWh`). `every_position_price_multiplies_out_to_its_amount` holds
the rule for every position of every product.

## Validated period, stated regime

`BillingContext.period` is a `BillingPeriod` — the constructor (and the serde
path) refuse `from > to`, so an inverted period is unrepresentable in every
provider and helper downstream.

`BillingContext.vertragsart` states the contractual regime and is emitted as
the `vertragsart` ZusatzAttribut on every invoice:

- **`Sondervertrag`** (default) — freely negotiated, §41 EnWG.
- **`Grundversorgung`** — the published Allgemeine Preise apply (§36 EnWG,
  StromGVV/GasGVV).
- **`Ersatzversorgung`** — §38 EnWG fallback supply. It ends by law three
  months after it began (§ 38 Abs. 4 EnWG), so the engine **refuses** a
  longer Ersatzversorgung period with `ERSATZVERSORGUNG_UEBER_3_MONATE`:
  such a supply cannot exist, and billing it would invent one.

## Architecture

```
billingd (HTTP service)
    │   productd/edmd/marktd clients · HTTP endpoints
    │   XRechnung 3.0 CII / PEPPOL UBL · PostgreSQL · CloudEvents
    └── energy-billing (pure crate)
            ├── Product                — typed enum with 13 per-category variants
            │     ├── Strom(ElectricityProduct)
            │     ├── Waermepumpe/Wallbox(ControllableLoadProduct)   §14a
            │     ├── Gas(GasProduct)
            │     ├── Waerme(HeatProduct)
            │     ├── Wasser(WaterProduct)                        Trinkwasser + Abwasser
            │     ├── Solar(SolarProduct)
            │     ├── Eeg(EegProduct)
            │     ├── Einspeisung(EinspeisungProduct)
            │     ├── Hems/Emobility/Energiedienstleistung(…)
            │     └── Sharing(SharingProduct)                        §42c
            ├── Quantities             — all meter inputs for one billing period
            ├── BillingContext         — period, IDs, invoice type, regulatory rates
            │     └── period: BillingPeriod   — validated; from > to unrepresentable
            ├── BillingEngine          — composes BillingProvider instances
            │     ├── validate()       — pre-flight regulatory check (no positions)
            │     ├── bill(&self, …)   — pure function → Result<Invoice, EngineError>
            │     └── bill_batch(…)    — portfolio billing
            ├── BillingProvider        — one implementation per product/tax type
            └── Invoice                — result with positions + totals + warnings + BO4E JSON
                  ├── warnings: Vec<BillingWarning>    — regulatory compliance notices
                  ├── has_errors()                     — any Error-severity warning?
                  └── to_rechnung_json()               — BO4E JSONB for accountingd (feature `bo4e`)
```

The engine runs in passes:

```
Pass 0  validate_warnings()      §38/§41a guards · regulatory pre-checks
Pass 1  commodity / levy providers   (ElectricityProvider, GasProvider, …)
Pass 2  tax provider                 (MwStProvider — sees all net positions)
Pass 3  Abschlag deductions          (Final invoice reconciliation)
Pass 4  Minimum invoice top-up       (B2B Mindestabnahmeverpflichtung)
Pass 5  Cancellation sign reversal   (Stornorechnung — all signs negated)
```

---

## Quick start

```rust
use energy_billing::{BillingContext, BillingPeriod, GridInput, InvoiceType, MeterInput,
                     Product, Quantities, RegulatoryRates};
use rust_decimal::dec;
use time::macros::date;

// Deserialize directly from productd JSONB using the "category" discriminator
let product: Product = serde_json::from_str(r#"{
    "category": "STROM",
    "arbeitspreis_ct_per_kwh": "32.0",
    "grundpreis_ct_per_day": 12.0
}"#)?;

let ctx = BillingContext {
    malo_id:          "51238696012".to_owned(),
    lf_mp_id:         "9910000000002".to_owned(),
    rechnungsnummer:  "R2026-06-001".to_owned(),
    period:           BillingPeriod::new(date!(2026-06-01), date!(2026-06-30))?,
    invoice_type:     InvoiceType::Initial,
    regulatory_rates: RegulatoryRates::default(),
    ..Default::default()
};

let quantities = Quantities {
    electricity: Some(MeterInput {
        arbeitsmenge_kwh: dec!(312.5),
        ..Default::default()
    }),
    ..Default::default()
};

// Build and run — Product::build_engine() handles all category routing
let invoice = product
    .build_engine(&GridInput::default(), &ctx.regulatory_rates)
    .bill(ctx, &quantities)?;

invoice.assert_valid();
println!("Brutto: {} EUR", invoice.brutto_eur);

let rechnung_json: serde_json::Value = invoice.to_rechnung_json();  // feature `bo4e`
```

---

## Product enum

`Product` is the typed dispatch enum the engine bills from. Each category has its own
struct carrying only the fields that category prices, so a Gas field cannot be read on an
electricity tariff.

```rust
// Deserializes via #[serde(tag = "category")] from flat productd JSONB:
// {"category":"STROM","arbeitspreis_ct_per_kwh":"28.5"} → Product::Strom(ElectricityProduct{...})
// {"category":"WAERMEPUMPE","sect14a_modul2_nne_reduktion_ct_per_kwh":"1.5",...} → Product::Waermepumpe(...)
// {"category":"GAS","gas_arbeitspreis_ct_per_kwh_hs":"7.5",...} → Product::Gas(GasProduct{...})
```

| `Product` variant | Category string | Provider | Key features |
|---|---|---|---|
| `Strom(ElectricityProduct)` | `STROM` | `ElectricityProvider` or `DynamicElectricityProvider` | SLP/RLM; HT/NT; block tariffs; §41a EPEX |
| `Waermepumpe(ControllableLoadProduct)` | `WAERMEPUMPE` | `ControllableLoadProvider` | §14a Modul 1/2/3 |
| `Wallbox(ControllableLoadProduct)` | `WALLBOX` | `ControllableLoadProvider` | §14a Modul 1/2/3 |
| `Gas(GasProduct)` | `GAS` | `GasProvider` | Brennwertkorrektur; Energiesteuer; BEHG CO₂ |
| `Waerme(HeatProduct)` | `WAERME` | `HeatProvider` | Fernwärme; standard-rated (19 %); AVBFernwärmeV §24 Preisgleitklausel; the CO2KostAufG § 3 Abs. 1 statement + § 14 WPG Anteil |
| `Wasser(WaterProduct)` | `WASSER` | `WaterProvider` | Trinkwasser 7 % USt; gesplittete Abwassergebühr (Schmutzwasser − Absetzungen, Niederschlagswasser m²); public-law fee is EN 16931 `O`, not `Z` |
| `Solar(SolarProduct)` | `SOLAR` | `SolarProvider` | §42b EnWG GGV; Mieterstrom mit § 42a Abs. 4 EnWG 90 %-Deckel; Stromsteuer per § 9 Abs. 1 Nr. 3 StromStG, **stated** not omitted; 0 % USt if Kleinunternehmer (§19 UStG) |
| `Eeg(EegProduct)` | `EEG` | `EegProvider` | LF-side Gutschrift; `eeg` feature for §51/§52 |
| `Einspeisung(EinspeisungProduct)` | `EINSPEISUNG` | `EinspeisungProvider` | Direktvermarktung Marktwert − Gebühr |
| `Hems(HemsProduct)` | `HEMS` | `HemsProvider` | Platform subscription + events |
| `Emobility(EmobilityProduct)` | `EMOBILITY` | `EmobilityProvider` | CPO/EMSP: service + kWh + session/roaming |
| `Energiedienstleistung(ServiceProduct)` | `ENERGIEDIENSTLEISTUNG` | `ServiceProvider` | Flat fee + per-event |
| `Sharing(SharingProduct)` | `SHARING` | `ElectricityProvider` + `EnergyShareProvider` | §42c Energiegemeinschaft credit |

`ControllableLoadProduct` composes `ElectricityProduct` (via `#[serde(flatten)]`) plus §14a fields — the standard electricity billing is delegated to `ElectricityProvider` then §14a credits are appended.

---

## Pricing capabilities

| Feature | How |
|---|---|
| HT/NT Zweitarif | `billing::TimeOfUsePricing` (validated, penny-correct) |
| Block / graduated tariffs | `billing::RateSchedule::graduated()` |
| Indexed prices (TTF, Phelix, NCG) | `IndexedPriceConfig { base_ct, spread_ct, index_value, factor }` |
| Gas indexed price | `gas_indexed_price: Option<IndexedPriceConfig>` in `GasProduct` |
| Fernwärme Preisgleitklausel | `waerme_indexed_price: Option<IndexedPriceConfig>` (AVBFernwärmeV §24 Abs. 4) |
| Seasonal prices | `SeasonalPriceOverride` by month range (wraps year boundary) |
| §41a EPEX dynamic | `billing::DynamicPricing`, per 15-min MTU: kWh × (spot + Aufschlag); spot clamped into `[dynamic_epex_floor_ct_kwh, dynamic_epex_cap_ct_kwh]` |
| §41a iMSys guard | Hard error when `dynamic_epex=true` and `MeteringMode != Imsys` |
| Pro-rata Grundpreis | `ctx.prorate_days()` clips to `vertragsbeginn`/`vertragsende` |
| Minimum invoice (B2B) | Pass 4 auto-top-up to `minimum_invoice_eur_brutto` |
| Discounts | `auf_abschlag_ct_per_kwh`, `auf_abschlag_eur_per_month` (`Discount` category) |
| Boni (§17 UStG Entgeltminderung) | `sofortbonus_eur` (one-time), `treuebonus_eur_per_year` (pro-rated) → `Bonus` category |
| MSB pass-through | `msb_gebuehr_ct_per_day` (MsbG) |
| Multi-rate MwSt | Per-position `applicable_tax_rate` → grouped `MwStProvider` |
| 0% USt feed-in Gutschrift | `kleinunternehmer_19_ustg` (§19 UStG Kleinunternehmer) |
| Verbrauchsteuer-Begünstigungen | `stromsteuer_tarif` / `energiesteuer_tarif` (Befreiung, Ermäßigung) and `steuerentlastungen` (notes only) — see below |
| Gas RLM Leistungspreis | `gas_leistungspreis_ct_per_kw_month` in `GasProduct` |
| §42 Energiemix | `EnergieQuellen` struct with `co2_g_per_kwh` (mandatory §42 Abs. 2 Nr. 2 EnWG) |

---

## Regulatory compliance

### §41a Abs. 1 EnWG — iMSys guard for dynamic tariffs

Dynamic tariffs (`Product::Strom(p)` where `p.dynamic_epex = true`) require an intelligent
metering system. `BillingEngine::bill()` rejects with
`EngineError::ValidationBlocked` — carrying every collected warning — when
`quantities.electricity.metering_mode != MeteringMode::Imsys`:

```rust
// Pre-flight check: validate without generating positions
let warnings = engine.validate(&ctx, &quantities);
for w in &warnings {
    if w.severity == WarningSeverity::Error {
        eprintln!("[{}] {}", w.code, w.message);
    }
}
// §41a violations produce BillingWarning { code: "SECT41A_IMSYS_REQUIRED", severity: Error }
```

### Verbrauchsteuern — Befreiung, Ermäßigung, Entlastung (`steuer` module)

German excise law knows three instruments and only two of them change what a
supplier may invoice:

| Instrument | Who acts | Effect on the invoice |
|---|---|---|
| **Steuerbefreiung** — § 9 Abs. 1 StromStG, §§ 25–28 EnergieStG | supplier, against the customer's Erlaubnis | the levy is **not** invoiced |
| **Steuerermäßigung** — § 9 Abs. 2/3 StromStG | supplier | invoiced at the **reduced** statutory rate |
| **Steuerentlastung** — § 9a/§ 9b/§ 9c StromStG, §§ 53a, 54 EnergieStG | the *customer*, afterwards, at the Hauptzollamt | **none** — invoiced in full |

```rust
pub enum StromsteuerTarif {
    Regel,                                        // § 3 StromStG — 2,05 ct/kWh
    Befreiung   { grund: StromsteuerBefreiung },  // § 9 Abs. 1 Nr. 1–8
    Ermaessigung{ grund: StromsteuerErmaessigung },// § 9 Abs. 2 (11,42 €/MWh) / Abs. 3 (0,50 €/MWh)
}

pub enum EnergiesteuerTarif {
    Regel,                                        // § 2 Abs. 3 S. 1 Nr. 4 — 0,55 ct/kWh_Hs
    Befreiung { grund: EnergiesteuerBefreiung },  // §§ 25–28 gegen Erlaubnis (§ 24 Abs. 2)
}

/// Notes only. Never an amount.
pub enum Steuerentlastung {
    Stromsteuer9a, Stromsteuer9b, Stromsteuer9c,
    Energiesteuer53a, Energiesteuer54,
}
```

**Why the split is load-bearing.** Zero-rating an Entlastung at supply
under-declares the supplier's own Stromsteueranmeldung, and the customer's later
Entlastungsantrag duplicates it rather than repairing it — a Unternehmen des
Produzierenden Gewerbes is invoiced the full 2,05 ct/kWh and reclaims 2,00 from
the Hauptzollamt (§ 9b, permanent at the EU minimum rate since 01.01.2026, from
12 500 kWh a year). Treating an Ermäßigung as an exemption fails the other way:
§ 9 Abs. 2 Fahrstrom is a *rate* of 11,42 EUR/MWh, and dropping the line loses
1,142 ct/kWh on every rail-traction invoice.

A `Steuerentlastung` renders one 0-EUR informational position stating the levy
it may be claimed against — the customer cannot file without that figure.

---

## Invoice types

```rust
pub enum InvoiceType {
    Initial,             // RECHNUNG — normal periodic billing
    AdvancePayment,      // ABSCHLAGSRECHNUNG — estimated advance request
    Final,               // SCHLUSSRECHNUNG — Jahresabrechnung, deducts ctx.abschlage
    CreditNote,          // GUTSCHRIFT — LF pays generator (EEG, EINSPEISUNG)
    PartialInvoice,      // TEILRECHNUNG — §41 EnWG move-in/move-out / Tarifwechsel
    Correction { original_invoice_id, reason },  // KORREKTURRECHNUNG (§ 147 AO / GoBD)
    Cancellation { original_invoice_id },         // STORNORECHNUNG — all signs negated
}
```

---

## Advance payments (Abschläge)

A Jahresabrechnung (`InvoiceType::Final`) reconciles the advances the customer
already paid. Each one is an `AbschlagDeduction`:

```rust
AbschlagDeduction {
    datum: date!(2026 - 01 - 15),
    betrag_eur: dec!(120.00),   // gross, as paid
    ust_satz: dec!(0.19),       // rate this advance was invoiced at
    beschreibung: Some("Abschlag Januar 2026".to_owned()),
}
```

`ust_satz` is mandatory because **§14 Abs. 5 Satz 2 UStG** requires an Endrechnung
to deduct the advances *and the tax attributable to them* — "die vereinnahmten
Teilentgelte und die auf sie entfallenden Steuerbeträge". A gross total alone
cannot express that: EUR 120 collected at 19 % and EUR 120 collected at 7 %
deduct different amounts of tax. The rate is per advance rather than per invoice,
so a rate change mid-year leaves earlier advances at the rate they were billed at.

| Field | Meaning |
|---|---|
| `betrag_eur` | gross paid |
| `netto_eur()` | `betrag_eur / (1 + ust_satz)`, to cents |
| `ust_eur()` | `betrag_eur - netto_eur()` — derived, so net + tax always re-sums to the gross paid |

On the resulting invoice:

```text
brutto_eur            gross for the period
- abschlag_total_eur  gross already paid
= zahlbetrag_eur      balance due (negative → refund)

abschlag_ust_eur      tax contained in abschlag_total_eur (§14 Abs. 5 Satz 2 UStG)
```

Abschlag positions never affect `netto_eur` / `mwst_eur` / `brutto_eur` — they
reconcile what was paid, they are not turnover.

### Two lawful settlement forms

`BillingContext::settlement_form` picks how a settling invoice presents them.
Both are lawful; they differ in what the document shows, not in what the customer
pays.

| `SettlementForm` | Shows | Basis |
|---|---|---|
| `Endrechnung` (default) | the whole supply, then deducts the advances **and their tax** | §14 Abs. 5 Satz 2 UStG |
| `Restrechnung` | only the remainder; advances are not listed | BMF 15.10.2024, Rn. 48 |

The Endrechnung form has one failure mode worth naming: deducting the advances
but not the tax contained in them. Under UStAE 14.8 Abs. 10 the issuer then owes
the tax shown **plus** the advance-related portion again under §14c Abs. 1 — the
same tax billed twice. `abschlag_ust_eur` exists so that figure is always
available to state.

The Restrechnung form is what the BMF recommends for e-invoices, because
EN 16931's core profiles have nowhere to carry per-advance tax. Compute the
residual directly with:

```rust
let residual = invoice.residual_breakdown(default_rate)?;  // supply − advances, per rate
```

Over-deduction is refused rather than silently accepted: advances exceeding the
supply in any `(category, rate)` group would understate the output tax owed.

**`to_en16931` implements the difference.** An Endrechnung states the full
supply and the advances as BT-113 *paid*. A Restrechnung deducts each advance as
a **BG-20 document-level allowance** carrying its own BT-95/96 VAT category and
rate — one per `(category, rate)` group, not one per advance, so a monthly
Abschlagsplan does not put eleven identical rows on the page. The reconciler then
derives BG-23 as `lines − allowances` per rate, which *is* the residual, and
nothing is stated as paid. Both documents ask the customer for the same BT-115.

That is what the flat BT-113 cannot do: an advance invoiced at 19 % stays a 19 %
deduction on a settlement billed at another rate. The field selected nothing at
all until it was wired — declared, documented here, and read by no code path, so
every settling invoice went out as an Endrechnung regardless.

### Crossing into `billing`

```rust
invoice.advance_payments()?   // Vec<billing::AdvancePayment> — each with its own tax
invoice.prepayment()?         // billing::Prepayment::Itemised, or ::None
```

Advances are always itemised, never collapsed to a flat total: the per-advance tax
is what makes the deduction lawful. `AdvancePayment` mirrors the ZUGFeRD /
Factur-X EXTENDED group `SpecifiedAdvancePayment` (BG-X-45), the standardised
place where per-advance tax data has a home.

---

## VAT breakdown (EN 16931 BG-23 / BO4E `steuerbetraege`)

`Invoice::tax_subtotals(default_rate)` groups the positions into one entry per
distinct rate, each with its own taxable base (BT-116) and tax amount (BT-117).
A single aggregate `mwst_eur` cannot describe an invoice that mixes 19 %
commodity with 7 % Fernwärme or 0 % PV feed-in.

Zero-rated bases are included. A supply taxed at 0 % is still a taxable supply,
and omitting it would make the sum of the bases differ from the invoice net —
exactly what the EN 16931 total-reconciliation rules check.

The breakdown is emitted as BO4E `steuerbetraege`, whose entries must sum to
`gesamtsteuer`, and is carried into XRechnung as BG-23.

---

## Meter inputs

```rust
pub struct MeterInput {
    pub arbeitsmenge_kwh:    Decimal,
    pub arbeitsmenge_ht_kwh: Option<Decimal>,  // HT register
    pub arbeitsmenge_nt_kwh: Option<Decimal>,  // NT register
    pub spitzenleistung_kw:  Option<Decimal>,  // peak demand (RLM)
    pub steuerung_stunden:   Option<Decimal>,  // §14a load-shedding hours
    pub zaehlernummer:       Option<String>,   // §41 EnWG — shown on invoice
    pub zaehlerstand_von:    Option<Decimal>,  // start reading
    pub zaehlerstand_bis:    Option<Decimal>,  // end reading
    pub metering_mode:       MeteringMode,     // Slp | Rlm | Imsys
    pub ablesungsart:        Ablesungsart,     // § 40 Abs. 2 Nr. 6 EnWG — its own duty
    pub is_estimated:        bool,             // § 40a Abs. 2 EnWG notice on invoice
    pub zaehler_replaced:    bool,             // Zählerwechsel notice on invoice
    pub coverage_pct:        Option<Decimal>,  // 0–100; below 100 the invoice rests in
                                               // part on a § 40a Abs. 2 estimate
}
```

---

## Key regulatory fields per product

### `ElectricityProduct` / `ControllableLoadProduct`

| Field | Law | Effect |
|---|---|---|
| `kleinunternehmer_19_ustg` | §19 UStG | 0 % USt on the feed-in Gutschrift (operator has elected Kleinunternehmer) |
| `stromsteuer_tarif` | § 9 StromStG | `REGEL` \| `BEFREIUNG{grund}` \| `ERMAESSIGUNG{grund}` |
| `steuerentlastungen` | § 9a/9b/9c StromStG | Notes only — the levy is billed in full |
| `leistungspreis_strom_ct_per_kw_month` | §41 EnWG | RLM demand charge (ct/kW/month) |
| `preisgarantie_bis` | §41 Abs. 1 Nr. 4 EnWG | Price guarantee expiry on invoice |
| `mwst_rate_override` | §12 UStG | Override 19% per product |
| `dynamic_epex` | §41a EnWG | 15-min EPEX MTU spot billing (requires `MeteringMode::Imsys`) |
| `dynamic_epex_floor_ct_kwh` | §41a EnWG | Price floor for the spot component (Aufschlag added on top) |
| `energiequellen` | §42 Abs. 2 Nr. 2 EnWG | Typed fuel mix with CO₂ label |

### `ControllableLoadProduct` (§14a extras)

| Field | Law | Effect |
|---|---|---|
| `sect14a_modul2_nne_reduktion_ct_per_kwh` | §14a EnWG Modul 2 | Per-kWh Arbeitspreis reduction |
| `sect14a_modul1_pauschale_eur_per_year` | §14a EnWG Modul 1 | Pauschale Reduzierung (EUR/year, flat) |
| `sect14a_steuerungsentschaedigung_ct_per_kwh` | §14a EnWG | Per-kWh Steuerungsentschädigung (not a module) |
| `sect14a_steuerungsentschaedigung_eur_per_kw_year` | §14a EnWG | Capacity Steuerungsentschädigung (not a module) |

### `GasProduct`

| Field | Law | Effect |
|---|---|---|
| `energiesteuer_tarif` | §§ 24 Abs. 2, 25–28 EnergieStG | `REGEL` \| `BEFREIUNG{grund}` (Erlaubnisschein) |
| `gas_leistungspreis_ct_per_kw_month` | §41 EnWG | RLM demand charge for large gas customers |
| `gas_indexed_price` | §41 EnWG (Sonderkundenvertrag) | B2B TTF/NCG indexed price |

---

## Advanced operations

### Tarifwechsel — mid-period price change

```rust
// Old tariff: Jan 1–14
let inv_old = old_product.build_engine(&grid, &rates).bill(ctx_jan1_14, &meter_old)?;
// New tariff: Jan 15–31
let inv_new = new_product.build_engine(&grid, &rates).bill(ctx_jan15_31, &meter_new)?;
// Combined January invoice
let merged = inv_old.merge(inv_new);
```

### Portfolio billing

```rust
let engine = product.build_engine(&grid, &rates);
let results: Vec<Result<Invoice, EngineError>> = engine.bill_batch(
    customers.into_iter().map(|(ctx, quantities)| (ctx, quantities)).collect()
);
```

### Regulatory pre-flight

```rust
let engine = product.build_engine(&grid, &rates);
let warnings = engine.validate(&ctx, &quantities);
if invoice.has_errors() {
    // Block dispatch — Error-severity regulatory violation
}
```

### Proportional cost allocation (B2B shared buildings)

```rust
// Weights, not shares — square metres here; they are normalised internally.
let parts = building_invoice.allocate_proportionally(
    &[dec!(84), dec!(73), dec!(52)],
    vec![ctx_tenant_a, ctx_tenant_b, ctx_tenant_c],
)?;
// Guaranteed: the parts' brutto sum back to the original brutto, and each
// position's split quantities sum back to the metered volume. Every part
// inherits the source invoice's warnings, so a blocking finding still blocks.
```

### §41a Abs. 6 annual savings comparison

```rust
let comparison = Sect41aAnnualComparison::compute(
    dec!(2400),   // actual kWh under dynamic tariff
    dec!(650.00), // actual EUR brutto
    dec!(40.0),   // reference fixed tariff ct/kWh
);
// Rendered as Info position on the annual invoice
```

---

## Optional features

```toml
energy-billing = { path = "…", features = ["eeg"] }   # full eeg-billing accuracy
# energy-billing = { path = "…", features = ["full"] }  # …or every optional feature
```

| Feature | Pulls in | Enables |
|---|---|---|
| `eeg` | `eeg-billing` | `EegProvider` delegates to `eeg_billing::calculate_settlement()` for §51/§52/§36h |
| `bo4e` | `rubo4e` | `Invoice::to_rechnung()` and `to_rechnung_json()`, `TaxSubtotal::to_bo4e()`, `InvoiceType::rechnungstyp()` |
| `en16931` | `en16931` | `Invoice::to_en16931(spec_id, seller, buyer)` → the semantic model `en16931-formats` renders |
| `full` | both of the above | everything |

All three are **off by default**, so the core engine computes and validates a
complete invoice without either boundary representation compiled in. `billingd`
runs with `full`.

---

## Audit trail and explainability

Every `BillingPosition` carries a `PositionTrace` with the full calculation audit:

```rust
pub struct PositionTrace {
    pub formula: String,              // "500.000 kWh × 0.30000 EUR/kWh = 150.00000 EUR"
    pub input_quantity: Decimal,
    pub input_unit_price_eur: Decimal,
    pub gross_eur: Decimal,
    pub regulatory_basis: Vec<String>, // ["§3 StromStG", "§41 EnWG"]
    pub tariff_source: Option<String>, // product sheet ID from productd
    pub pro_rata_fraction: Option<Decimal>,
    pub rounding_note: Option<String>,
}
```

The `BillingWarning` field on `Invoice` carries regulatory compliance notices:

```rust
// Check for dispatch-blocking violations
if invoice.has_errors() {
    for w in invoice.warnings.iter().filter(|w| w.severity == WarningSeverity::Error) {
        // e.g. { code: "SECT41A_IMSYS_REQUIRED", message: "§41a Abs. 1 EnWG: …" }
    }
}
```

---

## Regulatory basis

| Law | Coverage |
|---|---|
| §3 StromStG | Stromsteuer 2.05 ct/kWh; `stromsteuer_for_year(year)` for retroactive corrections |
| § 9 StromStG | All eight Abs. 1 Befreiungen plus the Abs. 2/3 ermäßigte Sätze, typed; § 9a/9b/9c Entlastungen kept out of the amounts |
| §2 EnergieStG | Erdgassteuer 0.55 ct/kWh; `energiesteuer_gas_for_year(year)` (incl. 2022 0-rate) |
| §§ 53a, 54 EnergieStG | Steuer**entlastungen** the customer claims at the Hauptzollamt — the supply is invoiced in full, and a `Steuerentlastung` renders the 0-EUR note the application needs |
| BEHG §10 | CO₂-Preis H-Gas (65 EUR/t 2026) + L-Gas factor; `behg_ct_per_kwh_for_year(year)` |
| §25 Nr. 4 MessEV | Brennwertkorrektur m³ → kWh_Hs |
| §12 Abs. 2 Nr. 1 UStG | Reduced 7% MwSt for Anlage-2 goods (Trinkwasser) — NOT district heating |
| §19 UStG | 0% USt on the feed-in Gutschrift (Kleinunternehmer election) |
| §14a EnWG | Controllable loads, Modul 1/2/3 (BNetzA BK8-22/010-A) via `ControllableLoadProvider` |
| § 40a Abs. 2 EnWG | Estimated reading notice on invoice |
| §40 / §40b EnWG | Mandatory ct/kWh; structured price-comparison data in JSON |
| §40 EnWG | Invoice content (Netzbetreiber, Energiemix §42) |
| § 40 Abs. 2 Nr. 6 EnWG | Anfangs-/Endzählerstand, the consumption **and** the `Ablesungsart` — the third is its own duty and was on the page only for estimates |
| §40 Abs. 2 Nr. 7/8 EnWG | Verbrauchshistorie (prior-year + national average) |
| §41a / §41a Abs. 1 EnWG | §41a EPEX per-interval; iMSys guard and missing-price guard both hard errors; the § 40 Abs. 2 display duties ride the dynamic path too |
| §42 Abs. 2 Nr. 2 EnWG | CO₂ emissions label via typed `EnergieQuellen.co2_g_per_kwh` |
| § 42a Abs. 4 EnWG | Mieterstrom price capped at 90 % of the Grundversorgung — refused above it, and the ceiling stated on the page |
| § 9 Abs. 1 Nr. 3 StromStG | A rooftop Mieterstrom/GGV supply is exempt (≤ 2 MW, räumlicher Zusammenhang) — the **default**, and the ground reaches the invoice |
| §42b EnWG | Gemeinschaftliche Gebäudeversorgung (PV/grid hybrid split) |
| CO2KostAufG § 3 Abs. 1 / § 14 WPG | The five-item CO₂ statement on every Gas and Fernwärme invoice, plus the Wärme renewable share |
| §42c EnWG | Energiegemeinschaft sharing credit via `SharingProduct` |
| §51 EEG 2023 | Negativpreisregel (contractual LF feature via `eeg` feature) |

---

## Testing

```bash
cargo test -p energy-billing --all-features
```

Coverage spans these suites:

| Suite | Coverage |
|---|---|
| Unit tests (lib) | `RegulatoryRates`, levy lookups, `prorate_days`, `InvoiceType`, `Product` enum roundtrip, `StromsteuerTarif`/`Steuerentlastung`, `billed_months`/`billed_years`, tariff deserialization |
| `calculator_tests` | All 13 categories (incl. WASSER), §14a/§41a/§41a Abs. 1, GGV, seasonal, indexed, prosumer, block tariffs, RLM demand charge, multi-rate MwSt, cancellation, BO4E JSON, pro-rata, Tarifwechsel, `bill_batch`, `validate` |
| `golden_scenarios` | Golden master: SLP electricity; gas + levies; EEG Gutschrift; RLM demand charge; §54 EnergieStG billed in full as an Entlastung; historic rates 2022 (heating gas constant 0.55, 7 % gas-USt window); §41a Abs. 1 rejection; §40 ct/kWh; §40 mandatory fields; §42c sharing; §9 exemption |
| `proptest_invoice` | Property-based: `brutto == netto + mwst`, cancellation sign, 0% MwSt, gas arithmetic, demand charge non-negative, StromStG year table |
| `en16931_conformance` | `Invoice::to_en16931` passes the real EN 16931 rule engine (per-line VAT + BG-23 reconcile), and the statutory `Info` disclosures reach BT-22 |
| `priceless_product_is_refused` | A product with every price field `None` refuses the run rather than billing the levies alone |
| Doc tests | Inline usage examples |

## Related crates

| Crate | Role |
|---|---|
| [`energy-billing`]https://docs.rs/energy-billing**this crate** | Retail invoice calculation — Strom, Gas, Wärme, Wasser, §14a, §41a, EEG |
| [`eeg-billing`]https://docs.rs/eeg-billing | The feed-in leg — EEG/KWKG Vergütung and the §14 UStG Gutschrift |
| [`mako-markt`]https://docs.rs/mako-markt | Marktstammdaten — Marktlokation, Messlokation, Marktpartner, Rollenzuordnung — the model the customer and Marktlokation inputs are keyed on |
| [`grid-billing`]https://docs.rs/grid-billing | The grid-side (NB) counterpart — NNE, KA, MMM, MSB |
| [`billingd`]https://hupe1980.github.io/mako/docs/services/billingd/ | Production daemon — the Energy Billing Engine for the LF role |

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