use billing::BillingError;
use crate::context::BillingContext;
use crate::invoice::Invoice;
use crate::position::BillingPosition;
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
}
pub fn bill(
self,
ctx: BillingContext,
quantities: &Quantities,
) -> Result<Invoice, BillingError> {
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());
let current_brutto = current_invoice.brutto_eur;
if current_brutto < min_brutto {
let gap_brutto = min_brutto - current_brutto;
let netto_sum: rust_decimal::Decimal = positions
.iter()
.filter(|p| {
!matches!(
p.category,
crate::position::PositionCategory::Tax
| crate::position::PositionCategory::Abschlag
| crate::position::PositionCategory::Info
)
})
.map(|p| p.net_eur)
.sum();
let tax_sum: rust_decimal::Decimal = positions
.iter()
.filter(|p| p.category == crate::position::PositionCategory::Tax)
.map(|p| p.net_eur)
.sum();
let mwst_rate = if netto_sum.is_zero() {
ctx.regulatory_rates.mwst_rate
} else {
(tax_sum / netto_sum).abs()
};
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));
}
}
if ctx.invoice_type.is_reversal() {
negate_positions(&mut positions);
}
Ok(Invoice::from_positions(ctx, positions))
}
}
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;
}
}