Skip to main content

energy_billing/
context.rs

1//! `BillingContext` — the immutable billing metadata passed to every provider.
2//!
3//! Separates *what we're billing* (quantities, products) from *how we're billing it*
4//! (period, identifiers, invoice type, regulatory rates).
5
6use crate::EuroAmount;
7use crate::rates::RoundMoney;
8use rust_decimal::Decimal;
9
10use crate::rates::RegulatoryRates;
11
12// ── Verbrauchshistorie ───────────────────────────────────────────────────────────
13
14/// §40 Abs. 2 EnWG — Verbrauchshistorie (consumption history for invoice display).
15///
16/// German energy invoices must compare the billed period consumption against
17/// the same period in the prior year and the national average for comparable
18/// customers. This is an **invoice display requirement**, not a calculation input.
19///
20/// ## Legal basis
21///
22/// §40 Abs. 2 EnWG: “der tatsächliche Energieverbrauch sowie — soweit technisch möglich
23/// und sinnvoll — ein Vergleich des aktuellen Energieverbrauchs des Letztverbrauchers mit
24/// seinem Verbrauch im gleichen Zeitraum des Vorjahres … und dem Verbrauch einer
25/// Vergleichsgruppe von Letztverbrauchern.”
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct Verbrauchshistorie {
28    /// Consumption in the same period of the prior year (kWh). §40 Abs. 2 Nr. 7 EnWG.
29    #[serde(default)]
30    pub vorjahr_kwh: Option<Decimal>,
31    /// National average consumption for comparable customers (kWh). §40 Abs. 2 Nr. 8 EnWG.
32    #[serde(default)]
33    pub bundesdurchschnitt_kwh: Option<Decimal>,
34    /// Description of the comparable customer group (e.g. `"2-Personen-Haushalt"`).
35    #[serde(default)]
36    pub kundengruppe: Option<String>,
37}
38
39// ── Vertragsinformationen ─────────────────────────────────────────────────────
40
41/// §40 Abs. 1 EnWG — contract facts the invoice must state.
42///
43/// Vertragsdauer, Kündigungsfrist, the next possible Kündigungstermin and the
44/// next Abrechnungstermin are invoice *contents*, not calculation inputs: they
45/// change no amount, but an electricity or gas invoice without them is
46/// incomplete under §40. Typed here so billingd can source them from vertragd
47/// and the engine can emit them without either side inventing prose.
48#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
49pub struct Vertragsinformationen {
50    /// Contract term as displayed, e.g. `"24 Monate"` or `"unbefristet"`.
51    #[serde(default)]
52    pub vertragsdauer: Option<String>,
53    /// Notice period as displayed, e.g. `"6 Wochen zum Vertragsende"`.
54    #[serde(default)]
55    pub kuendigungsfrist: Option<String>,
56    /// Next date the customer could terminate to.
57    #[serde(default)]
58    pub naechstmoeglicher_kuendigungstermin: Option<time::Date>,
59    /// Next scheduled Abrechnungstermin.
60    #[serde(default)]
61    pub naechster_abrechnungstermin: Option<time::Date>,
62}
63
64/// §40 Abs. 2 EnWG — consumer information the invoice must state.
65///
66/// Nr. 1 (supplier identity and contact), Nr. 9 (rights in dispute
67/// resolution, Schlichtungsstelle Energie per §111b EnWG), Nr. 10 (contact
68/// data of the Verbraucherservice der Bundesnetzagentur) and Nr. 11
69/// (Energieberatung contact). These change no amount, but a Letztverbraucher
70/// invoice without them is incomplete under §40 Abs. 2.
71///
72/// [`Default`] carries the statutory public contact data — the
73/// Schlichtungsstelle and BNetzA entries are fixed by law, not by operator —
74/// so a bill can never silently lack the mandatory hints. The supplier
75/// fields must be filled by the caller (billingd config).
76#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
77pub struct Verbraucherinformationen {
78    /// §40 Abs. 2 Nr. 1: supplier name as displayed on the bill.
79    #[serde(default)]
80    pub lieferant_name: Option<String>,
81    /// §40 Abs. 2 Nr. 1: supplier postal address.
82    #[serde(default)]
83    pub lieferant_anschrift: Option<String>,
84    /// §40 Abs. 2 Nr. 1: customer-service contact (hotline and/or e-mail).
85    #[serde(default)]
86    pub lieferant_kontakt: Option<String>,
87    /// §40 Abs. 2 Nr. 9: dispute-resolution hint incl. Schlichtungsstelle
88    /// Energie contact (§111b EnWG).
89    pub schlichtungsstelle: String,
90    /// §40 Abs. 2 Nr. 10: Verbraucherservice der Bundesnetzagentur contact.
91    pub bnetza_verbraucherservice: String,
92    /// §40 Abs. 2 Nr. 11: Energieberatung contact hint (Verbraucherzentrale).
93    pub energieberatung: String,
94    /// §40 Abs. 2 Nr. 12: supplier-switch hint incl. §41c price-comparison
95    /// tools.
96    pub wechselhinweis: String,
97}
98
99impl Default for Verbraucherinformationen {
100    fn default() -> Self {
101        Self {
102            lieferant_name: None,
103            lieferant_anschrift: None,
104            lieferant_kontakt: None,
105            schlichtungsstelle: "Bei Streitigkeiten können Sie die Schlichtungsstelle Energie e.V. \
106                 anrufen (§111b EnWG): Friedrichstraße 133, 10117 Berlin, \
107                 Tel. 030 2757240-0, info@schlichtungsstelle-energie.de, \
108                 www.schlichtungsstelle-energie.de. Voraussetzung ist, dass der \
109                 Lieferant Ihrer Beschwerde nicht binnen vier Wochen abgeholfen hat."
110                .to_owned(),
111            bnetza_verbraucherservice: "Verbraucherservice der Bundesnetzagentur für den Bereich Elektrizität \
112                 und Gas: Postfach 8001, 53105 Bonn, Tel. 030 22480-500, \
113                 verbraucherservice-energie@bnetza.de."
114                .to_owned(),
115            energieberatung: "Unabhängige Energieberatung erhalten Sie bei der \
116                 Energieberatung der Verbraucherzentrale, www.verbraucherzentrale-energieberatung.de."
117                .to_owned(),
118            wechselhinweis: "Informationen zum Lieferantenwechsel und behördlich zugelassene \
119                 Preisvergleichsinstrumente (§41c EnWG) finden Sie unter \
120                 www.bundesnetzagentur.de."
121                .to_owned(),
122        }
123    }
124}
125
126/// The party the invoice is addressed to — § 14 Abs. 4 Nr. 1 UStG's
127/// *Leistungsempfänger*, EN 16931's BG-7 buyer.
128///
129/// # Why this is on the context
130///
131/// The engine prices a Marktlokation; it holds no customer master. So it used
132/// to name the recipient by the **MaLo alone** — a `Geschaeftspartner` carrying
133/// one `mako:externe_kunden_id` ZusatzAttribut and no name, no address. That is
134/// not a document § 14 UStG describes, and a BO4E consumer reading the stored
135/// `Rechnung` found no recipient at all, while the *same* invoice's EN 16931
136/// model carried the customer in full because the caller supplied it on a
137/// separate channel.
138///
139/// One source now: the caller that resolves the customer puts it here, and both
140/// maps read it. `None` still works — the document then names the
141/// Marktlokation, which is the documented degradation for an uncontracted MaLo
142/// rather than a failed run — but it is now the same degradation on both sides.
143#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct Rechnungsempfaenger {
146    /// The addressee as printed — an organisation name or a person's full name.
147    #[serde(default)]
148    pub name: Option<String>,
149    /// Street and house number.
150    #[serde(default)]
151    pub line1: Option<String>,
152    /// Postcode.
153    #[serde(default)]
154    pub post_code: Option<String>,
155    /// Town.
156    #[serde(default)]
157    pub city: Option<String>,
158    /// ISO 3166-1 alpha-2. Absent is read as `DE`.
159    #[serde(default)]
160    pub country: Option<String>,
161    /// USt-IdNr., where the customer has one (BT-48).
162    #[serde(default)]
163    pub vat_id: Option<String>,
164}
165
166impl Rechnungsempfaenger {
167    /// Is there enough here to name a recipient at all?
168    ///
169    /// A recipient with no name is not one: the fallback that names the
170    /// Marktlokation is more honest than an empty BT-44.
171    #[must_use]
172    pub fn names_somebody(&self) -> bool {
173        self.name.as_deref().is_some_and(|n| !n.trim().is_empty())
174    }
175}
176
177// ── InvoiceType ───────────────────────────────────────────────────────────────
178
179/// Whether this is an initial invoice, a correction, a cancellation, or a final settlement.
180///
181/// German energy suppliers frequently perform:
182/// ```text
183/// Initial invoice  →  Correction (corrected meter reading)
184///                  →  Cancellation (full reversal)
185///                  →  Final (annual Schlussabrechnung)
186/// ```
187///
188/// ## § 147 AO / GoBD compliance
189///
190/// Corrections must reference the original invoice ID for the 3-year audit trail.
191/// Cancellations reverse the original to EUR 0.
192#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
193#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
194pub enum InvoiceType {
195    /// Standard billing run (Abschlagsrechnung, periodic invoice).
196    Initial,
197
198    /// Credit note (Gutschrift) — outgoing payment to a third party.
199    ///
200    /// Used for:
201    /// - EEG feed-in settlement (payment to generator)
202    /// - EINSPEISUNG Direktvermarktung settlement
203    /// - Reverse-charge scenarios
204    ///
205    /// `rechnungsart` = `"GUTSCHRIFT"`
206    CreditNote,
207
208    /// Correction superseding an earlier invoice (§ 147 AO / GoBD).
209    ///
210    /// The original invoice must be referenced in the accounting system.
211    /// The net effect is: `original + correction = corrected total`.
212    Correction {
213        /// ID of the original invoice this corrects.
214        original_invoice_id: String,
215        /// Human-readable reason (for audit trail).
216        reason: Option<String>,
217    },
218
219    /// Full reversal of an earlier invoice (Stornorechnung).
220    ///
221    /// All positions are sign-inverted to bring the original to EUR 0.
222    Cancellation {
223        /// ID of the original invoice being cancelled.
224        original_invoice_id: String,
225    },
226
227    /// Annual final settlement (Schlussabrechnung / Jahresabrechnung).
228    ///
229    /// Reconciles advance payments against measured consumption.
230    /// Include paid Abschläge in `BillingContext::abschlage` — they will be
231    /// deducted from `Invoice::zahlbetrag_eur`.
232    Final,
233
234    /// Advance payment request (Abschlagsrechnung).
235    ///
236    /// Use this for **estimated** periodic billing where no final meter reading
237    /// is available yet. The customer pays on account; the annual settlement
238    /// (`InvoiceType::Final`) reconciles the difference.
239    ///
240    /// BO4E `rechnungsart` = `"ABSCHLAGSRECHNUNG"`
241    ///
242    /// ## Distinction from `Initial`
243    ///
244    /// `Initial` represents billing for **actual metered consumption** — it maps
245    /// to `"RECHNUNG"`. `AdvancePayment` represents **estimated advance payments**
246    /// that will be settled annually.
247    AdvancePayment,
248
249    /// Partial delivery invoice (Teilrechnung) for incomplete supply periods.
250    ///
251    /// Used when a customer switches supplier mid-period, moves in/out, or when a
252    /// meter replacement creates a split period. The departing or arriving supplier
253    /// issues a Teilrechnung for the exact days of actual supply.
254    ///
255    /// ## Legal basis
256    ///
257    /// §41 EnWG Abs. 1: the invoice must cover the actual supply period.
258    /// StromGVV §17 / GasGVV §14: Lieferungsende is billed on the day of change.
259    ///
260    /// `rechnungsart` = `"TEILRECHNUNG"`
261    PartialInvoice,
262}
263
264impl InvoiceType {
265    /// The typed BO4E [`Rechnungstyp`](rubo4e::current::Rechnungstyp), where the
266    /// BO4E vocabulary has a value:
267    ///
268    /// | `InvoiceType` | BO4E `rechnungstyp` |
269    /// |---|---|
270    /// | `Initial` | `ENDKUNDENRECHNUNG` |
271    /// | `AdvancePayment` | `ABSCHLAGSRECHNUNG` |
272    /// | `Final` | `ABSCHLUSSRECHNUNG` (Schlussrechnung) |
273    /// | `PartialInvoice` | `ZWISCHENRECHNUNG` (mid-period settlement) |
274    /// | `CreditNote` / `Correction` / `Cancellation` | `None` |
275    ///
276    /// The three `None` cases have no BO4E Rechnungstyp; they are carried by
277    /// `istStorno`, `originalRechnungsnummer` and the `rechnungsart`
278    /// ZusatzAttribut on the emitted Rechnung.
279    #[must_use]
280    #[cfg(feature = "bo4e")]
281    pub fn rechnungstyp(&self) -> Option<rubo4e::current::Rechnungstyp> {
282        use rubo4e::current::Rechnungstyp as R;
283        match self {
284            Self::Initial => Some(R::Endkundenrechnung),
285            Self::AdvancePayment => Some(R::Abschlagsrechnung),
286            Self::Final => Some(R::Abschlussrechnung),
287            Self::PartialInvoice => Some(R::Zwischenrechnung),
288            Self::CreditNote | Self::Correction { .. } | Self::Cancellation { .. } => None,
289        }
290    }
291
292    /// Process-level Rechnungsart label (mako vocabulary, superset of BO4E).
293    ///
294    /// Emitted as the `rechnungsart` ZusatzAttribut for invoice types the BO4E
295    /// `Rechnungstyp` enum cannot express losslessly.
296    #[must_use]
297    pub fn rechnungsart(&self) -> &'static str {
298        match self {
299            Self::Initial => "RECHNUNG",
300            Self::AdvancePayment => "ABSCHLAGSRECHNUNG",
301            Self::CreditNote => "GUTSCHRIFT",
302            Self::Correction { .. } => "KORREKTURRECHNUNG",
303            Self::Cancellation { .. } => "STORNORECHNUNG",
304            Self::Final => "SCHLUSSRECHNUNG",
305            Self::PartialInvoice => "TEILRECHNUNG",
306        }
307    }
308
309    /// Returns the original invoice ID for corrections and cancellations.
310    #[must_use]
311    pub fn original_invoice_id(&self) -> Option<&str> {
312        match self {
313            Self::Correction {
314                original_invoice_id,
315                ..
316            }
317            | Self::Cancellation {
318                original_invoice_id,
319            } => Some(original_invoice_id),
320            _ => None,
321        }
322    }
323
324    /// `true` when this invoice reverses all positions of the original.
325    #[must_use]
326    pub fn is_reversal(&self) -> bool {
327        matches!(self, Self::Cancellation { .. })
328    }
329
330    /// `true` when this document discharges the advances the context carries.
331    ///
332    /// § 40 Abs. 1 EnWG makes the settling invoice itemise and deduct each
333    /// advance payment. An [`AdvancePayment`](Self::AdvancePayment) is the
334    /// document that *collects* one, so it discharges none: netting the
335    /// advances already paid against it would reduce the very request that asks
336    /// for the next.
337    #[must_use]
338    pub fn settles_advances(&self) -> bool {
339        !matches!(self, Self::AdvancePayment)
340    }
341}
342
343#[allow(clippy::derivable_impls)]
344impl Default for InvoiceType {
345    fn default() -> Self {
346        Self::Initial
347    }
348}
349
350// ── CustomerKategorie ─────────────────────────────────────────────────────────
351
352/// Customer category for the delivery point.
353///
354/// Determines applicable tariff categories, regulatory exemptions, and invoice
355/// disclosure requirements. Affects Stromsteuer (§9 Nr. 1 StromStG industrial
356/// exemption threshold), Preisangabenverordnung, and §41 EnWG disclosure depth.
357///
358/// ## Legal basis
359///
360/// - §2 Nr. 4 StromStG — definition of "Unternehmen des produzierenden Gewerbes"
361/// - § 12 StromNZV / §14 NAV — RLM metering thresholds
362/// - §41 Abs. 1 EnWG — invoice disclosure requirements vary by customer type
363/// - Grundversorgung (StromGVV) vs. Sondervertrag — different contract law
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
365#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
366pub enum CustomerKategorie {
367    /// Household customer (Haushaltskunde, §2 Nr. 25 EnWG).
368    ///
369    /// B2C. StromGVV / GasGVV apply. §40 EnWG Kilowattstundenpreis mandatory.
370    /// Invoice must include Verbrauchshistorie (§40 Abs. 2 EnWG).
371    #[default]
372    Haushalt,
373
374    /// Small commercial customer (Gewerbekunde, not a household but not RLM-obligated).
375    ///
376    /// B2B < 100 MWh/year. StromGVV / GasGVV still apply in most cases.
377    /// May be on SLP or transitioning to iMSys.
378    Gewerbe,
379
380    /// Industrial / large commercial customer (Sonderkunde).
381    ///
382    /// B2B ≥ 100 MWh/year electricity (§ 12 StromNZV), RLM mandatory.
383    /// Sondervertrag, not Grundversorgung. Eligible for §9 Nr. 1–3 StromStG
384    /// industrial exemption, KWKG Selbstbehaltsgrenze, and capacity pricing.
385    Industrie,
386
387    /// Agricultural customer (Landwirtschaft).
388    ///
389    /// Special BEHG/Energiesteuer treatment may apply for agricultural use.
390    /// §2 Abs. 1 Nr. 4 UStG (7% reduced VAT on certain agricultural inputs).
391    Landwirtschaft,
392
393    /// Public authority / public transport (öffentliche Einrichtung).
394    ///
395    /// May qualify for Konzessionsabgabe exemption (§2 Abs. 7 KAV).
396    OeffentlicheEinrichtung,
397}
398
399impl CustomerKategorie {
400    /// Whether this customer category typically uses SLP billing.
401    #[must_use]
402    pub fn is_slp_customer(self) -> bool {
403        matches!(self, Self::Haushalt | Self::Gewerbe)
404    }
405
406    /// Whether the annual Verbrauchshistorie (§40 Abs. 2 EnWG) applies.
407    ///
408    /// Mandatory for household customers (B2C). Recommended for Gewerbe.
409    /// Not required for industrial / RLM customers.
410    #[must_use]
411    pub fn requires_verbrauchshistorie(self) -> bool {
412        matches!(self, Self::Haushalt)
413    }
414
415    /// Whether the §40 EnWG Kilowattstundenpreis must appear on the invoice.
416    ///
417    /// Mandatory for all non-RLM electricity customers.
418    #[must_use]
419    pub fn requires_kilowattstundenpreis(self) -> bool {
420        !matches!(self, Self::Industrie)
421    }
422}
423
424// ── AbschlagDeduction ─────────────────────────────────────────────────────────
425
426/// An advance payment (Abschlag) previously collected from the customer.
427///
428/// Include these in `BillingContext::abschlage` for `InvoiceType::Final`
429/// (Jahresabrechnung) to deduct prior payments from the final amount due.
430///
431/// ## §41 EnWG
432///
433/// The annual final settlement must show each advance payment date and amount
434/// so the customer can verify the reconciliation.
435///
436/// ## §14 Abs. 5 Satz 2 UStG
437///
438/// An Endrechnung must deduct the advances **and the tax attributable to them**
439/// ("die vereinnahmten Teilentgelte und die auf sie entfallenden Steuerbeträge"),
440/// so each advance carries the rate it was invoiced at. A gross total alone
441/// cannot express that, which is why [`ust_satz`](Self::ust_satz) is not
442/// optional: an advance collected at 19 % and one collected at 7 % deduct
443/// different amounts of tax from the same gross sum.
444///
445/// [`betrag_eur`](Self::betrag_eur) is the **gross** amount the customer paid;
446/// the net and the tax it contains are derived from it by
447/// [`netto_eur`](Self::netto_eur) and [`ust_eur`](Self::ust_eur).
448///
449/// ## Example
450///
451/// A customer paying EUR 120/month → 12 × EUR 120 = EUR 1 440 in advances.
452/// If consumption bill = EUR 1 600, Zahlbetrag = EUR 160 (balance due).
453/// If consumption bill = EUR 1 300, Zahlbetrag = EUR -140 (refund).
454#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
455pub struct AbschlagDeduction {
456    /// Payment date (shown on the invoice for §41 EnWG compliance).
457    pub datum: time::Date,
458    /// Gross EUR amount already paid (positive = customer paid this amount).
459    pub betrag_eur: Decimal,
460    /// VAT rate contained in `betrag_eur`, as a fraction — `0.19` for 19 %.
461    ///
462    /// This is the rate the *advance* was invoiced at, which is not necessarily
463    /// the rate on the final invoice: a rate change mid-year leaves earlier
464    /// advances at the old rate.
465    pub ust_satz: Decimal,
466    /// Optional description shown on invoice (e.g. `"Abschlag März 2026"`).
467    #[serde(default)]
468    pub beschreibung: Option<String>,
469}
470
471impl AbschlagDeduction {
472    /// The net amount contained in the gross payment (Herausrechnung).
473    ///
474    /// `betrag_eur / (1 + ust_satz)`, rounded to cents. Returns the gross
475    /// unchanged when the rate is zero, so a zero-rated advance needs no
476    /// special-casing at the call site.
477    #[must_use]
478    pub fn netto_eur(&self) -> Decimal {
479        if self.ust_satz.is_zero() {
480            return self.betrag_eur;
481        }
482        (self.betrag_eur / (Decimal::ONE + self.ust_satz)).round_kfm(2)
483    }
484
485    /// The tax contained in the gross payment.
486    ///
487    /// Derived as `betrag_eur - netto_eur` rather than `netto × rate`, so that
488    /// net and tax always re-sum to the gross the customer actually paid.
489    #[must_use]
490    pub fn ust_eur(&self) -> Decimal {
491        self.betrag_eur - self.netto_eur()
492    }
493
494    /// Project into a [`billing::AdvancePayment`] carrying this advance's own tax.
495    ///
496    /// This is the structure EN 16931's flat BT-113 cannot hold and that
497    /// §14 Abs. 5 Satz 2 UStG requires on a settling invoice. It mirrors the
498    /// ZUGFeRD / Factur-X EXTENDED group `SpecifiedAdvancePayment` (BG-X-45).
499    ///
500    /// The category is derived from the rate: a positive rate is a standard-rated
501    /// advance, a zero rate a zero-rated one. An advance under reverse charge
502    /// (§13b UStG) is not expressible this way and is not produced here — such a
503    /// supply carries no advance tax to deduct.
504    ///
505    /// # Errors
506    ///
507    /// Returns [`EngineError::Arithmetic`](crate::EngineError::Arithmetic) if
508    /// the amounts overflow [`EuroAmount`].
509    pub fn to_advance_payment(&self) -> Result<billing::AdvancePayment, crate::EngineError> {
510        let category = if self.ust_satz.is_zero() {
511            billing::TaxCategory::ZeroRated
512        } else {
513            billing::TaxCategory::Standard
514        };
515        let entry = billing::TaxBreakdownEntry::new(
516            category,
517            self.ust_satz,
518            EuroAmount::checked_from_decimal(self.netto_eur())?,
519            EuroAmount::checked_from_decimal(self.ust_eur())?,
520        );
521        let advance =
522            billing::AdvancePayment::new(vec![entry])?.with_received_on(self.datum.to_string());
523        Ok(match &self.beschreibung {
524            Some(r) => advance.with_reference(r.clone()),
525            None => advance,
526        })
527    }
528}
529
530// ── SettlementForm ────────────────────────────────────────────────────────────
531
532/// How a settling invoice accounts for advances the customer already paid.
533///
534/// Both shapes are lawful and both are in use; they differ in what the document
535/// shows, not in what the customer ends up paying.
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
537#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
538pub enum SettlementForm {
539    /// **Endrechnung** — invoice the whole supply, then deduct the advances and
540    /// the tax contained in them (§14 Abs. 5 Satz 2 UStG).
541    ///
542    /// Totals and the VAT breakdown describe the full period; only the amount
543    /// payable shrinks. Deducting the advances but *not* their tax is the failure
544    /// this form has to avoid: under UStAE 14.8 Abs. 10 the issuer then owes the
545    /// tax shown plus the advance-related portion again under §14c Abs. 1 — the
546    /// same tax twice.
547    #[default]
548    Endrechnung,
549
550    /// **Restrechnung** — invoice only the remainder; the advances are not listed.
551    ///
552    /// Structurally simpler, and what the BMF recommends for e-invoices (Schreiben
553    /// v. 15.10.2024, Rn. 48), because EN 16931's core profiles have nowhere to
554    /// carry per-advance tax. The taxable base is the residual per rate rather
555    /// than the full supply.
556    Restrechnung,
557}
558
559// ── BillingPeriod ─────────────────────────────────────────────────────────────
560
561/// A validated billing period — first and last day, both inclusive.
562///
563/// The constructor refuses `from > to`, so an inverted period is
564/// unrepresentable everywhere downstream: no provider, no pro-rata helper,
565/// no JSON assembly ever needs to re-check the ordering.
566///
567/// Deserialization runs through the same validation
568/// (`#[serde(try_from = …)]`), so a period arriving over the wire holds the
569/// same invariant as one built in code.
570#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
571#[serde(try_from = "PeriodEndpoints", into = "PeriodEndpoints")]
572pub struct BillingPeriod {
573    from: time::Date,
574    to: time::Date,
575}
576
577/// Serde carrier for [`BillingPeriod`] — validation happens in `TryFrom`.
578#[derive(serde::Serialize, serde::Deserialize)]
579struct PeriodEndpoints {
580    from: time::Date,
581    to: time::Date,
582}
583
584impl TryFrom<PeriodEndpoints> for BillingPeriod {
585    type Error = crate::EngineError;
586    fn try_from(p: PeriodEndpoints) -> Result<Self, Self::Error> {
587        Self::new(p.from, p.to)
588    }
589}
590
591impl From<BillingPeriod> for PeriodEndpoints {
592    fn from(p: BillingPeriod) -> Self {
593        Self {
594            from: p.from,
595            to: p.to,
596        }
597    }
598}
599
600impl BillingPeriod {
601    /// Build a period from first and last day (both inclusive).
602    ///
603    /// # Errors
604    ///
605    /// [`EngineError::InvalidPeriod`](crate::EngineError::InvalidPeriod) when `from > to`.
606    pub fn new(from: time::Date, to: time::Date) -> Result<Self, crate::EngineError> {
607        if from > to {
608            return Err(crate::EngineError::InvalidPeriod { from, to });
609        }
610        Ok(Self { from, to })
611    }
612
613    /// First day of the period (inclusive).
614    #[must_use]
615    pub const fn from(self) -> time::Date {
616        self.from
617    }
618
619    /// Last day of the period (inclusive).
620    #[must_use]
621    pub const fn to(self) -> time::Date {
622        self.to
623    }
624
625    /// Number of calendar days, inclusive of both endpoints. Always ≥ 1.
626    #[must_use]
627    pub fn days(self) -> i64 {
628        (self.to - self.from).whole_days() + 1
629    }
630
631    /// Whether the given date falls inside the period.
632    #[must_use]
633    pub fn contains(self, date: time::Date) -> bool {
634        self.from <= date && date <= self.to
635    }
636}
637
638impl Default for BillingPeriod {
639    /// Placeholder single-day period at `Date::MIN` — used only by
640    /// `BillingContext::default()`. Always set an explicit period before
641    /// billing.
642    fn default() -> Self {
643        Self {
644            from: time::Date::MIN,
645            to: time::Date::MIN,
646        }
647    }
648}
649
650impl std::fmt::Display for BillingPeriod {
651    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652        write!(f, "{}..{}", self.from, self.to)
653    }
654}
655
656// ── Vertragsart ───────────────────────────────────────────────────────────────
657
658/// The contractual regime the delivery runs under — drives which invoice
659/// disclosures and period limits apply.
660#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
661#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
662pub enum Vertragsart {
663    /// Freely negotiated supply contract (§41 EnWG). The default.
664    #[default]
665    Sondervertrag,
666
667    /// Grundversorgung (§36 EnWG, StromGVV/GasGVV): the published Allgemeine
668    /// Preise apply, termination per §20 StromGVV/GasGVV is two weeks.
669    /// Emitted as the `vertragsart` ZusatzAttribut so the invoice states the
670    /// regime the prices come from.
671    Grundversorgung,
672
673    /// Ersatzversorgung (§38 EnWG): the fallback supply when energy is drawn
674    /// without an assignable contract. Ends by law after **three months** at
675    /// the latest (§ 38 Abs. 4 EnWG) — the engine refuses to bill a
676    /// longer Ersatzversorgung period, because such a supply cannot exist.
677    Ersatzversorgung,
678}
679
680impl Vertragsart {
681    /// The label emitted as the `vertragsart` ZusatzAttribut.
682    #[must_use]
683    pub const fn label(self) -> &'static str {
684        match self {
685            Self::Sondervertrag => "SONDERVERTRAG",
686            Self::Grundversorgung => "GRUNDVERSORGUNG",
687            Self::Ersatzversorgung => "ERSATZVERSORGUNG",
688        }
689    }
690}
691
692// ── BillingContext ────────────────────────────────────────────────────────────
693
694/// Immutable billing metadata — the *context* for one invoice generation run.
695///
696/// Every [`BillingProvider`][crate::BillingProvider] receives a reference to
697/// the same `BillingContext` so all positions share identical period, party IDs,
698/// and regulatory rates.
699///
700/// ## New in this version
701///
702/// - `vertragsbeginn` / `vertragsende` — enables automatic pro-rata billing
703///   when a contract starts or ends mid-period
704/// - `zaehler_id` — §41 EnWG Zählernummer on invoice
705/// - `abschlage` — advance payments deducted in `Invoice::zahlbetrag_eur`
706///   (required for `InvoiceType::Final` / Jahresabrechnung)
707///
708/// ## Example
709///
710/// ```rust
711/// use energy_billing::{AbschlagDeduction, BillingContext, BillingPeriod, InvoiceType, RegulatoryRates};
712/// use time::macros::date;
713/// use rust_decimal::dec;
714///
715/// let ctx = BillingContext {
716///     malo_id: "51238696012".to_owned(),
717///     lf_mp_id: "9900000000001".to_owned(),
718///     rechnungsnummer: "R2026-001".to_owned(),
719///     period: BillingPeriod::new(date!(2026-01-01), date!(2026-12-31)).unwrap(),
720///     invoice_type: InvoiceType::Final,
721///     regulatory_rates: RegulatoryRates::default(),
722///     contract_id: None,
723///     abschlage: vec![
724///         AbschlagDeduction {
725///             datum: date!(2026-01-15),
726///             betrag_eur: dec!(120.00),
727///             ust_satz: dec!(0.19),
728///             beschreibung: Some("Abschlag Januar 2026".to_owned()),
729///         },
730///     ],
731///     ..Default::default()
732/// };
733/// assert_eq!(ctx.total_abschlage_eur(), dec!(120.00));
734/// ```
735#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
736pub struct BillingContext {
737    /// 11-digit Marktlokations-ID of the delivery point.
738    pub malo_id: String,
739
740    /// BDEW/DVGW Codenummer of the Lieferant (invoice issuer).
741    pub lf_mp_id: String,
742
743    /// Invoice number (Rechnungsnummer) — unique per invoice.
744    ///
745    /// Operator's responsibility to ensure uniqueness. Recommended format:
746    /// `{prefix}-{year}-{sequence}` (e.g. `"INV-2026-000001"`).
747    pub rechnungsnummer: String,
748
749    /// The billing period — validated, `from > to` unrepresentable.
750    pub period: BillingPeriod,
751
752    /// Invoice type: initial, correction, cancellation, or final settlement.
753    pub invoice_type: InvoiceType,
754
755    /// The contractual regime — Sondervertrag, Grundversorgung, or
756    /// Ersatzversorgung. Emitted as the `vertragsart` ZusatzAttribut; an
757    /// Ersatzversorgung period longer than three months blocks the run
758    /// (§ 38 Abs. 4 EnWG).
759    #[serde(default)]
760    pub vertragsart: Vertragsart,
761
762    /// How advances are accounted for on a settling invoice.
763    ///
764    /// Only consulted when `abschlage` is non-empty. See [`SettlementForm`].
765    #[serde(default)]
766    pub settlement_form: SettlementForm,
767
768    /// The VAT rate a `minimum_invoice_eur_brutto` top-up is agreed at.
769    ///
770    /// The Mindestbetrag is a contractual charge, not a statutory one, so the
771    /// contract names its rate. `None` uses the period's standard rate — right
772    /// for a single-rate invoice and wrong for a mixed one, where the gross-up
773    /// otherwise misses the configured minimum by the rate difference.
774    #[serde(default)]
775    pub minimum_invoice_mwst_rate: Option<Decimal>,
776
777    /// Statutory levy rates (Stromsteuer, Energiesteuer, BEHG, MwSt).
778    ///
779    /// Sourced from `billingd.toml [rates]` — never hardcoded in the library.
780    pub regulatory_rates: RegulatoryRates,
781
782    /// The day the invoice is issued — the day it reaches the customer.
783    ///
784    /// `None` keeps the library clock-free and falls back to the period end.
785    /// A caller that has a clock should set it, because two statutory facts
786    /// hang off the issue date and neither is measurable from the period:
787    ///
788    /// - **§ 40c Abs. 1 EnWG** makes the amount due at the earliest **two weeks
789    ///   after the payment request reaches the customer**. Counting from the
790    ///   period end instead meant a catch-up run or a late Schlussrechnung
791    ///   issued an invoice that was *already overdue on arrival*, which the
792    ///   dunning downstream then acted on.
793    /// - § 14 Abs. 4 Nr. 3 UStG wants the actual Ausstellungsdatum.
794    #[serde(default)]
795    pub issue_date: Option<time::Date>,
796
797    /// The party the invoice is addressed to. See [`Rechnungsempfaenger`].
798    ///
799    /// `None` names the Marktlokation instead, which is the documented
800    /// degradation for a MaLo with no contract on file.
801    #[serde(default)]
802    pub rechnungsempfaenger: Option<Rechnungsempfaenger>,
803
804    /// Optional contract reference (for LF internal use / ERP routing).
805    #[serde(default)]
806    pub contract_id: Option<String>,
807
808    /// Contract start date (§41 EnWG).
809    ///
810    /// When set AND `period_from < vertragsbeginn`, `billing_days_fraction()`
811    /// returns a value < 1.0 for pro-rata first-month billing.
812    #[serde(default)]
813    pub vertragsbeginn: Option<time::Date>,
814
815    /// Contract end date.
816    ///
817    /// When set AND `period_to > vertragsende`, `billing_days_fraction()`
818    /// returns a value < 1.0 for pro-rata last-month billing.
819    #[serde(default)]
820    pub vertragsende: Option<time::Date>,
821
822    /// Zählernummer (§41 EnWG — mandatory on electricity invoices).
823    ///
824    /// Appears on the invoice as an informational line item.
825    #[serde(default)]
826    pub zaehler_id: Option<String>,
827
828    /// Advance payments to deduct from the final invoice (Jahresabrechnung).
829    ///
830    /// Used exclusively with `InvoiceType::Final`. Each entry produces an
831    /// `Abschlag` deduction line in `Invoice::zahlbetrag_eur`.
832    ///
833    /// The German retail practice: monthly advance payments are collected
834    /// throughout the year; the annual settlement debits/credits the difference.
835    #[serde(default)]
836    pub abschlage: Vec<AbschlagDeduction>,
837
838    /// §40 Abs. 2 EnWG — Verbrauchshistorie for invoice display.
839    ///
840    /// When set, appears as informational ZusatzAttribute in the Rechnung JSON
841    /// showing the customer's consumption history vs. prior year and average.
842    #[serde(default)]
843    pub verbrauchshistorie: Option<Verbrauchshistorie>,
844
845    /// §40 Abs. 1 EnWG contract facts, emitted as ZusatzAttribute.
846    #[serde(default)]
847    pub vertragsinformationen: Option<Vertragsinformationen>,
848
849    /// §40 Abs. 2 EnWG — consumer information (supplier contact,
850    /// Schlichtungsstelle, BNetzA Verbraucherservice, Energieberatung,
851    /// Wechselhinweis). `None` falls back to
852    /// [`Verbraucherinformationen::default`] at render time — the statutory
853    /// hints are never omitted from a Rechnung.
854    pub verbraucherinformationen: Option<Verbraucherinformationen>,
855
856    /// §42 EnWG — Stromkennzeichnung, structured.
857    ///
858    /// Fuel-mix percentages, the specific CO₂ emissions (§42 Abs. 2 Nr. 2 —
859    /// mandatory on every electricity invoice), and HKN certification. Emitted
860    /// as the `stromkennzeichnung` ZusatzAttribut with the structure intact;
861    /// prose belongs in [`crate::tariff::EnergieQuellen::beschreibung`].
862    ///
863    /// Structured rather than a free-text `energiemix` string, which cannot
864    /// carry the CO₂ figure the law names explicitly.
865    #[serde(default)]
866    pub energiequellen: Option<crate::tariff::EnergieQuellen>,
867
868    /// Minimum invoice amount (brutto) in EUR.
869    ///
870    /// When set and the computed `brutto_eur < minimum_invoice_eur_brutto`, the
871    /// engine adds a `Mindestbetrag` position to reach the minimum.
872    ///
873    /// Set from `TariffInput.minimum_invoice_eur_brutto` by the service layer
874    /// (`billingd`) when building the billing context.
875    ///
876    /// ## Use case
877    ///
878    /// B2B contracts with a minimum annual consumption commitment
879    /// (Mindestabnahmeverpflichtung). The customer pays at least this amount
880    /// per billing period regardless of actual consumption.
881    #[serde(default)]
882    pub minimum_invoice_eur_brutto: Option<Decimal>,
883
884    /// BDEW-Codenummer of the Netzbetreiber (§41 EnWG — mandatory on invoices).
885    ///
886    /// German energy invoices must identify the network operator who provides
887    /// the grid infrastructure at the delivery point (§41 Abs. 1 Nr. 5 EnWG).
888    /// This appears as `"netzbetreiber"."marktpartnercode"` in the Rechnung JSON.
889    ///
890    /// When `None`, the `netzbetreiber` field is omitted from the invoice JSON.
891    /// For full §41 EnWG compliance on retail electricity/gas invoices, always set this.
892    #[serde(default)]
893    pub nb_mp_id: Option<String>,
894
895    /// Unique billing run identifier for audit trail and duplicate detection.
896    ///
897    /// When set, propagated to `Invoice.billing_run_id` and included in the
898    /// Rechnung JSON as a `ZusatzAttribut` under key `"billingRunId"`.
899    ///
900    /// Use a UUID v4 generated by `billingd` at invoice time to correlate the
901    /// database record (`billing_records.id`) with calculation outputs.
902    #[serde(default)]
903    pub billing_run_id: Option<String>,
904
905    /// Customer category — drives regulatory exemptions and invoice disclosure.
906    ///
907    /// | Category | SLP | Verbrauchshistorie | §40 kWh-Preis |
908    /// |---|---|---|---|
909    /// | `Haushalt` | ✅ | Mandatory | Mandatory |
910    /// | `Gewerbe` | ✅ | Recommended | Mandatory |
911    /// | `Industrie` | ❌ (RLM) | — | — |
912    /// | `Landwirtschaft` | ✅ | Recommended | Mandatory |
913    /// | `OeffentlicheEinrichtung` | ✅/❌ | — | Mandatory |
914    ///
915    /// Defaults to `Haushalt` — always set explicitly for B2B customers.
916    #[serde(default)]
917    pub kundenkategorie: CustomerKategorie,
918
919    /// §13b UStG reverse charge (Steuerschuldnerschaft des Leistungsempfängers).
920    ///
921    /// Set `true` when the customer is a **Stromwiederverkäufer** (electricity/gas
922    /// reseller, §13b Abs. 2 Nr. 5 lit. b UStG): the whole supply is invoiced net
923    /// and the recipient owes the VAT. The engine then marks every supply position
924    /// reverse-charge before the MwSt pass, so the `MwStProvider` charges no VAT and
925    /// the EN 16931 tax breakdown carries an `AE` subtotal instead of `S`/`Z`.
926    /// Defaults to `false` (normal Steuerschuldnerschaft des Leistenden).
927    #[serde(default)]
928    pub reverse_charge: bool,
929}
930
931impl BillingContext {
932    /// First day of the billing period (inclusive).
933    #[must_use]
934    pub const fn period_from(&self) -> time::Date {
935        self.period.from()
936    }
937
938    /// Last day of the billing period (inclusive).
939    #[must_use]
940    pub const fn period_to(&self) -> time::Date {
941        self.period.to()
942    }
943
944    /// The day the invoice is issued: [`Self::issue_date`], else the period end.
945    #[must_use]
946    pub fn ausstellungsdatum(&self) -> time::Date {
947        match self.issue_date {
948            Some(d) => d,
949            None => self.period.to(),
950        }
951    }
952
953    /// The day payment falls due — two weeks after issue.
954    ///
955    /// § 40c Abs. 1 EnWG: due at the earliest two weeks after the payment
956    /// request reaches the customer. Measured from the **issue** date, so an
957    /// invoice for an old period does not arrive already overdue.
958    #[must_use]
959    pub fn faelligkeitsdatum(&self) -> time::Date {
960        self.ausstellungsdatum()
961            .saturating_add(time::Duration::days(14))
962    }
963
964    /// Number of calendar days in the billing period.
965    ///
966    /// Used for Grundpreis (daily rate × days) and pro-rata calculations.
967    #[must_use]
968    pub fn days(&self) -> i64 {
969        self.period.days()
970    }
971
972    /// Pro-rata fraction of the billing period actually billable.
973    ///
974    /// Returns `None` when the full period is billable (no pro-rata applies).
975    /// Returns `Some(fraction)` where `0 < fraction < 1` when:
976    /// - `vertragsbeginn` falls within the period (late contract start)
977    /// - `vertragsende` falls within the period (early contract end)
978    ///
979    /// ## §41 EnWG — pro-rata billing
980    ///
981    /// First and last billing periods are prorated to the actual contract days.
982    ///
983    /// # Example
984    ///
985    /// ```rust
986    /// use energy_billing::{BillingContext, BillingPeriod, InvoiceType, RegulatoryRates};
987    /// use time::macros::date;
988    ///
989    /// let ctx = BillingContext {
990    ///     period: BillingPeriod::new(date!(2026-01-01), date!(2026-01-31)).unwrap(),
991    ///     vertragsbeginn: Some(date!(2026-01-16)), // contract started mid-month
992    ///     ..Default::default()
993    /// };
994    /// let frac = ctx.billing_days_fraction().unwrap();
995    /// // 16 billable days out of 31: ≈ 0.516
996    /// assert!(frac > rust_decimal::dec!(0.50) && frac < rust_decimal::dec!(0.55));
997    /// ```
998    #[must_use]
999    pub fn billing_days_fraction(&self) -> Option<Decimal> {
1000        let period_days = self.days();
1001        if period_days <= 0 {
1002            return None;
1003        }
1004
1005        // Effective start: max(period_from, vertragsbeginn)
1006        let effective_from = match self.vertragsbeginn {
1007            Some(vb) if vb > self.period_from() => vb,
1008            _ => self.period_from(),
1009        };
1010
1011        // Effective end: min(period_to, vertragsende)
1012        let effective_to = match self.vertragsende {
1013            Some(ve) if ve < self.period_to() => ve,
1014            _ => self.period_to(),
1015        };
1016
1017        let billable = (effective_to - effective_from).whole_days() + 1;
1018        if billable <= 0 {
1019            return None;
1020        }
1021        if billable >= period_days {
1022            return None; // full period, no pro-rata
1023        }
1024
1025        let frac = Decimal::from(billable) / Decimal::from(period_days);
1026        Some(frac.round_kfm(6))
1027    }
1028
1029    /// Total advance payments included in this context.
1030    ///
1031    /// For `InvoiceType::Final`, this equals the amount deducted from
1032    /// `Invoice::zahlbetrag_eur`.
1033    #[must_use]
1034    pub fn total_abschlage_eur(&self) -> Decimal {
1035        self.abschlage.iter().map(|a| a.betrag_eur).sum()
1036    }
1037
1038    /// Return `(active_days, total_days)` for use with `billing::prorate` /
1039    /// `billing::prorate_amount`.
1040    ///
1041    /// - `total_days` = calendar days in the billing period (`days()`)
1042    /// - `active_days` = billable days after clipping to `vertragsbeginn` /
1043    ///   `vertragsende`
1044    ///
1045    /// When no pro-rata applies (full period billable), `active_days == total_days`.
1046    /// When the period would yield zero billable days, returns `(0, total_days)`.
1047    ///
1048    /// ## Example — Grundpreis pro-rata
1049    ///
1050    /// ```rust
1051    /// # use energy_billing::{BillingContext, BillingPeriod};
1052    /// # use time::macros::date;
1053    /// let ctx = BillingContext {
1054    ///     period: BillingPeriod::new(date!(2026-01-01), date!(2026-01-31)).unwrap(),
1055    ///     vertragsbeginn: Some(date!(2026-01-16)),
1056    ///     ..Default::default()
1057    /// };
1058    /// let (active, total) = ctx.prorate_days();
1059    /// assert_eq!(total, 31);
1060    /// assert_eq!(active, 16); // Jan 16–31
1061    /// ```
1062    #[must_use]
1063    pub fn prorate_days(&self) -> (u32, u32) {
1064        let total = self.days().max(0) as u32;
1065        if total == 0 {
1066            return (0, 1);
1067        }
1068        // Effective start: max(period_from, vertragsbeginn)
1069        let effective_from = self
1070            .vertragsbeginn
1071            .filter(|&vb| vb > self.period_from())
1072            .unwrap_or(self.period_from());
1073        // Effective end: min(period_to, vertragsende)
1074        let effective_to = self
1075            .vertragsende
1076            .filter(|&ve| ve < self.period_to())
1077            .unwrap_or(self.period_to());
1078        let active = ((effective_to - effective_from).whole_days() + 1).max(0) as u32;
1079        (active.min(total), total)
1080    }
1081
1082    /// The active contract window inside the billing period.
1083    ///
1084    /// `(from, to)` clipped by `vertragsbeginn` / `vertragsende`, inclusive.
1085    /// Returns `None` when the contract does not overlap the period at all.
1086    #[must_use]
1087    pub fn active_window(&self) -> Option<(time::Date, time::Date)> {
1088        let from = self
1089            .vertragsbeginn
1090            .filter(|&vb| vb > self.period_from())
1091            .unwrap_or(self.period_from());
1092        let to = self
1093            .vertragsende
1094            .filter(|&ve| ve < self.period_to())
1095            .unwrap_or(self.period_to());
1096        (from <= to).then_some((from, to))
1097    }
1098
1099    /// The billed period expressed in **months**, for EUR/month rates.
1100    ///
1101    /// Each calendar month contributes `billed days ÷ that month's length`, so
1102    /// January 1–31 is exactly `1`, a full year is exactly `12`, and a
1103    /// mid-month move-in gets the fraction of the month it actually occupied —
1104    /// none of which `days ÷ 30.4375` produces (it makes a billed January
1105    /// 1.0185 months, and a leap year 12.0164).
1106    ///
1107    /// Clipped to the active contract window, like every other periodic charge.
1108    #[must_use]
1109    pub fn billed_months(&self) -> rust_decimal::Decimal {
1110        use rust_decimal::Decimal;
1111        let Some((from, to)) = self.active_window() else {
1112            return Decimal::ZERO;
1113        };
1114        let mut months = Decimal::ZERO;
1115        let mut cursor = from;
1116        while cursor <= to {
1117            let len = time::util::days_in_month(cursor.month(), cursor.year());
1118            let month_end = time::Date::from_calendar_date(cursor.year(), cursor.month(), len)
1119                .expect("last day of the month is a valid date");
1120            let slice_end = month_end.min(to);
1121            let days = (slice_end - cursor).whole_days() + 1;
1122            months += Decimal::from(days) / Decimal::from(len);
1123            let Some(next) = month_end.next_day() else {
1124                break;
1125            };
1126            cursor = next;
1127        }
1128        months
1129    }
1130
1131    /// The billed period expressed in **years**, for EUR/year rates.
1132    ///
1133    /// Leap-aware: each calendar year contributes `billed days ÷ that year's
1134    /// length`, so 2024 divides by 366 and 2025 by 365.
1135    #[must_use]
1136    pub fn billed_years(&self) -> rust_decimal::Decimal {
1137        use rust_decimal::Decimal;
1138        let Some((from, to)) = self.active_window() else {
1139            return Decimal::ZERO;
1140        };
1141        let mut years = Decimal::ZERO;
1142        let mut cursor = from;
1143        while cursor <= to {
1144            let len = time::util::days_in_year(cursor.year());
1145            let year_end = time::Date::from_calendar_date(cursor.year(), time::Month::December, 31)
1146                .expect("31 December is a valid date");
1147            let slice_end = year_end.min(to);
1148            let days = (slice_end - cursor).whole_days() + 1;
1149            years += Decimal::from(days) / Decimal::from(len);
1150            let Some(next) = year_end.next_day() else {
1151                break;
1152            };
1153            cursor = next;
1154        }
1155        years
1156    }
1157}
1158
1159#[cfg(test)]
1160mod period_fraction_tests {
1161    use super::*;
1162    use rust_decimal::dec;
1163    use time::macros::date;
1164
1165    fn ctx(from: time::Date, to: time::Date) -> BillingContext {
1166        BillingContext {
1167            period: BillingPeriod::new(from, to).expect("period"),
1168            ..Default::default()
1169        }
1170    }
1171
1172    /// The property `days ÷ 30.4375` cannot have: calendar-aligned periods come
1173    /// out exact.
1174    #[test]
1175    fn calendar_aligned_periods_are_exact() {
1176        assert_eq!(
1177            ctx(date!(2026 - 01 - 01), date!(2026 - 01 - 31)).billed_months(),
1178            dec!(1)
1179        );
1180        assert_eq!(
1181            ctx(date!(2026 - 02 - 01), date!(2026 - 02 - 28)).billed_months(),
1182            dec!(1)
1183        );
1184        assert_eq!(
1185            ctx(date!(2026 - 01 - 01), date!(2026 - 12 - 31)).billed_months(),
1186            dec!(12)
1187        );
1188        // …in a leap year too, where 366 ÷ 30.4375 would be 12.0246.
1189        assert_eq!(
1190            ctx(date!(2024 - 01 - 01), date!(2024 - 12 - 31)).billed_months(),
1191            dec!(12)
1192        );
1193        assert_eq!(
1194            ctx(date!(2024 - 01 - 01), date!(2024 - 12 - 31)).billed_years(),
1195            dec!(1)
1196        );
1197    }
1198
1199    /// A mid-month move-in pays for the part of that month it occupied.
1200    #[test]
1201    fn a_partial_month_is_that_months_own_fraction() {
1202        // 16–31 January = 16 of 31 days.
1203        let c = BillingContext {
1204            period: BillingPeriod::new(date!(2026 - 01 - 01), date!(2026 - 01 - 31)).unwrap(),
1205            vertragsbeginn: Some(date!(2026 - 01 - 16)),
1206            ..Default::default()
1207        };
1208        assert_eq!(c.billed_months(), dec!(16) / dec!(31));
1209        // …and February's 13 days are 13/28, not 13/30.4375.
1210        let c = ctx(date!(2026 - 02 - 16), date!(2026 - 02 - 28));
1211        assert_eq!(c.billed_months(), dec!(13) / dec!(28));
1212    }
1213
1214    /// A contract that ended before the period began bills nothing.
1215    #[test]
1216    fn a_closed_contract_bills_no_months() {
1217        let c = BillingContext {
1218            period: BillingPeriod::new(date!(2026 - 03 - 01), date!(2026 - 03 - 31)).unwrap(),
1219            vertragsende: Some(date!(2026 - 02 - 10)),
1220            ..Default::default()
1221        };
1222        assert_eq!(c.active_window(), None);
1223        assert_eq!(c.billed_months(), rust_decimal::Decimal::ZERO);
1224        assert_eq!(c.billed_years(), rust_decimal::Decimal::ZERO);
1225    }
1226}
1227
1228#[cfg(test)]
1229mod faelligkeit_tests {
1230    use super::*;
1231    use time::macros::date;
1232
1233    fn ctx(period_to: time::Date, issue: Option<time::Date>) -> BillingContext {
1234        BillingContext {
1235            period: BillingPeriod::new(date!(2026 - 01 - 01), period_to).expect("period"),
1236            issue_date: issue,
1237            ..Default::default()
1238        }
1239    }
1240
1241    #[test]
1242    fn without_a_clock_the_period_end_stands_in_for_the_issue_date() {
1243        // The library is pure; a caller with no clock still gets a document.
1244        let c = ctx(date!(2026 - 01 - 31), None);
1245        assert_eq!(c.ausstellungsdatum(), date!(2026 - 01 - 31));
1246        assert_eq!(c.faelligkeitsdatum(), date!(2026 - 02 - 14));
1247    }
1248
1249    #[test]
1250    fn the_due_date_runs_from_the_issue_date_not_the_period_end() {
1251        // § 40c Abs. 1 EnWG measures the two weeks from when the payment
1252        // request reaches the customer, so a catch-up run billing an old period
1253        // must date the Fälligkeit from the issue date. Measuring from the
1254        // period end would deliver a document overdue on arrival, and the
1255        // dunning downstream would act on it.
1256        let c = ctx(date!(2026 - 01 - 31), Some(date!(2026 - 06 - 10)));
1257        assert_eq!(c.ausstellungsdatum(), date!(2026 - 06 - 10));
1258        assert_eq!(c.faelligkeitsdatum(), date!(2026 - 06 - 24));
1259        assert!(
1260            c.faelligkeitsdatum() > c.ausstellungsdatum(),
1261            "an invoice is never due before it is issued"
1262        );
1263    }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::*;
1269    use rust_decimal::dec;
1270    use time::macros::date;
1271
1272    fn base_ctx() -> BillingContext {
1273        BillingContext {
1274            period: BillingPeriod::new(date!(2026 - 01 - 01), date!(2026 - 01 - 31)).unwrap(),
1275            ..Default::default()
1276        }
1277    }
1278
1279    #[test]
1280    fn days_full_january() {
1281        assert_eq!(base_ctx().days(), 31);
1282    }
1283
1284    #[test]
1285    fn billing_days_fraction_no_pro_rata_returns_none() {
1286        assert!(base_ctx().billing_days_fraction().is_none());
1287    }
1288
1289    #[test]
1290    fn billing_days_fraction_mid_month_start() {
1291        let ctx = BillingContext {
1292            vertragsbeginn: Some(date!(2026 - 01 - 16)),
1293            ..base_ctx()
1294        };
1295        let frac = ctx.billing_days_fraction().unwrap();
1296        // billable: Jan 16..31 = 16 days out of 31
1297        let expected = Decimal::from(16) / Decimal::from(31);
1298        assert_eq!(frac, expected.round_kfm(6));
1299    }
1300
1301    #[test]
1302    fn billing_days_fraction_mid_month_end() {
1303        let ctx = BillingContext {
1304            vertragsende: Some(date!(2026 - 01 - 15)),
1305            ..base_ctx()
1306        };
1307        let frac = ctx.billing_days_fraction().unwrap();
1308        // billable: Jan 01..15 = 15 days out of 31
1309        let expected = Decimal::from(15) / Decimal::from(31);
1310        assert_eq!(frac, expected.round_kfm(6));
1311    }
1312
1313    #[test]
1314    fn total_abschlage_sums_correctly() {
1315        let ctx = BillingContext {
1316            abschlage: vec![
1317                AbschlagDeduction {
1318                    datum: date!(2026 - 01 - 15),
1319                    betrag_eur: dec!(100.00),
1320                    ust_satz: dec!(0.19),
1321                    beschreibung: None,
1322                },
1323                AbschlagDeduction {
1324                    datum: date!(2026 - 02 - 15),
1325                    betrag_eur: dec!(120.00),
1326                    ust_satz: dec!(0.19),
1327                    beschreibung: None,
1328                },
1329            ],
1330            ..base_ctx()
1331        };
1332        assert_eq!(ctx.total_abschlage_eur(), dec!(220.00));
1333    }
1334}