use rust_decimal::Decimal;
use rust_decimal_macros::dec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PositionCategory {
Commodity,
GridCharge,
Levy,
Tax,
Credit,
Discount,
Fee,
Info,
Abschlag,
Bonus,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BillingPosition {
pub description: String,
pub legal_basis: Option<String>,
pub quantity: Decimal,
pub unit: String,
pub unit_price_eur: Decimal,
pub net_eur: Decimal,
pub category: PositionCategory,
pub tags: Vec<String>,
#[serde(default)]
pub applicable_tax_rate: Option<Decimal>,
}
impl BillingPosition {
#[must_use]
pub fn debit(
description: impl Into<String>,
quantity: Decimal,
unit: impl Into<String>,
unit_price_eur: Decimal,
category: PositionCategory,
) -> Self {
let net_eur = validated_eur(quantity * unit_price_eur);
Self {
description: description.into(),
legal_basis: None,
quantity,
unit: unit.into(),
unit_price_eur,
net_eur,
category,
tags: Vec::new(),
applicable_tax_rate: None,
}
}
#[must_use]
pub fn credit(
description: impl Into<String>,
quantity: Decimal,
unit: impl Into<String>,
abs_rate_eur: Decimal,
category: PositionCategory,
) -> Self {
let net_eur = -validated_eur(quantity * abs_rate_eur);
Self {
description: description.into(),
legal_basis: None,
quantity,
unit: unit.into(),
unit_price_eur: -abs_rate_eur,
net_eur,
category,
tags: Vec::new(),
applicable_tax_rate: None,
}
}
#[must_use]
pub fn with_legal_basis(mut self, basis: impl Into<String>) -> Self {
self.legal_basis = Some(basis.into());
self
}
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
#[must_use]
pub fn with_tax_rate(mut self, rate: Decimal) -> Self {
self.applicable_tax_rate = Some(rate);
self
}
#[must_use]
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
#[must_use]
pub fn net_total(positions: &[BillingPosition]) -> Decimal {
positions.iter().map(|p| p.net_eur).sum()
}
#[must_use]
pub fn total_by_tag(positions: &[BillingPosition], tag: &str) -> Decimal {
positions
.iter()
.filter(|p| p.has_tag(tag))
.map(|p| p.net_eur)
.sum()
}
}
pub(crate) fn validated_eur(amount: Decimal) -> Decimal {
billing::EuroAmount::checked_from_decimal(amount)
.map(|a| a.to_decimal())
.unwrap_or(Decimal::ZERO)
}
pub(crate) fn grundpreis_position(
label: impl Into<String>,
daily_rate_eur: Decimal,
days: i64,
legal_basis: &'static str,
tags: &[&'static str],
) -> BillingPosition {
let mut p = BillingPosition::debit(
label,
Decimal::from(days),
"Tage",
daily_rate_eur,
PositionCategory::Commodity,
)
.with_legal_basis(legal_basis)
.with_tag("commodity")
.with_tag("grundpreis");
for tag in tags {
p = p.with_tag(*tag);
}
p
}
pub(crate) fn arbeitspreis_position(
label: impl Into<String>,
kwh: Decimal,
rate_ct_kwh: Decimal,
unit: &'static str,
legal_basis: &'static str,
tags: &[&'static str],
) -> BillingPosition {
let mut p = BillingPosition::debit(
label,
kwh,
unit,
rate_ct_kwh / dec!(100),
PositionCategory::Commodity,
)
.with_legal_basis(legal_basis)
.with_tag("commodity")
.with_tag("arbeitspreis");
for tag in tags {
p = p.with_tag(*tag);
}
p
}
pub(crate) fn levy_position(
label: impl Into<String>,
quantity: Decimal,
unit: &'static str,
rate_ct: Decimal,
legal_basis: &'static str,
tag: &'static str,
) -> BillingPosition {
BillingPosition::debit(
label,
quantity,
unit,
rate_ct / dec!(100),
PositionCategory::Levy,
)
.with_legal_basis(legal_basis)
.with_tag("levy")
.with_tag(tag)
}