energy-billing 0.10.0

Pure multi-product retail energy billing for German markets. STROM, GAS, WAERME, SOLAR, EEG, EINSPEISUNG, §14a WAERMEPUMPE/WALLBOX, HEMS, EMOBILITY, ENERGIEDIENSTLEISTUNG, §41a EPEX dynamic. Regulatory rates: §3 StromStG, §2 EnergieStG, BEHG CO₂. BO4E Rechnung JSON. Zero I/O, zero async, no float money.
Documentation
//! `BillingEngine` — the composition root for multi-product invoice generation.
//!
//! Register one `BillingProvider` per product/service. Call `bill()` to run all
//! providers in order and assemble the `Invoice`.
//!
//! ## Execution model
//!
//! Providers run in two passes:
//!
//! **Pass 1 — commodity / levy providers** (`is_tax_pass() == false`):
//! Each receives the accumulated positions from all earlier providers.
//!
//! **Pass 2 — tax providers** (`is_tax_pass() == true`, typically `MwStProvider`):
//! Sees all commodity/levy positions as `prior`, computes tax on the net base.
//!
//! ## Example
//!
//! ```rust
//! use energy_billing::{
//!     BillingContext, BillingEngine, ElectricityProvider, GasProvider,
//!     InvoiceType, MwStProvider, Quantities, RegulatoryRates,
//!     TariffInput, GridInput, MeterInput, GasMeterInput,
//! };
//! use rust_decimal_macros::dec;
//! use time::macros::date;
//!
//! let rates = RegulatoryRates::default();
//! let ctx = BillingContext {
//!     malo_id:          "51238696781".to_owned(),
//!     lf_mp_id:         "9900000000001".to_owned(),
//!     rechnungsnummer:  "R2026-001".to_owned(),
//!     period_from:       date!(2026-01-01),
//!     period_to:         date!(2026-01-31),
//!     invoice_type:      InvoiceType::Initial,
//!     contract_id:       None,
//!     regulatory_rates:  rates.clone(),
//! };
//! let quantities = Quantities {
//!     electricity: Some(MeterInput {
//!         arbeitsmenge_kwh: dec!(500),
//!         ..Default::default()
//!     }),
//!     ..Default::default()
//! };
//! let tariff: TariffInput = serde_json::from_str(r#"{"category":"STROM","arbeitspreis_ct_per_kwh":30.0}"#).unwrap();
//! let invoice = BillingEngine::new()
//!     .add(ElectricityProvider::from_tariff(&tariff, &GridInput::default()))
//!     .add(MwStProvider::new(dec!(0.19)))
//!     .bill(ctx, &quantities)
//!     .unwrap();
//! assert!(invoice.brutto_eur > invoice.netto_eur);
//! ```

use billing::BillingError;

use crate::context::BillingContext;
use crate::invoice::Invoice;
use crate::position::BillingPosition;
use crate::provider::BillingProvider;
use crate::quantities::Quantities;

/// The composition root for multi-product invoice generation.
#[derive(Default)]
pub struct BillingEngine {
    providers: Vec<Box<dyn BillingProvider>>,
}

impl BillingEngine {
    /// Create an empty engine. Register providers with [`add`](Self::add).
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a `BillingProvider`. Returns `self` for method chaining.
    ///
    /// Providers run in registration order. Register tax providers (e.g.
    /// `MwStProvider`) **last** — they will automatically run in a second pass.
    #[must_use]
    #[allow(clippy::should_implement_trait)] // `add` is idiomatic for builder APIs in Rust
    pub fn add<P: BillingProvider + 'static>(mut self, provider: P) -> Self {
        self.providers.push(Box::new(provider));
        self
    }

    /// Run all providers and assemble an `Invoice`.
    ///
    /// Two-pass execution:
    /// 1. Commodity + levy providers (all `is_tax_pass() == false`)
    /// 2. Tax providers (all `is_tax_pass() == true`)
    pub fn bill(
        self,
        ctx: BillingContext,
        quantities: &Quantities,
    ) -> Result<Invoice, BillingError> {
        let mut positions: Vec<BillingPosition> = Vec::new();

        // ── Pass 1: commodity, grid, levy ─────────────────────────────────────
        for provider in self.providers.iter().filter(|p| !p.is_tax_pass()) {
            let new = provider.bill(&ctx, quantities, &positions)?;
            positions.extend(new);
        }

        // ── Pass 2: taxes (MwSt sees the full commodity/levy base) ─────────────
        let pre_tax_snap: Vec<BillingPosition> = positions.clone();
        for provider in self.providers.iter().filter(|p| p.is_tax_pass()) {
            let new = provider.bill(&ctx, quantities, &pre_tax_snap)?;
            positions.extend(new);
        }

        Ok(Invoice::from_positions(ctx, positions))
    }
}