use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::Serialize;
use crate::context::BillingContext;
use crate::position::{BillingPosition, PositionCategory};
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct Invoice {
pub context: BillingContext,
pub positions: Vec<BillingPosition>,
pub netto_eur: Decimal,
pub mwst_eur: Decimal,
pub brutto_eur: Decimal,
}
impl Invoice {
#[must_use]
pub fn from_positions(context: BillingContext, positions: Vec<BillingPosition>) -> Self {
let netto_eur: Decimal = positions
.iter()
.filter(|p| p.category != PositionCategory::Tax)
.map(|p| p.net_eur)
.sum();
let mwst_eur: Decimal = positions
.iter()
.filter(|p| p.category == PositionCategory::Tax)
.map(|p| p.net_eur)
.sum();
let brutto_eur = netto_eur + mwst_eur;
Self {
context,
positions,
netto_eur,
mwst_eur,
brutto_eur,
}
}
#[must_use]
pub fn total_by_tag(&self, tag: &str) -> Decimal {
BillingPosition::total_by_tag(&self.positions, tag)
}
pub fn positions_by_tag<'a>(
&'a self,
tag: &'a str,
) -> impl Iterator<Item = &'a BillingPosition> {
self.positions.iter().filter(move |p| p.has_tag(tag))
}
pub fn assert_valid(&self) {
let expected = self.netto_eur + self.mwst_eur;
let diff = (self.brutto_eur - expected).abs();
assert!(
diff < dec!(0.001),
"Invoice invariant violated: netto {:.5} + mwst {:.5} = {:.5} != brutto {:.5}",
self.netto_eur,
self.mwst_eur,
expected,
self.brutto_eur
);
}
#[must_use]
pub fn to_rechnung_json(&self) -> serde_json::Value {
let ctx = &self.context;
let pos_json: Vec<serde_json::Value> = self
.positions
.iter()
.enumerate()
.map(|(i, p)| {
serde_json::json!({
"_typ": "RECHNUNGSPOSITION",
"positionsnummer": i + 1,
"positionstext": p.description,
"rechtlicheGrundlage": p.legal_basis,
"positionsMenge": {
"_typ": "MENGE",
"wert": p.quantity.to_string(),
"einheit": p.unit
},
"einzelpreis": {
"_typ": "PREIS",
"wert": p.unit_price_eur.to_string(),
"einheit": "EUR"
},
"gesamtpreis": {
"_typ": "BETRAG",
"wert": p.net_eur.to_string(),
"waehrung": "EUR"
},
"positionstyp": p.tags.first().map(String::as_str).unwrap_or("POSITION"),
"kategorie": format!("{:?}", p.category),
})
})
.collect();
let zahlungsziel = ctx.period_to + time::Duration::days(14);
let zusatz_attribute: Vec<serde_json::Value> = self
.positions
.iter()
.filter(|p| p.has_tag("gasqualitaet") && p.category == PositionCategory::Info)
.map(|p| {
serde_json::json!({
"_typ": "ZUSATZ_ATTRIBUT",
"name": "gasqualitaet",
"wert": p.legal_basis.as_deref().unwrap_or("")
})
})
.collect();
serde_json::json!({
"_typ": "RECHNUNG",
"rechnungsnummer": ctx.rechnungsnummer,
"rechnungsart": ctx.invoice_type.rechnungsart(),
"rechnungsdatum": ctx.period_to.to_string(), "originalRechnungsId": ctx.invoice_type.original_invoice_id(),
"marktlokationsId": ctx.malo_id,
"herausgeber": {
"_typ": "MARKTTEILNEHMER",
"marktpartnercode": ctx.lf_mp_id
},
"vertragsId": ctx.contract_id,
"rechnungsperiode": {
"_typ": "ZEITRAUM",
"startdatum": ctx.period_from.to_string(),
"enddatum": ctx.period_to.to_string()
},
"rechnungspositionen": pos_json,
"zusatzAttribute": if zusatz_attribute.is_empty() { serde_json::Value::Null } else { serde_json::json!(zusatz_attribute) },
"gesamtnetto": { "_typ": "BETRAG", "wert": self.netto_eur.to_string(), "waehrung": "EUR" },
"gesamtsteuer": { "_typ": "BETRAG", "wert": self.mwst_eur.to_string(), "waehrung": "EUR" },
"gesamtbrutto": { "_typ": "BETRAG", "wert": self.brutto_eur.to_string(), "waehrung": "EUR" },
"rechnungsempfaenger": {
"_typ": "MARKTTEILNEHMER",
"externeKundenId": ctx.malo_id
},
"zahlungsziel": zahlungsziel.to_string()
})
}
}