use billing::BillingError;
use crate::context::BillingContext;
use crate::invoice::Invoice;
use crate::position::{BillingPosition, BillingWarning, WarningSeverity};
use crate::provider::BillingProvider;
use crate::quantities::Quantities;
#[derive(Default)]
pub struct BillingEngine {
providers: Vec<Box<dyn BillingProvider>>,
}
impl BillingEngine {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
#[allow(clippy::should_implement_trait)] pub fn add<P: BillingProvider + 'static>(mut self, provider: P) -> Self {
self.providers.push(Box::new(provider));
self
}
#[must_use]
pub fn validate(&self, ctx: &BillingContext, quantities: &Quantities) -> Vec<BillingWarning> {
self.providers
.iter()
.flat_map(|p| p.validate_warnings(ctx, quantities))
.collect()
}
pub fn bill_batch(
&self,
batch: Vec<(BillingContext, Quantities)>,
) -> Vec<Result<Invoice, BillingError>> {
batch
.into_iter()
.map(|(ctx, quantities)| self.bill(ctx, &quantities))
.collect()
}
pub fn bill(
&self,
ctx: BillingContext,
quantities: &Quantities,
) -> Result<Invoice, BillingError> {
let mut warnings: Vec<BillingWarning> = Vec::new();
for provider in self.providers.iter() {
let w = provider.validate_warnings(&ctx, quantities);
if w.iter().any(|x| x.severity == WarningSeverity::Error) {
warnings.extend(w);
let reasons: Vec<&str> = warnings
.iter()
.filter(|x| x.severity == WarningSeverity::Error)
.map(|x| x.message.as_str())
.collect();
return Err(BillingError::InvalidInput {
reason: reasons.join("; "),
});
}
warnings.extend(w);
}
let mut positions: Vec<BillingPosition> = Vec::new();
for provider in self.providers.iter().filter(|p| !p.is_tax_pass()) {
let new = provider.bill(&ctx, quantities, &positions)?;
positions.extend(new);
}
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);
}
for abschlag in &ctx.abschlage {
let label = abschlag
.beschreibung
.clone()
.unwrap_or_else(|| format!("Abschlag {}", abschlag.datum));
positions.push(
crate::position::BillingPosition::debit(
label,
rust_decimal::Decimal::ONE,
"EUR",
-abschlag.betrag_eur, crate::position::PositionCategory::Abschlag,
)
.with_legal_basis("§41 EnWG"),
);
}
if let Some(min_brutto) = ctx.minimum_invoice_eur_brutto {
let current_invoice = Invoice::from_positions(ctx.clone(), positions.clone(), vec![]);
let current_brutto = current_invoice.brutto_eur;
if current_brutto < min_brutto {
let gap_brutto = min_brutto - current_brutto;
let mwst_rate = ctx.regulatory_rates.mwst_rate;
let divisor = rust_decimal::Decimal::ONE + mwst_rate;
let gap_netto = if divisor.is_zero() {
gap_brutto
} else {
(gap_brutto / divisor).round_dp(5)
};
let mut positions2: Vec<BillingPosition> = positions
.iter()
.filter(|p| p.category != crate::position::PositionCategory::Tax)
.cloned()
.collect();
positions2.push(
crate::position::BillingPosition::debit(
format!("Mindestbetrag (Minimum {min_brutto:.2}\u{202f}EUR brutto)"),
rust_decimal::Decimal::ONE,
"EUR",
gap_netto,
crate::position::PositionCategory::Commodity,
)
.with_legal_basis("Vertraglich")
.with_tag("mindestbetrag"),
);
let pre_tax2: Vec<BillingPosition> = positions2.clone();
for provider in self.providers.iter().filter(|p| p.is_tax_pass()) {
let new = provider.bill(&ctx, quantities, &pre_tax2)?;
positions2.extend(new);
}
for p in positions
.iter()
.filter(|p| p.category == crate::position::PositionCategory::Abschlag)
{
positions2.push(p.clone());
}
return Ok(Invoice::from_positions(ctx, positions2, warnings));
}
}
if ctx.invoice_type.is_reversal() {
negate_positions(&mut positions);
}
Ok(Invoice::from_positions(ctx, positions, warnings))
}
}
fn negate_positions(positions: &mut [crate::position::BillingPosition]) {
for p in positions.iter_mut() {
p.net_eur = -p.net_eur;
p.unit_price_eur = -p.unit_price_eur;
}
}