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}